diff --git a/IWantToHeal-Thor-v1.1.31.apk b/IWantToHeal-Thor-v1.1.31.apk new file mode 100644 index 0000000..ca6b18c Binary files /dev/null and b/IWantToHeal-Thor-v1.1.31.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index d6224a2..faf8b7a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.warren.iwanttoheal" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 111 - versionName "1.1.30" + versionCode 112 + versionName "1.1.31" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/screenshots/iwt2-party-frames-compact-hp-bars-thor-960x540.png b/screenshots/iwt2-party-frames-compact-hp-bars-thor-960x540.png new file mode 100644 index 0000000..73aa8d2 Binary files /dev/null and b/screenshots/iwt2-party-frames-compact-hp-bars-thor-960x540.png differ diff --git a/screenshots/iwt2-party-frames-left-expanded-thor-960x540.png b/screenshots/iwt2-party-frames-left-expanded-thor-960x540.png new file mode 100644 index 0000000..0ff3ae9 Binary files /dev/null and b/screenshots/iwt2-party-frames-left-expanded-thor-960x540.png differ diff --git a/screenshots/iwt2-party-frames-left-thor-960x540.png b/screenshots/iwt2-party-frames-left-thor-960x540.png new file mode 100644 index 0000000..60b0119 Binary files /dev/null and b/screenshots/iwt2-party-frames-left-thor-960x540.png differ diff --git a/src/App.css b/src/App.css index 6858253..4234b8b 100644 --- a/src/App.css +++ b/src/App.css @@ -212,6 +212,8 @@ } .iwt2-arena-layout { + display: grid; + grid-template-columns: 172px minmax(0, 1fr); height: 100vh; overflow: hidden; padding: 0; @@ -221,6 +223,7 @@ .iwt2-arena-stage { background: #090c10; border: 0; + grid-column: 2; height: 100%; min-height: 0; overflow: hidden; @@ -317,6 +320,22 @@ 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 { background: #dc5162; } @@ -337,17 +356,18 @@ .iwt2-party-list { background: rgba(15, 19, 26, 0.88); border: 0; - left: 28px; - max-height: calc(100% - 112px); + bottom: 0; + left: 0; + max-height: none; outline: 0; padding: 6px; - position: absolute; - top: 28px; + position: fixed; + top: 0; width: 172px; z-index: 4; display: grid; gap: 8px; - align-content: start; + align-content: center; overflow: auto; } @@ -1099,7 +1119,8 @@ .iwt2-gear-class, .iwt2-gear-slot, .iwt2-infusion-row, -.iwt2-gear-upgrade-button { +.iwt2-gear-upgrade-button, +.iwt2-gear-back { background: #151922; border: 2px solid #08090d; color: var(--ink); @@ -1177,7 +1198,8 @@ .iwt2-gear-class.game-selected, .iwt2-gear-slot.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; } @@ -1187,6 +1209,12 @@ text-align: center; } +.iwt2-gear-back { + min-height: 36px; + padding: 8px 10px; + text-align: center; +} + .iwt2-gear-upgrade-button:disabled, .iwt2-infusion-row:disabled { color: var(--muted); @@ -1401,6 +1429,10 @@ } @media (max-width: 1000px) and (max-height: 620px) { + .iwt2-arena-layout { + grid-template-columns: 154px minmax(0, 1fr); + } + .game-shell.iwt2-shell { padding: 6px 0; width: min(100%, calc(100% - 20px)); @@ -1640,6 +1672,11 @@ padding: 7px 9px; } + .iwt2-gear-back { + min-height: 30px; + padding: 5px 8px; + } + .iwt2-gear-detail { gap: 6px; padding: 7px; @@ -1735,20 +1772,22 @@ } .iwt2-party-list { - gap: 5px; - left: 10px; - max-height: calc(100% - 84px); + align-content: center; + gap: 7px; + bottom: 0; + left: 0; + max-height: none; overflow: hidden; padding: 4px; - top: 10px; + top: 0; width: 154px; } .iwt2-party-row { gap: 5px; grid-template-columns: 22px minmax(0, 1fr); - height: 62px; - min-height: 62px; + height: 70px; + min-height: 70px; padding: 4px; } @@ -1782,8 +1821,12 @@ } .iwt2-party-row .iwt2-bar { - height: 7px; - margin-top: 2px; + height: 12px; + margin-top: 3px; + } + + .iwt2-party-row .iwt2-bar.mana { + height: 8px; } .iwt2-party-effects { @@ -1803,6 +1846,7 @@ .iwt2-ability-bar { bottom: 8px; + display: none; gap: 8px; grid-template-columns: repeat(6, 36px); max-width: 256px; diff --git a/src/input.tsx b/src/input.tsx index 5d3b7c7..f605ba3 100644 --- a/src/input.tsx +++ b/src/input.tsx @@ -435,6 +435,10 @@ function hasDedicatedGameNavigation() { ).some(isVisible) } +function isDedicatedNavigationAction(action: InputAction) { + return action.startsWith('navigate') || action === 'confirm' || action === 'back' +} + function dispatchGameAction(action: InputAction, device: InputDevice) { window.dispatchEvent(new CustomEvent(GAME_ACTION_EVENT, { detail: { action, device }, @@ -621,7 +625,12 @@ export function InputProvider({ children }: { children: ReactNode }) { setLastDevice(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() dispatchGameAction(action, device) return @@ -709,6 +718,7 @@ export function InputProvider({ children }: { children: ReactNode }) { 'navigateRight', 'confirm', 'back', + 'pause', ] satisfies InputAction[] const directTargetActions = [ 'targetParty1', @@ -738,7 +748,13 @@ export function InputProvider({ children }: { children: ReactNode }) { 'navigateLeft', 'navigateRight', ] 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] : uiOverlay ? uiPriority.find((candidate) => bindingsRef.current.controller[candidate] === token) diff --git a/src/modes/iwt2/components/ArenaBars.tsx b/src/modes/iwt2/components/ArenaBars.tsx index cff62bc..bcfe5c2 100644 --- a/src/modes/iwt2/components/ArenaBars.tsx +++ b/src/modes/iwt2/components/ArenaBars.tsx @@ -1,11 +1,13 @@ export function ArenaBar({ className = '', current, + label, max, shield = 0, }: { className?: string current: number + label?: string max: number shield?: number }) { @@ -15,6 +17,7 @@ export function ArenaBar({
{shieldPercent > 0 && } + {label && {label}}
) } diff --git a/src/modes/iwt2/components/PartyFrames.tsx b/src/modes/iwt2/components/PartyFrames.tsx index c5018ac..ac02245 100644 --- a/src/modes/iwt2/components/PartyFrames.tsx +++ b/src/modes/iwt2/components/PartyFrames.tsx @@ -46,9 +46,8 @@ export function PartyFrames({
{meta.name} - {Math.ceil(member.health)}
- + {member.maxMana > 0 && ( )} diff --git a/src/modes/iwt2/screens/BossArenaScreen.tsx b/src/modes/iwt2/screens/BossArenaScreen.tsx index 807aea3..b6929ff 100644 --- a/src/modes/iwt2/screens/BossArenaScreen.tsx +++ b/src/modes/iwt2/screens/BossArenaScreen.tsx @@ -12,7 +12,7 @@ import { tickIwt2Arena, type Iwt2ArenaState, } from '../sim/arenaState' -import type { Iwt2EntityId } from '../sim' +import type { Iwt2ArenaBounds, Iwt2EntityId } from '../sim' import { castIwt2HealerAbility } from '../sim' import { PhaserArena } from '../render/PhaserArena' import { @@ -64,6 +64,10 @@ const PVP_RESULT_OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [ ] const EMPTY_ROGUELIKE_BUFFS: Iwt2RoguelikeSelfBuffId[] = [] 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 = { bossId: Iwt2BossId @@ -103,6 +107,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, const pvpBossHealthScale = pvpRoguelike ? IWT2_PVP_BOSS_HEALTH_MULTIPLIER : 1 const combinedBossHealthScale = bossHealthScale * difficultyHealthScale * pvpBossHealthScale const combinedDamageScale = difficultyDamageScale * roguelikeDamageScale + const arenaBounds = useMemo(() => createTopScreenArenaBounds(), []) const [arenaState, setArenaState] = useState(() => createArenaState( bossId, bossIds, @@ -111,6 +116,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, roguelikeBuffs, createPressureState(roguelikeStage, roguelikeContentType), pveGearActive ? save.gearProgress : undefined, + arenaBounds, )) const [opponentArenaState, setOpponentArenaState] = useState(() => ( pvpRoguelike @@ -120,6 +126,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, combinedBossHealthScale, combinedDamageScale, createPressureState(roguelikeStage, roguelikeContentType), + arenaBounds, ) : null )) @@ -183,6 +190,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, roguelikeBuffs, pressureState, pveGearActive ? save.gearProgress : undefined, + arenaBounds, ) const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState( @@ -191,6 +199,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, combinedBossHealthScale, combinedDamageScale, pressureState, + arenaBounds, ) : null recordedKillIdsRef.current = new Set() @@ -205,7 +214,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, setPetAwards([]) setSelectedOverlayAction('primary') 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) => { setSelectedOverlayAction('primary') @@ -337,6 +346,7 @@ export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, return } if (action.startsWith('targetParty')) { + if (statusRef.current !== 'playing') return const index = Number(action.replace('targetParty', '')) - 1 const target = stateRef.current.party[index] if (target) setSelectedPartyId(target.id) @@ -701,6 +711,7 @@ function createArenaState( buffs: Iwt2RoguelikeSelfBuffId[], roguelikePressure: ReturnType, gearProgress?: Iwt2Save['gearProgress'], + bounds?: Iwt2ArenaBounds, ): Iwt2ArenaState { const baseState = createInitialIwt2ArenaState( bossId, @@ -708,6 +719,7 @@ function createArenaState( bossHealthScale, partyDamageTakenScale, roguelikePressure, + bounds, ) const state = gearProgress ? applyIwt2PveGearStats(baseState, gearProgress) : baseState 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( stage: number | undefined, contentType: Iwt2RoguelikeContentType | undefined, diff --git a/src/modes/iwt2/screens/Iwt2ShellScreens.tsx b/src/modes/iwt2/screens/Iwt2ShellScreens.tsx index d25f155..db261dd 100644 --- a/src/modes/iwt2/screens/Iwt2ShellScreens.tsx +++ b/src/modes/iwt2/screens/Iwt2ShellScreens.tsx @@ -1256,7 +1256,7 @@ export function Iwt2RoguelikeUpgradeScreen({ setSelectedIndex((current) => moveUpgradeSelection(entries, current, action)) return } - if (action === 'back' || action === 'pause') return + if (action === 'back') onBack() }) return ( @@ -1547,33 +1547,10 @@ export function Iwt2HunterProfileScreen({ function moveSelection(action: InputAction) { if (!action.startsWith('navigate') || navEntries.length === 0) return - setSelectedIndex((current) => { - const bounded = Math.min(current, navEntries.length - 1) - const active = navEntries[bounded] - if (!active) return 0 - 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 - }) + const nextIndex = moveHunterProfileSelection(navEntries, selectedIndex, action) + const nextEntry = navEntries[nextIndex] + setSelectedIndex(nextIndex) + if (nextEntry?.kind === 'item') setFocusedItemKey(nextEntry.itemKey) } function openEntry(entry: Iwt2HunterProfileNavEntry | undefined) { @@ -1863,6 +1840,7 @@ function Iwt2ProfileNameEditor({