) => {
setSelectedOverlayAction('primary')
+ statusRef.current = nextStatus
setStatus(nextStatus)
}, [])
+ const resumeArena = useCallback(() => {
+ statusRef.current = 'playing'
+ setStatus('playing')
+ }, [])
+
const abilities = useMemo(
() => {
const baseAbilities = abilitiesForHealer(
@@ -268,11 +275,16 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
return
}
if (overlayAction === 'menu') {
+ if (statusRef.current === 'paused') {
+ const returnToMainMenu = onMainMenu ?? onBack
+ returnToMainMenu()
+ return
+ }
onBack()
return
}
if (statusRef.current === 'paused') {
- setStatus('playing')
+ resumeArena()
return
}
if (statusRef.current === 'victory' && roguelikeRun) {
@@ -280,10 +292,10 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
return
}
resetArena()
- }, [onBack, onPvpRequeue, resetArena, roguelikeRun])
+ }, [onBack, onMainMenu, onPvpRequeue, resetArena, resumeArena, roguelikeRun])
useGameAction((action, device) => {
- if (device === 'controller' && statusRef.current !== 'playing') {
+ if (statusRef.current !== 'playing' && (device === 'controller' || device === 'pc')) {
if (action.startsWith('navigate')) {
moveOverlaySelection(action)
return
@@ -293,10 +305,15 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
return
}
if (action === 'back') {
- if (statusRef.current === 'paused') setStatus('playing')
+ if (statusRef.current === 'paused') resumeArena()
return
}
}
+ if (action === 'pause' || action === 'back') {
+ if (statusRef.current === 'playing') showOverlay('paused')
+ else if (statusRef.current === 'paused') resumeArena()
+ return
+ }
if (statusRef.current === 'playing' && action.startsWith('ability')) {
const ability = abilities[IWT2_ABILITY_ACTIONS.indexOf(action)]
if (ability) castAbility(ability)
@@ -325,11 +342,6 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
if (target) setSelectedPartyId(target.id)
return
}
- if (device !== 'controller') return
- if (action === 'pause' || action === 'back') {
- if (statusRef.current === 'playing') showOverlay('paused')
- else if (statusRef.current === 'paused') setStatus('playing')
- }
})
const onStep = useCallback((movement: MovementVector, dtSeconds: number) => {
@@ -438,12 +450,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
const pauseTitle = arenaPauseTitle(roguelikeRun, modeLabel ?? bossMetadata.name)
const pauseCopy = roguelikeRun?.variant === 'pvp'
? undefined
- : 'Combat is stopped. Resume the fight or leave the current run.'
- const pauseLeaveLabel = roguelikeRun?.variant === 'pvp'
- ? 'Leave'
- : roguelikeRun
- ? 'Leave Roguelike'
- : `Leave ${modeLabel ?? 'Arena'}`
+ : 'Combat is stopped. Continue the fight or return to the main menu.'
const dualScreenState = useMemo(
() => buildIwt2DualScreenCombatState({
abilities,
@@ -508,23 +515,21 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
{pauseCopy && {pauseCopy}
}
diff --git a/src/modes/iwt2/screens/Iwt2ShellScreens.tsx b/src/modes/iwt2/screens/Iwt2ShellScreens.tsx
index 5f80f35..d25f155 100644
--- a/src/modes/iwt2/screens/Iwt2ShellScreens.tsx
+++ b/src/modes/iwt2/screens/Iwt2ShellScreens.tsx
@@ -54,6 +54,7 @@ import {
iwt2GearUpgradeCosts,
iwt2InfusionCosts,
isIwt2InfusionUnlocked,
+ type Iwt2GearStatId,
type Iwt2GearSlotId,
} from '../content/gear'
import {
@@ -2021,6 +2022,9 @@ export function Iwt2GearUpgradeScreen({
const classProgress = save.gearProgress[selectedClassId]
const selectedSlot = classProgress.slots[selectedSlotId]
const selectedRecipe = IWT2_GEAR_SLOT_RECIPES[selectedClassId][selectedSlotId]
+ const selectedBonus = gearBonusSummary(selectedRecipe.statId, selectedSlot.level, selectedClassId)
+ const nextLevel = Math.min(5, selectedSlot.level + 1)
+ const nextBonus = gearBonusSummary(selectedRecipe.statId, nextLevel, selectedClassId)
const selectedClassName = gearClassDisplayName(selectedClassId, save)
const upgradeCosts = iwt2GearUpgradeCosts(selectedClassId, selectedSlotId, selectedSlot.level)
const canUpgrade = selectedSlot.level < 5 && canAffordIwt2Costs(save, upgradeCosts)
@@ -2186,6 +2190,16 @@ export function Iwt2GearUpgradeScreen({
{IWT2_GEAR_SLOT_LABELS[selectedSlotId]} +{selectedSlot.level}
{IWT2_GEAR_STAT_LABELS[selectedRecipe.statId]} from {bossName(selectedRecipe.primaryBossId)} and {bossName(selectedRecipe.secondaryBossId)} coins.
+
+
+ Current bonus
+ {selectedBonus.text}
+
+
+ {selectedSlot.level >= 5 ? 'Max rank' : `Upgrade preview +${selectedSlot.level} -> +${nextLevel}`}
+ {selectedSlot.level >= 5 ? infusionAnchorText(classProgress.slots[selectedSlotId].level) : `${selectedBonus.label}: ${selectedBonus.value} -> ${nextBonus.value}`}
+
+
{upgradeCosts.length === 0 ? (
Max rank reached.
@@ -2245,6 +2259,38 @@ function gearClassSubtitle(classId: Iwt2PlayerClassId, save: Iwt2Save): string {
return role === 'damage' ? 'Damage' : 'Tank'
}
+function gearBonusSummary(
+ statId: Iwt2GearStatId,
+ level: number,
+ classId: Iwt2PlayerClassId,
+): { label: string, text: string, value: string } {
+ const bonus = Math.max(0, level)
+ if (statId === 'maxHealth') return gearBonus('Max health', `+${bonus * 4}%`)
+ if (statId === 'moveSpeed') return gearBonus('Move speed', `+${formatPercent(bonus * 2.5)}%`)
+ if (statId === 'damage') return gearBonus('Attack damage', `+${bonus * 4}%`)
+ if (statId === 'attackCooldown') {
+ const label = classId === 'healer' ? 'Ability cooldown' : 'Attack cooldown'
+ return gearBonus(label, `-${bonus * 3}%`)
+ }
+ if (statId === 'projectileSpeed') return gearBonus('Projectile speed', `+${bonus * 3}%`)
+ if (statId === 'hazardDamageTaken') return gearBonus('Hazard damage taken', `-${bonus * 3}%`)
+ if (statId === 'stunResist') return gearBonus('Stun/knockdown duration', `-${bonus * 8}%`)
+ if (statId === 'healingPower') return gearBonus('Ability healing', `+${bonus * 4}%`)
+ return gearBonus('Bonus', '+0%')
+}
+
+function gearBonus(label: string, value: string): { label: string, text: string, value: string } {
+ return { label, text: `${label} ${value}`, value }
+}
+
+function infusionAnchorText(level: number): string {
+ return level >= 5 ? 'Infusion anchor available.' : 'No further bonus.'
+}
+
+function formatPercent(value: number): string {
+ return Number.isInteger(value) ? String(value) : value.toFixed(1)
+}
+
function clampToEnabled(actions: Iwt2NavAction[], index: number) {
if (actions.length === 0) return 0
const bounded = Math.min(Math.max(0, index), actions.length - 1)
diff --git a/src/modes/iwt2/sim/arena.ts b/src/modes/iwt2/sim/arena.ts
index 3542a35..d02b5d2 100644
--- a/src/modes/iwt2/sim/arena.ts
+++ b/src/modes/iwt2/sim/arena.ts
@@ -846,6 +846,13 @@ function applyPartyAttacks(
damage: member.attackDamage,
remainingSeconds: 1.2,
})
+ events.push({
+ id: 0,
+ time,
+ type: 'partyAttack',
+ sourceId: member.id,
+ targetId: target.id,
+ })
projectileId += 1
return {
...member,
@@ -876,6 +883,13 @@ function applyPartyAttacks(
targetId: target.id,
value: damage,
})
+ events.push({
+ id: 0,
+ time,
+ type: 'partyAttack',
+ sourceId: member.id,
+ targetId: target.id,
+ })
if (defeated) {
events.push({
id: 0,
diff --git a/src/modes/iwt2/sim/equipmentStats.ts b/src/modes/iwt2/sim/equipmentStats.ts
index bb9825f..0f412e1 100644
--- a/src/modes/iwt2/sim/equipmentStats.ts
+++ b/src/modes/iwt2/sim/equipmentStats.ts
@@ -43,7 +43,16 @@ function applyMemberGearStats(
gearProgress: Iwt2GearProgress,
): Iwt2PartyEntityState {
const classProgress = gearProgress[member.classId]
- let next = { ...member }
+ let next: Iwt2PartyEntityState = {
+ ...member,
+ gearLevels: {
+ weapon: classProgress.slots.weapon.level,
+ helmet: classProgress.slots.helmet.level,
+ chest: classProgress.slots.chest.level,
+ legs: classProgress.slots.legs.level,
+ feet: classProgress.slots.feet.level,
+ },
+ }
for (const slotId of IWT2_GEAR_SLOTS) {
const level = classProgress.slots[slotId].level
if (level <= 0) continue
diff --git a/src/modes/iwt2/sim/roguelikePressure.ts b/src/modes/iwt2/sim/roguelikePressure.ts
index 57b65db..9c33b4e 100644
--- a/src/modes/iwt2/sim/roguelikePressure.ts
+++ b/src/modes/iwt2/sim/roguelikePressure.ts
@@ -9,18 +9,13 @@ import { applyPartyDamageToMember, createArenaEvent } from './mechanics'
const ROGUELIKE_PRESSURE_INTERVAL_SECONDS = 5
const ROGUELIKE_PRESSURE_BASE_DAMAGE = 9
const ROGUELIKE_PRESSURE_INITIAL_STAGE_DAMAGE = 2
-const ROGUELIKE_DUNGEON_DAMAGE_BASE_SCALE = 1.25
-const ROGUELIKE_RAID_DAMAGE_BASE_SCALE = 1.45
const ROGUELIKE_PRESSURE_SOURCE_ID = 'roguelike-pressure'
export function roguelikeIncomingDamageScale(
_stage: number,
- contentType: Iwt2RoguelikePressureContentType,
+ _contentType: Iwt2RoguelikePressureContentType,
): number {
- const base = contentType === 'dungeon'
- ? ROGUELIKE_DUNGEON_DAMAGE_BASE_SCALE
- : ROGUELIKE_RAID_DAMAGE_BASE_SCALE
- return base
+ return 1
}
export function createRoguelikePressureState(
diff --git a/src/modes/iwt2/sim/types.ts b/src/modes/iwt2/sim/types.ts
index 145ce6e..bdba562 100644
--- a/src/modes/iwt2/sim/types.ts
+++ b/src/modes/iwt2/sim/types.ts
@@ -1,5 +1,6 @@
import type { Iwt2BossId } from '../content/bosses'
import type { Iwt2PlayerClassId } from '../content/classes'
+import type { Iwt2GearLevel, Iwt2GearSlotId } from '../content/gear'
import type { Iwt2InfusionAbilityId } from '../content/infusionAbilities'
export type Iwt2Vec2 = {
@@ -70,6 +71,7 @@ export type Iwt2PartyEntityState = {
mana: number
maxMana: number
damageDone: number
+ gearLevels?: Partial>
attackCooldownRemaining: number
castSecondsRemaining: number
attackReady: boolean
@@ -283,6 +285,7 @@ export type Iwt2GroundHazardState = {
export type Iwt2ArenaEventType =
| 'bossDamaged'
| 'healerSpellCast'
+ | 'partyAttack'
| 'partyHealed'
| 'partyDamaged'
| 'partyStunned'
diff --git a/tmp/iwt2-layer-generation/warrior-axe-source.png b/tmp/iwt2-layer-generation/warrior-axe-source.png
new file mode 100644
index 0000000..3dfb217
Binary files /dev/null and b/tmp/iwt2-layer-generation/warrior-axe-source.png differ
diff --git a/tmp/iwt2-layer-generation/warrior-body-source.png b/tmp/iwt2-layer-generation/warrior-body-source.png
new file mode 100644
index 0000000..303edda
Binary files /dev/null and b/tmp/iwt2-layer-generation/warrior-body-source.png differ
diff --git a/tmp/iwt2-layer-generation/warrior-layer-preview.png b/tmp/iwt2-layer-generation/warrior-layer-preview.png
new file mode 100644
index 0000000..a0c87c4
Binary files /dev/null and b/tmp/iwt2-layer-generation/warrior-layer-preview.png differ
diff --git a/tmp/iwt2-layer-generation/warrior-runtime-layer-preview.png b/tmp/iwt2-layer-generation/warrior-runtime-layer-preview.png
new file mode 100644
index 0000000..6328ede
Binary files /dev/null and b/tmp/iwt2-layer-generation/warrior-runtime-layer-preview.png differ