Android build v1.1.31

This commit is contained in:
Warren H
2026-07-06 23:49:14 -04:00
parent f48e6130b8
commit a6cca2810c
14 changed files with 219 additions and 96 deletions
Binary file not shown.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.warren.iwanttoheal" applicationId "com.warren.iwanttoheal"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 111 versionCode 112
versionName "1.1.30" versionName "1.1.31"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+59 -15
View File
@@ -212,6 +212,8 @@
} }
.iwt2-arena-layout { .iwt2-arena-layout {
display: grid;
grid-template-columns: 172px minmax(0, 1fr);
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
@@ -221,6 +223,7 @@
.iwt2-arena-stage { .iwt2-arena-stage {
background: #090c10; background: #090c10;
border: 0; border: 0;
grid-column: 2;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
@@ -317,6 +320,22 @@
top: 0; top: 0;
} }
.iwt2-bar em {
color: #fff7df;
font-size: 0.56rem;
font-style: normal;
font-weight: 800;
left: 0;
line-height: 1;
pointer-events: none;
position: absolute;
right: 0;
text-align: center;
text-shadow: 0 1px 0 #050608;
top: 50%;
transform: translateY(-50%);
}
.iwt2-bar.boss span { .iwt2-bar.boss span {
background: #dc5162; background: #dc5162;
} }
@@ -337,17 +356,18 @@
.iwt2-party-list { .iwt2-party-list {
background: rgba(15, 19, 26, 0.88); background: rgba(15, 19, 26, 0.88);
border: 0; border: 0;
left: 28px; bottom: 0;
max-height: calc(100% - 112px); left: 0;
max-height: none;
outline: 0; outline: 0;
padding: 6px; padding: 6px;
position: absolute; position: fixed;
top: 28px; top: 0;
width: 172px; width: 172px;
z-index: 4; z-index: 4;
display: grid; display: grid;
gap: 8px; gap: 8px;
align-content: start; align-content: center;
overflow: auto; overflow: auto;
} }
@@ -1099,7 +1119,8 @@
.iwt2-gear-class, .iwt2-gear-class,
.iwt2-gear-slot, .iwt2-gear-slot,
.iwt2-infusion-row, .iwt2-infusion-row,
.iwt2-gear-upgrade-button { .iwt2-gear-upgrade-button,
.iwt2-gear-back {
background: #151922; background: #151922;
border: 2px solid #08090d; border: 2px solid #08090d;
color: var(--ink); color: var(--ink);
@@ -1177,7 +1198,8 @@
.iwt2-gear-class.game-selected, .iwt2-gear-class.game-selected,
.iwt2-gear-slot.game-selected, .iwt2-gear-slot.game-selected,
.iwt2-infusion-row.game-selected, .iwt2-infusion-row.game-selected,
.iwt2-gear-upgrade-button.game-selected { .iwt2-gear-upgrade-button.game-selected,
.iwt2-gear-back.game-selected {
outline-color: #e5b95f; outline-color: #e5b95f;
} }
@@ -1187,6 +1209,12 @@
text-align: center; text-align: center;
} }
.iwt2-gear-back {
min-height: 36px;
padding: 8px 10px;
text-align: center;
}
.iwt2-gear-upgrade-button:disabled, .iwt2-gear-upgrade-button:disabled,
.iwt2-infusion-row:disabled { .iwt2-infusion-row:disabled {
color: var(--muted); color: var(--muted);
@@ -1401,6 +1429,10 @@
} }
@media (max-width: 1000px) and (max-height: 620px) { @media (max-width: 1000px) and (max-height: 620px) {
.iwt2-arena-layout {
grid-template-columns: 154px minmax(0, 1fr);
}
.game-shell.iwt2-shell { .game-shell.iwt2-shell {
padding: 6px 0; padding: 6px 0;
width: min(100%, calc(100% - 20px)); width: min(100%, calc(100% - 20px));
@@ -1640,6 +1672,11 @@
padding: 7px 9px; padding: 7px 9px;
} }
.iwt2-gear-back {
min-height: 30px;
padding: 5px 8px;
}
.iwt2-gear-detail { .iwt2-gear-detail {
gap: 6px; gap: 6px;
padding: 7px; padding: 7px;
@@ -1735,20 +1772,22 @@
} }
.iwt2-party-list { .iwt2-party-list {
gap: 5px; align-content: center;
left: 10px; gap: 7px;
max-height: calc(100% - 84px); bottom: 0;
left: 0;
max-height: none;
overflow: hidden; overflow: hidden;
padding: 4px; padding: 4px;
top: 10px; top: 0;
width: 154px; width: 154px;
} }
.iwt2-party-row { .iwt2-party-row {
gap: 5px; gap: 5px;
grid-template-columns: 22px minmax(0, 1fr); grid-template-columns: 22px minmax(0, 1fr);
height: 62px; height: 70px;
min-height: 62px; min-height: 70px;
padding: 4px; padding: 4px;
} }
@@ -1782,8 +1821,12 @@
} }
.iwt2-party-row .iwt2-bar { .iwt2-party-row .iwt2-bar {
height: 7px; height: 12px;
margin-top: 2px; margin-top: 3px;
}
.iwt2-party-row .iwt2-bar.mana {
height: 8px;
} }
.iwt2-party-effects { .iwt2-party-effects {
@@ -1803,6 +1846,7 @@
.iwt2-ability-bar { .iwt2-ability-bar {
bottom: 8px; bottom: 8px;
display: none;
gap: 8px; gap: 8px;
grid-template-columns: repeat(6, 36px); grid-template-columns: repeat(6, 36px);
max-width: 256px; max-width: 256px;
+18 -2
View File
@@ -435,6 +435,10 @@ function hasDedicatedGameNavigation() {
).some(isVisible) ).some(isVisible)
} }
function isDedicatedNavigationAction(action: InputAction) {
return action.startsWith('navigate') || action === 'confirm' || action === 'back'
}
function dispatchGameAction(action: InputAction, device: InputDevice) { function dispatchGameAction(action: InputAction, device: InputDevice) {
window.dispatchEvent(new CustomEvent(GAME_ACTION_EVENT, { window.dispatchEvent(new CustomEvent(GAME_ACTION_EVENT, {
detail: { action, device }, detail: { action, device },
@@ -621,7 +625,12 @@ export function InputProvider({ children }: { children: ReactNode }) {
setLastDevice(device) setLastDevice(device)
document.documentElement.dataset.inputDevice = device document.documentElement.dataset.inputDevice = device
if (controllerUiInput && dedicatedNavAction && hasDedicatedGameNavigation()) { if (
device === 'controller'
&& dedicatedNavAction
&& !keyboardInputRef.current
&& hasDedicatedGameNavigation()
) {
if (document.activeElement instanceof HTMLElement) document.activeElement.blur() if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
dispatchGameAction(action, device) dispatchGameAction(action, device)
return return
@@ -709,6 +718,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
'navigateRight', 'navigateRight',
'confirm', 'confirm',
'back', 'back',
'pause',
] satisfies InputAction[] ] satisfies InputAction[]
const directTargetActions = [ const directTargetActions = [
'targetParty1', 'targetParty1',
@@ -738,7 +748,13 @@ export function InputProvider({ children }: { children: ReactNode }) {
'navigateLeft', 'navigateLeft',
'navigateRight', 'navigateRight',
] satisfies InputAction[] ] satisfies InputAction[]
const action = DPAD_NAV_ACTIONS[token] && (!combatActive || uiOverlay) const dedicatedNavigationActive = hasDedicatedGameNavigation()
const action = dedicatedNavigationActive
? uiPriority.find((candidate) => bindingsRef.current.controller[candidate] === token)
?? (DPAD_NAV_ACTIONS[token] && isDedicatedNavigationAction(DPAD_NAV_ACTIONS[token])
? DPAD_NAV_ACTIONS[token]
: undefined)
: DPAD_NAV_ACTIONS[token] && (!combatActive || uiOverlay)
? DPAD_NAV_ACTIONS[token] ? DPAD_NAV_ACTIONS[token]
: uiOverlay : uiOverlay
? uiPriority.find((candidate) => bindingsRef.current.controller[candidate] === token) ? uiPriority.find((candidate) => bindingsRef.current.controller[candidate] === token)
+3
View File
@@ -1,11 +1,13 @@
export function ArenaBar({ export function ArenaBar({
className = '', className = '',
current, current,
label,
max, max,
shield = 0, shield = 0,
}: { }: {
className?: string className?: string
current: number current: number
label?: string
max: number max: number
shield?: number shield?: number
}) { }) {
@@ -15,6 +17,7 @@ export function ArenaBar({
<div className={`iwt2-bar ${className}`}> <div className={`iwt2-bar ${className}`}>
<span style={{ width: `${percent}%` }} /> <span style={{ width: `${percent}%` }} />
{shieldPercent > 0 && <i style={{ width: `${shieldPercent}%` }} />} {shieldPercent > 0 && <i style={{ width: `${shieldPercent}%` }} />}
{label && <em>{label}</em>}
</div> </div>
) )
} }
+1 -2
View File
@@ -46,9 +46,8 @@ export function PartyFrames({
<div> <div>
<div className="iwt2-party-row-title"> <div className="iwt2-party-row-title">
<strong>{meta.name}</strong> <strong>{meta.name}</strong>
<small>{Math.ceil(member.health)}</small>
</div> </div>
<ArenaBar className="hp" current={member.health} max={member.maxHealth} shield={member.shield} /> <ArenaBar className="hp" current={member.health} label={`${Math.ceil(member.maxHealth)}`} max={member.maxHealth} shield={member.shield} />
{member.maxMana > 0 && ( {member.maxMana > 0 && (
<ArenaBar className="mana" current={member.mana} max={member.maxMana} /> <ArenaBar className="mana" current={member.mana} max={member.maxMana} />
)} )}
+25 -2
View File
@@ -12,7 +12,7 @@ import {
tickIwt2Arena, tickIwt2Arena,
type Iwt2ArenaState, type Iwt2ArenaState,
} from '../sim/arenaState' } from '../sim/arenaState'
import type { Iwt2EntityId } from '../sim' import type { Iwt2ArenaBounds, Iwt2EntityId } from '../sim'
import { castIwt2HealerAbility } from '../sim' import { castIwt2HealerAbility } from '../sim'
import { PhaserArena } from '../render/PhaserArena' import { PhaserArena } from '../render/PhaserArena'
import { import {
@@ -64,6 +64,10 @@ const PVP_RESULT_OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [
] ]
const EMPTY_ROGUELIKE_BUFFS: Iwt2RoguelikeSelfBuffId[] = [] const EMPTY_ROGUELIKE_BUFFS: Iwt2RoguelikeSelfBuffId[] = []
const IWT2_PVP_BOSS_HEALTH_MULTIPLIER = 0.7 const IWT2_PVP_BOSS_HEALTH_MULTIPLIER = 0.7
const IWT2_TOP_PARTY_RAIL_WIDTH = 172
const IWT2_THOR_TOP_PARTY_RAIL_WIDTH = 154
const IWT2_THOR_TOP_BREAKPOINT_WIDTH = 1000
const IWT2_THOR_TOP_BREAKPOINT_HEIGHT = 620
type BossArenaScreenProps = { type BossArenaScreenProps = {
bossId: Iwt2BossId bossId: Iwt2BossId
@@ -103,6 +107,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
const pvpBossHealthScale = pvpRoguelike ? IWT2_PVP_BOSS_HEALTH_MULTIPLIER : 1 const pvpBossHealthScale = pvpRoguelike ? IWT2_PVP_BOSS_HEALTH_MULTIPLIER : 1
const combinedBossHealthScale = bossHealthScale * difficultyHealthScale * pvpBossHealthScale const combinedBossHealthScale = bossHealthScale * difficultyHealthScale * pvpBossHealthScale
const combinedDamageScale = difficultyDamageScale * roguelikeDamageScale const combinedDamageScale = difficultyDamageScale * roguelikeDamageScale
const arenaBounds = useMemo(() => createTopScreenArenaBounds(), [])
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createArenaState( const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createArenaState(
bossId, bossId,
bossIds, bossIds,
@@ -111,6 +116,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
roguelikeBuffs, roguelikeBuffs,
createPressureState(roguelikeStage, roguelikeContentType), createPressureState(roguelikeStage, roguelikeContentType),
pveGearActive ? save.gearProgress : undefined, pveGearActive ? save.gearProgress : undefined,
arenaBounds,
)) ))
const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => ( const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
pvpRoguelike pvpRoguelike
@@ -120,6 +126,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
combinedBossHealthScale, combinedBossHealthScale,
combinedDamageScale, combinedDamageScale,
createPressureState(roguelikeStage, roguelikeContentType), createPressureState(roguelikeStage, roguelikeContentType),
arenaBounds,
) )
: null : null
)) ))
@@ -183,6 +190,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
roguelikeBuffs, roguelikeBuffs,
pressureState, pressureState,
pveGearActive ? save.gearProgress : undefined, pveGearActive ? save.gearProgress : undefined,
arenaBounds,
) )
const nextOpponentState = pvpRoguelike const nextOpponentState = pvpRoguelike
? createInitialIwt2ArenaState( ? createInitialIwt2ArenaState(
@@ -191,6 +199,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
combinedBossHealthScale, combinedBossHealthScale,
combinedDamageScale, combinedDamageScale,
pressureState, pressureState,
arenaBounds,
) )
: null : null
recordedKillIdsRef.current = new Set() recordedKillIdsRef.current = new Set()
@@ -205,7 +214,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
setPetAwards([]) setPetAwards([])
setSelectedOverlayAction('primary') setSelectedOverlayAction('primary')
setStatus('playing') setStatus('playing')
}, [bossId, bossIds, combinedBossHealthScale, combinedDamageScale, pveGearActive, pvpRoguelike, roguelikeBuffs, roguelikeContentType, roguelikeStage, save.gearProgress]) }, [arenaBounds, bossId, bossIds, combinedBossHealthScale, combinedDamageScale, pveGearActive, pvpRoguelike, roguelikeBuffs, roguelikeContentType, roguelikeStage, save.gearProgress])
const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => { const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
setSelectedOverlayAction('primary') setSelectedOverlayAction('primary')
@@ -337,6 +346,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save,
return return
} }
if (action.startsWith('targetParty')) { if (action.startsWith('targetParty')) {
if (statusRef.current !== 'playing') return
const index = Number(action.replace('targetParty', '')) - 1 const index = Number(action.replace('targetParty', '')) - 1
const target = stateRef.current.party[index] const target = stateRef.current.party[index]
if (target) setSelectedPartyId(target.id) if (target) setSelectedPartyId(target.id)
@@ -701,6 +711,7 @@ function createArenaState(
buffs: Iwt2RoguelikeSelfBuffId[], buffs: Iwt2RoguelikeSelfBuffId[],
roguelikePressure: ReturnType<typeof createPressureState>, roguelikePressure: ReturnType<typeof createPressureState>,
gearProgress?: Iwt2Save['gearProgress'], gearProgress?: Iwt2Save['gearProgress'],
bounds?: Iwt2ArenaBounds,
): Iwt2ArenaState { ): Iwt2ArenaState {
const baseState = createInitialIwt2ArenaState( const baseState = createInitialIwt2ArenaState(
bossId, bossId,
@@ -708,6 +719,7 @@ function createArenaState(
bossHealthScale, bossHealthScale,
partyDamageTakenScale, partyDamageTakenScale,
roguelikePressure, roguelikePressure,
bounds,
) )
const state = gearProgress ? applyIwt2PveGearStats(baseState, gearProgress) : baseState const state = gearProgress ? applyIwt2PveGearStats(baseState, gearProgress) : baseState
const shieldedDamageTakenMultiplier = shieldedDamageTakenMultiplierForBuffs(buffs) const shieldedDamageTakenMultiplier = shieldedDamageTakenMultiplierForBuffs(buffs)
@@ -723,6 +735,17 @@ function createArenaState(
} }
} }
function createTopScreenArenaBounds(): Iwt2ArenaBounds {
if (typeof window === 'undefined') return { width: 960, height: 540 }
const thorTopLayout = window.innerWidth <= IWT2_THOR_TOP_BREAKPOINT_WIDTH
&& window.innerHeight <= IWT2_THOR_TOP_BREAKPOINT_HEIGHT
const railWidth = thorTopLayout ? IWT2_THOR_TOP_PARTY_RAIL_WIDTH : IWT2_TOP_PARTY_RAIL_WIDTH
return {
width: Math.max(320, Math.round(window.innerWidth - railWidth)),
height: Math.max(240, Math.round(window.innerHeight)),
}
}
function createPressureState( function createPressureState(
stage: number | undefined, stage: number | undefined,
contentType: Iwt2RoguelikeContentType | undefined, contentType: Iwt2RoguelikeContentType | undefined,
+85 -60
View File
@@ -1256,7 +1256,7 @@ export function Iwt2RoguelikeUpgradeScreen({
setSelectedIndex((current) => moveUpgradeSelection(entries, current, action)) setSelectedIndex((current) => moveUpgradeSelection(entries, current, action))
return return
} }
if (action === 'back' || action === 'pause') return if (action === 'back') onBack()
}) })
return ( return (
@@ -1547,33 +1547,10 @@ export function Iwt2HunterProfileScreen({
function moveSelection(action: InputAction) { function moveSelection(action: InputAction) {
if (!action.startsWith('navigate') || navEntries.length === 0) return if (!action.startsWith('navigate') || navEntries.length === 0) return
setSelectedIndex((current) => { const nextIndex = moveHunterProfileSelection(navEntries, selectedIndex, action)
const bounded = Math.min(current, navEntries.length - 1) const nextEntry = navEntries[nextIndex]
const active = navEntries[bounded] setSelectedIndex(nextIndex)
if (!active) return 0 if (nextEntry?.kind === 'item') setFocusedItemKey(nextEntry.itemKey)
const candidates = navEntries
.map((entry, index) => ({ entry, index }))
.filter(({ index }) => index !== bounded)
.filter(({ entry }) => {
if (action === 'navigateLeft') return entry.column < active.column
if (action === 'navigateRight') return entry.column > active.column
if (action === 'navigateUp') return entry.row < active.row
return entry.row > active.row
})
if (candidates.length === 0) return bounded
candidates.sort((a, b) => {
const aPrimary = Math.abs(a.entry.row - active.row) + Math.abs(a.entry.column - active.column)
const bPrimary = Math.abs(b.entry.row - active.row) + Math.abs(b.entry.column - active.column)
const aSecondary = action === 'navigateLeft' || action === 'navigateRight'
? Math.abs(a.entry.row - active.row)
: Math.abs(a.entry.column - active.column)
const bSecondary = action === 'navigateLeft' || action === 'navigateRight'
? Math.abs(b.entry.row - active.row)
: Math.abs(b.entry.column - active.column)
return aPrimary - bPrimary || aSecondary - bSecondary || a.index - b.index
})
return candidates[0]?.index ?? bounded
})
} }
function openEntry(entry: Iwt2HunterProfileNavEntry | undefined) { function openEntry(entry: Iwt2HunterProfileNavEntry | undefined) {
@@ -1863,6 +1840,7 @@ function Iwt2ProfileNameEditor({
<label> <label>
<span>Profile Name</span> <span>Profile Name</span>
<input <input
data-controller-nav="skip"
maxLength={IWT2_NAME_MAX_LENGTH} maxLength={IWT2_NAME_MAX_LENGTH}
onChange={(event) => setDraft(event.target.value.slice(0, IWT2_NAME_MAX_LENGTH))} onChange={(event) => setDraft(event.target.value.slice(0, IWT2_NAME_MAX_LENGTH))}
value={draft} value={draft}
@@ -1901,38 +1879,42 @@ export function Iwt2CloudSaveScreen({
const [onlineSave, setOnlineSave] = useState<Iwt2Save | null>(null) const [onlineSave, setOnlineSave] = useState<Iwt2Save | null>(null)
const [syncingOnlineSave, setSyncingOnlineSave] = useState(false) const [syncingOnlineSave, setSyncingOnlineSave] = useState(false)
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const statusMessage = message || (!onlineBackupsAvailable ? 'Online save unavailable in offline mode.' : '')
const checkOnlineSave = useCallback(async (isCancelled: () => boolean) => {
setSyncingOnlineSave(true)
setMessage('Checking online save...')
try {
const result = await loadIwt2OnlineSave()
if (isCancelled()) return
setOnlineSave(result.save)
setMessage(result.save ? 'Online save loaded.' : 'No online save yet.')
} catch (reason) {
if (isCancelled()) return
setOnlineSave(null)
setMessage(reason instanceof Error ? reason.message : 'Unable to check online save.')
} finally {
if (!isCancelled()) setSyncingOnlineSave(false)
}
}, [])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
if (!onlineBackupsAvailable) { if (!onlineBackupsAvailable) {
setOnlineSave(null)
setMessage('Online save unavailable in offline mode.')
return () => { return () => {
cancelled = true cancelled = true
} }
} }
setSyncingOnlineSave(true) const timer = window.setTimeout(() => {
setMessage('Checking online save...') void checkOnlineSave(() => cancelled)
loadIwt2OnlineSave() }, 0)
.then((result) => {
if (cancelled) return
setOnlineSave(result.save)
setMessage(result.save ? 'Online save loaded.' : 'No online save yet.')
})
.catch((reason) => {
if (cancelled) return
setOnlineSave(null)
setMessage(reason instanceof Error ? reason.message : 'Unable to check online save.')
})
.finally(() => {
if (!cancelled) setSyncingOnlineSave(false)
})
return () => { return () => {
cancelled = true cancelled = true
window.clearTimeout(timer)
} }
}, [onlineBackupsAvailable]) }, [checkOnlineSave, onlineBackupsAvailable])
const useLocalSave = useCallback(async () => { const handleUseLocalSave = useCallback(async () => {
if (!onlineBackupsAvailable) { if (!onlineBackupsAvailable) {
setMessage('Using local save. Sign in online to update the server copy.') setMessage('Using local save. Sign in online to update the server copy.')
return return
@@ -1950,13 +1932,13 @@ export function Iwt2CloudSaveScreen({
} }
}, [onlineBackupsAvailable, save]) }, [onlineBackupsAvailable, save])
const useOnlineSave = useCallback(() => { const handleUseOnlineSave = useCallback(() => {
if (!onlineSave) return if (!onlineBackupsAvailable || !onlineSave) return
onSaveUpdated(onlineSave) onSaveUpdated(onlineSave)
setMessage('Local save now uses online progress.') setMessage('Local save now uses online progress.')
}, [onlineSave, onSaveUpdated]) }, [onlineBackupsAvailable, onlineSave, onSaveUpdated])
const useNewSave = useCallback(() => { const handleUseNewSave = useCallback(() => {
onSaveUpdated(createDefaultIwt2Save()) onSaveUpdated(createDefaultIwt2Save())
setMessage('Started a new local IWT2 save.') setMessage('Started a new local IWT2 save.')
}, [onSaveUpdated]) }, [onSaveUpdated])
@@ -1969,7 +1951,7 @@ export function Iwt2CloudSaveScreen({
value: onlineBackupsAvailable ? 'Upload' : 'Current', value: onlineBackupsAvailable ? 'Upload' : 'Current',
disabled: syncingOnlineSave, disabled: syncingOnlineSave,
onConfirm: () => { onConfirm: () => {
void useLocalSave() void handleUseLocalSave()
}, },
}, },
{ {
@@ -1977,30 +1959,30 @@ export function Iwt2CloudSaveScreen({
label: 'Use Online Save', label: 'Use Online Save',
detail: syncingOnlineSave ? 'Checking online save...' : formatIwt2SaveSummary(onlineSave), detail: syncingOnlineSave ? 'Checking online save...' : formatIwt2SaveSummary(onlineSave),
value: onlineSave ? 'Download' : 'Empty', value: onlineSave ? 'Download' : 'Empty',
disabled: syncingOnlineSave || !onlineSave, disabled: syncingOnlineSave || !onlineBackupsAvailable || !onlineSave,
onConfirm: useOnlineSave, onConfirm: handleUseOnlineSave,
}, },
{ {
key: 'new-save', key: 'new-save',
label: 'Use New Save File', label: 'Use New Save File',
detail: 'Start over with a fresh IWT2 character. Online save is not overwritten until you choose local save.', detail: 'Start over with a fresh IWT2 character. Online save is not overwritten until you choose local save.',
value: 'New', value: 'New',
onConfirm: useNewSave, onConfirm: handleUseNewSave,
}, },
], onBack), [ ], onBack), [
handleUseLocalSave,
handleUseNewSave,
handleUseOnlineSave,
onlineBackupsAvailable, onlineBackupsAvailable,
onlineSave, onlineSave,
onBack, onBack,
save, save,
syncingOnlineSave, syncingOnlineSave,
useLocalSave,
useNewSave,
useOnlineSave,
]) ])
return ( return (
<Iwt2ScreenShell title="Backup Slot" onBack={onBack}> <Iwt2ScreenShell title="Backup Slot" onBack={onBack}>
{message && <p className="iwt2-screen-note">{message}</p>} {statusMessage && <p className="iwt2-screen-note">{statusMessage}</p>}
<Iwt2ActionList actions={actions} /> <Iwt2ActionList actions={actions} />
</Iwt2ScreenShell> </Iwt2ScreenShell>
) )
@@ -2114,6 +2096,16 @@ export function Iwt2GearUpgradeScreen({
<section className="content-screen iwt2-screen-shell iwt2-gear-screen" data-game-nav-active="true"> <section className="content-screen iwt2-screen-shell iwt2-gear-screen" data-game-nav-active="true">
<div className="iwt2-gear-layout"> <div className="iwt2-gear-layout">
<section className="iwt2-gear-column"> <section className="iwt2-gear-column">
<button
className={`back-button iwt2-gear-back ${activeEntry?.kind === 'back' ? 'game-selected' : ''}`}
data-controller-nav="skip"
data-game-selected={activeEntry?.kind === 'back' ? 'true' : undefined}
onClick={onBack}
onPointerDown={() => selectGearEntry(navEntries, setSelectedIndex, 'back')}
type="button"
>
Back
</button>
<div className="iwt2-gear-class-list"> <div className="iwt2-gear-class-list">
{IWT2_PARTY_ORDER.map((classId) => { {IWT2_PARTY_ORDER.map((classId) => {
const metadata = IWT2_CLASS_METADATA[classId] const metadata = IWT2_CLASS_METADATA[classId]
@@ -2372,6 +2364,39 @@ function activeUpgradeEntry(entries: Iwt2UpgradeNavEntry[], index: number) {
return entries[Math.min(index, entries.length - 1)] return entries[Math.min(index, entries.length - 1)]
} }
function moveHunterProfileSelection(
entries: Iwt2HunterProfileNavEntry[],
current: number,
action: InputAction,
) {
if (!action.startsWith('navigate') || entries.length === 0) return current
const bounded = Math.min(current, entries.length - 1)
const active = entries[bounded]
if (!active) return 0
const candidates = entries
.map((entry, index) => ({ entry, index }))
.filter(({ index }) => index !== bounded)
.filter(({ entry }) => {
if (action === 'navigateLeft') return entry.column < active.column
if (action === 'navigateRight') return entry.column > active.column
if (action === 'navigateUp') return entry.row < active.row
return entry.row > active.row
})
if (candidates.length === 0) return bounded
candidates.sort((a, b) => {
const aPrimary = Math.abs(a.entry.row - active.row) + Math.abs(a.entry.column - active.column)
const bPrimary = Math.abs(b.entry.row - active.row) + Math.abs(b.entry.column - active.column)
const aSecondary = action === 'navigateLeft' || action === 'navigateRight'
? Math.abs(a.entry.row - active.row)
: Math.abs(a.entry.column - active.column)
const bSecondary = action === 'navigateLeft' || action === 'navigateRight'
? Math.abs(b.entry.row - active.row)
: Math.abs(b.entry.column - active.column)
return aPrimary - bPrimary || aSecondary - bSecondary || a.index - b.index
})
return candidates[0]?.index ?? bounded
}
function upgradeEntryDisabled(entry: Iwt2UpgradeNavEntry) { function upgradeEntryDisabled(entry: Iwt2UpgradeNavEntry) {
return entry.kind === 'upgradeContinue' && Boolean(entry.disabled) return entry.kind === 'upgradeContinue' && Boolean(entry.disabled)
} }
+19 -10
View File
@@ -9,6 +9,7 @@ import type {
Iwt2EntityId, Iwt2EntityId,
Iwt2GroundHazardState, Iwt2GroundHazardState,
Iwt2HostileAddState, Iwt2HostileAddState,
Iwt2ArenaBounds,
Iwt2PartyAiRole, Iwt2PartyAiRole,
Iwt2PartyEntityId, Iwt2PartyEntityId,
Iwt2PartyEntityState, Iwt2PartyEntityState,
@@ -66,14 +67,15 @@ export function createInitialIwt2ArenaState(
bossHealthScale = 1, bossHealthScale = 1,
partyDamageTakenScale = 1, partyDamageTakenScale = 1,
roguelikePressure?: Iwt2RoguelikePressureState, roguelikePressure?: Iwt2RoguelikePressureState,
bounds: Iwt2ArenaBounds = { width: DEFAULT_ARENA_WIDTH, height: DEFAULT_ARENA_HEIGHT },
): Iwt2ArenaState { ): Iwt2ArenaState {
const initialBossIds = bossIds?.length ? bossIds.slice(0, 2) : chooseInitialBossIds(bossId) const initialBossIds = bossIds?.length ? bossIds.slice(0, 2) : chooseInitialBossIds(bossId)
const bosses = initialBossIds.map((id, index) => createBossEntity(id, index, bossHealthScale)) const bosses = initialBossIds.map((id, index) => createBossEntity(id, index, bossHealthScale, bounds))
return { return {
schemaVersion: 1, schemaVersion: 1,
time: 0, time: 0,
bounds: { width: DEFAULT_ARENA_WIDTH, height: DEFAULT_ARENA_HEIGHT }, bounds,
party: INITIAL_PARTY.map((member) => createPartyMember(member, partyDamageTakenScale)), party: INITIAL_PARTY.map((member) => createPartyMember(member, partyDamageTakenScale, bounds)),
projectiles: [], projectiles: [],
hostileAdds: [], hostileAdds: [],
hazards: [], hazards: [],
@@ -95,9 +97,9 @@ function chooseInitialBossIds(primaryBossId: Iwt2BossId): Iwt2BossId[] {
return [primaryBossId, random] return [primaryBossId, random]
} }
function createBossEntity(bossId: Iwt2BossId, index: number, healthScale: number): Iwt2BossEntityState { function createBossEntity(bossId: Iwt2BossId, index: number, healthScale: number, bounds: Iwt2ArenaBounds): Iwt2BossEntityState {
const bossMetadata = IWT2_BOSS_METADATA[bossId] const bossMetadata = IWT2_BOSS_METADATA[bossId]
const position = initialBossPosition(index) const position = scaleArenaPoint(initialBossPosition(index), bounds)
const maxHealth = Math.max(1, Math.round(bossMetadata.maxHealth * Math.max(0.01, healthScale))) const maxHealth = Math.max(1, Math.round(bossMetadata.maxHealth * Math.max(0.01, healthScale)))
return { return {
id: bossId, id: bossId,
@@ -123,9 +125,9 @@ function createBossEntity(bossId: Iwt2BossId, index: number, healthScale: number
chargeHitEntityIds: [], chargeHitEntityIds: [],
slamApplied: false, slamApplied: false,
wallContactSeconds: 0, wallContactSeconds: 0,
relocateTarget: { x: DEFAULT_ARENA_WIDTH * 0.58, y: DEFAULT_ARENA_HEIGHT * 0.5 + (index === 0 ? -64 : 64) }, relocateTarget: scaleArenaPoint({ x: DEFAULT_ARENA_WIDTH * 0.58, y: DEFAULT_ARENA_HEIGHT * 0.5 + (index === 0 ? -64 : 64) }, bounds),
fireballCooldownRemaining: initialBossSecondaryCooldown(bossId) + index * 0.7, fireballCooldownRemaining: initialBossSecondaryCooldown(bossId) + index * 0.7,
fireballTarget: { x: 320, y: 250 }, fireballTarget: scaleArenaPoint({ x: 320, y: 250 }, bounds),
birdWaveThresholdsTriggered: [], birdWaveThresholdsTriggered: [],
mechanicLanes: [], mechanicLanes: [],
mechanicCircles: [], mechanicCircles: [],
@@ -141,6 +143,13 @@ function initialBossPosition(index: number) {
} }
} }
function scaleArenaPoint(point: Iwt2Vec2, bounds: Iwt2ArenaBounds): Iwt2Vec2 {
return {
x: point.x * bounds.width / DEFAULT_ARENA_WIDTH,
y: point.y * bounds.height / DEFAULT_ARENA_HEIGHT,
}
}
function initialBossSpecialCooldown(bossId: Iwt2BossId): number { function initialBossSpecialCooldown(bossId: Iwt2BossId): number {
if (bossId === 'bulldrome') return 2 if (bossId === 'bulldrome') return 2
if (bossId === 'great-jaggi') return 2.4 if (bossId === 'great-jaggi') return 2.4
@@ -324,7 +333,7 @@ function regeneratePartyMana(party: Iwt2PartyEntityState[], dt: number): Iwt2Par
}) })
} }
function createPartyMember(initial: InitialPartyMember, damageTakenScale: number): Iwt2PartyEntityState { function createPartyMember(initial: InitialPartyMember, damageTakenScale: number, bounds: Iwt2ArenaBounds): Iwt2PartyEntityState {
const metadata = IWT2_CLASS_METADATA[initial.classId] const metadata = IWT2_CLASS_METADATA[initial.classId]
const safeDamageTakenScale = Number.isFinite(damageTakenScale) ? Math.max(0.01, damageTakenScale) : 1 const safeDamageTakenScale = Number.isFinite(damageTakenScale) ? Math.max(0.01, damageTakenScale) : 1
return { return {
@@ -332,7 +341,7 @@ function createPartyMember(initial: InitialPartyMember, damageTakenScale: number
kind: 'party', kind: 'party',
classId: initial.classId, classId: initial.classId,
aiRole: initial.aiRole, aiRole: initial.aiRole,
position: { x: initial.x, y: initial.y }, position: scaleArenaPoint({ x: initial.x, y: initial.y }, bounds),
velocity: { x: 0, y: 0 }, velocity: { x: 0, y: 0 },
facing: { x: 1, y: 0 }, facing: { x: 1, y: 0 },
radius: metadata.radius, radius: metadata.radius,
@@ -355,7 +364,7 @@ function createPartyMember(initial: InitialPartyMember, damageTakenScale: number
attackReady: false, attackReady: false,
status: createEmptyStatus(), status: createEmptyStatus(),
hotEffects: [], hotEffects: [],
preferredOffset: { ...initial.preferredOffset }, preferredOffset: scaleArenaPoint(initial.preferredOffset, bounds),
decisionSecondsRemaining: initial.decisionOffset, decisionSecondsRemaining: initial.decisionOffset,
} }
} }
+3 -1
View File
@@ -7,6 +7,7 @@ import {
import type { import type {
Iwt2ArenaIndicator, Iwt2ArenaIndicator,
Iwt2ArenaInput, Iwt2ArenaInput,
Iwt2ArenaBounds,
Iwt2ArenaState as Iwt2CoreArenaState, Iwt2ArenaState as Iwt2CoreArenaState,
Iwt2BossEntityState, Iwt2BossEntityState,
Iwt2HostileAddState, Iwt2HostileAddState,
@@ -61,8 +62,9 @@ export function createInitialIwt2ArenaState(
bossHealthScale?: number, bossHealthScale?: number,
partyDamageTakenScale?: number, partyDamageTakenScale?: number,
roguelikePressure?: Iwt2RoguelikePressureState, roguelikePressure?: Iwt2RoguelikePressureState,
bounds?: Iwt2ArenaBounds,
): Iwt2ArenaState { ): Iwt2ArenaState {
return decorateArenaState(createCoreIwt2ArenaState(bossId, bossIds, bossHealthScale, partyDamageTakenScale, roguelikePressure)) return decorateArenaState(createCoreIwt2ArenaState(bossId, bossIds, bossHealthScale, partyDamageTakenScale, roguelikePressure, bounds))
} }
export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt: number): Iwt2ArenaState { export function tickIwt2Arena(state: Iwt2ArenaState, input: Iwt2ArenaInput, dt: number): Iwt2ArenaState {
+4 -2
View File
@@ -12,9 +12,11 @@ const ROGUELIKE_PRESSURE_INITIAL_STAGE_DAMAGE = 2
const ROGUELIKE_PRESSURE_SOURCE_ID = 'roguelike-pressure' const ROGUELIKE_PRESSURE_SOURCE_ID = 'roguelike-pressure'
export function roguelikeIncomingDamageScale( export function roguelikeIncomingDamageScale(
_stage: number, stage: number,
_contentType: Iwt2RoguelikePressureContentType, contentType: Iwt2RoguelikePressureContentType,
): number { ): number {
void stage
void contentType
return 1 return 1
} }