diff --git a/IWantToHeal-Thor-v1.0.47.apk b/IWantToHeal-Thor-v1.0.47.apk deleted file mode 100644 index 009dd3b..0000000 Binary files a/IWantToHeal-Thor-v1.0.47.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.49.apk b/IWantToHeal-Thor-v1.0.49.apk deleted file mode 100644 index e778e09..0000000 Binary files a/IWantToHeal-Thor-v1.0.49.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.50.apk b/IWantToHeal-Thor-v1.0.50.apk deleted file mode 100644 index 3fbdcda..0000000 Binary files a/IWantToHeal-Thor-v1.0.50.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.51.apk b/IWantToHeal-Thor-v1.0.51.apk deleted file mode 100644 index 236df01..0000000 Binary files a/IWantToHeal-Thor-v1.0.51.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.52.apk b/IWantToHeal-Thor-v1.0.52.apk deleted file mode 100644 index 20c7859..0000000 Binary files a/IWantToHeal-Thor-v1.0.52.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.53.apk b/IWantToHeal-Thor-v1.0.53.apk deleted file mode 100644 index 0cee2f4..0000000 Binary files a/IWantToHeal-Thor-v1.0.53.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.54.apk b/IWantToHeal-Thor-v1.0.54.apk deleted file mode 100644 index b29985c..0000000 Binary files a/IWantToHeal-Thor-v1.0.54.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.55.apk b/IWantToHeal-Thor-v1.0.55.apk deleted file mode 100644 index c44ecfb..0000000 Binary files a/IWantToHeal-Thor-v1.0.55.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.56.apk b/IWantToHeal-Thor-v1.0.56.apk deleted file mode 100644 index 4e939e1..0000000 Binary files a/IWantToHeal-Thor-v1.0.56.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.57.apk b/IWantToHeal-Thor-v1.0.57.apk deleted file mode 100644 index e39d571..0000000 Binary files a/IWantToHeal-Thor-v1.0.57.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.58.apk b/IWantToHeal-Thor-v1.0.58.apk deleted file mode 100644 index 1d3bb6f..0000000 Binary files a/IWantToHeal-Thor-v1.0.58.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.59.apk b/IWantToHeal-Thor-v1.0.59.apk deleted file mode 100644 index 7da1228..0000000 Binary files a/IWantToHeal-Thor-v1.0.59.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.0.60.apk b/IWantToHeal-Thor-v1.0.60.apk deleted file mode 100644 index 6023356..0000000 Binary files a/IWantToHeal-Thor-v1.0.60.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.0.apk b/IWantToHeal-Thor-v1.1.0.apk deleted file mode 100644 index 6d18657..0000000 Binary files a/IWantToHeal-Thor-v1.1.0.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.10.apk b/IWantToHeal-Thor-v1.1.10.apk deleted file mode 100644 index 160e59d..0000000 Binary files a/IWantToHeal-Thor-v1.1.10.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.11.apk b/IWantToHeal-Thor-v1.1.11.apk deleted file mode 100644 index 491af07..0000000 Binary files a/IWantToHeal-Thor-v1.1.11.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.12.apk b/IWantToHeal-Thor-v1.1.12.apk deleted file mode 100644 index 1d375ec..0000000 Binary files a/IWantToHeal-Thor-v1.1.12.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.14.apk b/IWantToHeal-Thor-v1.1.14.apk deleted file mode 100644 index f242016..0000000 Binary files a/IWantToHeal-Thor-v1.1.14.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.13.apk b/IWantToHeal-Thor-v1.1.17.apk similarity index 86% rename from IWantToHeal-Thor-v1.1.13.apk rename to IWantToHeal-Thor-v1.1.17.apk index 5c5b5a1..62b6111 100644 Binary files a/IWantToHeal-Thor-v1.1.13.apk and b/IWantToHeal-Thor-v1.1.17.apk differ diff --git a/IWantToHeal-Thor-v1.1.2.apk b/IWantToHeal-Thor-v1.1.2.apk deleted file mode 100644 index 7f10384..0000000 Binary files a/IWantToHeal-Thor-v1.1.2.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.3.apk b/IWantToHeal-Thor-v1.1.3.apk deleted file mode 100644 index aec5b2e..0000000 Binary files a/IWantToHeal-Thor-v1.1.3.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.4.apk b/IWantToHeal-Thor-v1.1.4.apk deleted file mode 100644 index 4131da6..0000000 Binary files a/IWantToHeal-Thor-v1.1.4.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.5.apk b/IWantToHeal-Thor-v1.1.5.apk deleted file mode 100644 index 730766c..0000000 Binary files a/IWantToHeal-Thor-v1.1.5.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.6.apk b/IWantToHeal-Thor-v1.1.6.apk deleted file mode 100644 index f97238b..0000000 Binary files a/IWantToHeal-Thor-v1.1.6.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.7.apk b/IWantToHeal-Thor-v1.1.7.apk deleted file mode 100644 index b9d046d..0000000 Binary files a/IWantToHeal-Thor-v1.1.7.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.8.apk b/IWantToHeal-Thor-v1.1.8.apk deleted file mode 100644 index 1e8677d..0000000 Binary files a/IWantToHeal-Thor-v1.1.8.apk and /dev/null differ diff --git a/IWantToHeal-Thor-v1.1.9.apk b/IWantToHeal-Thor-v1.1.9.apk deleted file mode 100644 index 669eea0..0000000 Binary files a/IWantToHeal-Thor-v1.1.9.apk and /dev/null differ diff --git a/android/app/build.gradle b/android/app/build.gradle index 4ecf5cb..e03ecee 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 95 - versionName "1.1.16" + versionCode 96 + versionName "1.1.17" 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/src/App.css b/src/App.css index 4589efe..f606e26 100644 --- a/src/App.css +++ b/src/App.css @@ -837,6 +837,285 @@ } } +@media (max-width: 1000px) and (max-height: 620px) { + .game-shell.iwt2-shell { + padding: 6px 0; + width: min(100%, calc(100% - 20px)); + } + + .iwt2-shell .app-header { + min-height: 44px; + padding: 6px 10px; + } + + .iwt2-shell .brand-button strong { + font-size: 10px; + } + + .iwt2-shell .character-summary { + gap: 7px; + } + + .iwt2-shell .character-summary strong { + font-size: 14px; + } + + .iwt2-shell .character-summary small { + font-size: 6px; + } + + .iwt2-menu-screen { + flex: 1; + gap: 10px; + min-height: 0; + overflow: hidden; + padding: 10px 12px; + } + + .iwt2-menu-heading { + padding-bottom: 8px; + } + + .iwt2-menu-heading .eyebrow { + font-size: 6px; + margin-bottom: 4px; + } + + .iwt2-menu-heading h1 { + font-size: 1.55rem; + } + + .iwt2-menu-grid { + gap: 10px; + } + + .iwt2-menu-card { + gap: 8px; + grid-template-columns: 42px minmax(0, 1fr); + min-height: 92px; + padding: 8px; + } + + .iwt2-menu-card > span { + font-size: 0.95rem; + height: 42px; + width: 42px; + } + + .iwt2-menu-card strong { + font-size: 0.8rem; + margin-bottom: 3px; + } + + .iwt2-menu-card small { + font-size: 0.64rem; + line-height: 1.25; + } + + .iwt2-screen-shell { + flex: 1; + gap: 10px; + margin-top: 8px; + max-width: none; + min-height: 0; + overflow: hidden; + padding: 10px 12px; + } + + .iwt2-screen-shell .screen-heading { + padding-bottom: 8px; + } + + .iwt2-screen-shell .screen-heading .eyebrow { + font-size: 6px; + margin-bottom: 4px; + } + + .iwt2-screen-shell .screen-heading h1 { + font-size: 17px; + line-height: 1.1; + } + + .iwt2-screen-shell .back-button { + min-height: 30px; + padding: 5px 9px; + } + + .iwt2-action-list, + .iwt2-info-list { + gap: 7px; + min-height: 0; + overflow-y: auto; + padding: 3px; + } + + .iwt2-action-row, + .iwt2-info-row { + gap: 8px; + min-height: 54px; + padding: 8px 10px; + } + + .iwt2-action-row strong, + .iwt2-info-row strong { + font-size: 0.78rem; + } + + .iwt2-action-row small, + .iwt2-info-row small { + font-size: 0.62rem; + line-height: 1.2; + margin-top: 2px; + } + + .iwt2-profile-grid, + .iwt2-settings-panel, + .iwt2-customize-layout { + min-height: 0; + overflow: hidden; + } + + .iwt2-settings-panel, + .iwt2-customize-layout { + gap: 10px; + } + + .iwt2-settings-panel > *, + .iwt2-customize-layout > *, + .iwt2-loadout-preview { + min-height: 0; + overflow: hidden; + } + + .iwt2-loadout-preview { + gap: 8px; + } + + .iwt2-loadout-preview h2 { + font-size: 12px; + line-height: 1.1; + } + + .iwt2-loadout-preview span { + font-size: 0.62rem; + } + + .iwt2-controller-preview { + min-height: 112px; + padding: 10px; + } + + .iwt2-boss-hud { + min-width: 280px; + padding: 6px 10px 8px; + top: 6px; + } + + .iwt2-boss-hud strong { + font-size: 0.82rem; + } + + .iwt2-boss-phase { + font-size: 0.5rem; + height: 10px; + line-height: 10px; + } + + .iwt2-party-list { + gap: 5px; + left: 10px; + max-height: calc(100% - 84px); + overflow: hidden; + padding: 4px; + top: 10px; + width: 154px; + } + + .iwt2-party-row { + gap: 5px; + grid-template-columns: 20px minmax(0, 1fr); + height: 62px; + min-height: 62px; + padding: 4px; + } + + .iwt2-party-row.has-target-binding { + grid-template-columns: 24px 20px minmax(0, 1fr); + } + + .iwt2-party-row > span, + .iwt2-party-row .iwt2-party-target-key { + height: 20px; + min-width: 20px; + width: 20px; + } + + .iwt2-party-row .iwt2-party-target-key .controller-face-icon { + height: 16px; + min-width: 16px; + } + + .iwt2-party-row-title { + gap: 4px; + } + + .iwt2-party-row strong { + font-size: 0.62rem; + } + + .iwt2-party-row small { + font-size: 0.52rem; + line-height: 1.15; + } + + .iwt2-party-row .iwt2-bar { + height: 7px; + margin-top: 2px; + } + + .iwt2-party-effects { + height: 10px; + margin-top: 2px; + } + + .iwt2-effect-badge { + font-size: 0.44rem; + line-height: 9px; + padding: 0 2px; + } + + .iwt2-party-damage { + display: none; + } + + .iwt2-ability-bar { + bottom: 8px; + gap: 8px; + grid-template-columns: repeat(6, 36px); + max-width: 256px; + width: 256px; + } + + .iwt2-ability-button { + min-height: 44px; + padding: 3px; + } + + .iwt2-ability-button > span { + font-size: 0.9rem; + height: 24px; + width: 24px; + } + + .iwt2-ability-button strong { + font-size: 0.5rem; + } + + .iwt2-ability-button i { + font-size: 0.46rem; + } +} + .combat-touch-lock-status { background: #101216; border: 2px solid #d9b55a; @@ -11576,3 +11855,57 @@ h2 { height: 20px; } } + +@media (max-width: 1000px) and (max-height: 620px) { + .iwt2-screen-shell.content-screen { + gap: 10px; + margin-top: 8px; + max-width: none; + min-height: 0; + overflow: hidden; + padding: 10px 12px; + } + + .iwt2-screen-shell.content-screen > .screen-heading { + padding-bottom: 8px; + } + + .iwt2-screen-shell.content-screen > .screen-heading .eyebrow { + font-size: 6px; + margin-bottom: 4px; + } + + .iwt2-screen-shell.content-screen > .screen-heading h1 { + font-size: 17px; + line-height: 1.1; + } + + .iwt2-screen-shell .iwt2-action-list, + .iwt2-screen-shell .iwt2-info-list, + .iwt2-settings-panel .iwt2-action-list, + .iwt2-customize-layout .iwt2-action-list, + .iwt2-loadout-preview .iwt2-info-list { + gap: 4px; + min-height: 0; + overflow-y: auto; + } + + .iwt2-screen-shell.content-screen > .iwt2-action-list, + .iwt2-screen-shell.content-screen > .iwt2-info-list { + flex: 1 1 auto; + } + + .iwt2-action-row:not(:has(small)), + .iwt2-info-row:not(:has(small)) { + min-height: 32px; + padding: 5px 10px; + } + + .iwt2-settings-panel, + .iwt2-customize-layout, + .iwt2-settings-panel > *, + .iwt2-customize-layout > *, + .iwt2-loadout-preview { + min-height: 0; + } +} diff --git a/src/modes/iwt2/IWantToHeal2App.tsx b/src/modes/iwt2/IWantToHeal2App.tsx index 5753d6a..73c7601 100644 --- a/src/modes/iwt2/IWantToHeal2App.tsx +++ b/src/modes/iwt2/IWantToHeal2App.tsx @@ -1,6 +1,13 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useGameAction } from '../../input' import { summarizeChoiceStacks } from '../../combat/roguelikeUpgrades' +import { + useDualScreen, + useDualScreenSetupPublisher, + useDualScreenWorkshopPublisher, + type DualScreenSetupState, + type DualScreenWorkshopState, +} from '../../dualScreen' import { BossArenaScreen } from './screens/BossArenaScreen' import { Iwt2CloudSaveScreen, @@ -17,8 +24,8 @@ import { writeIwt2Save, type Iwt2Save, } from './save/iwt2Repository' -import type { Iwt2BossId } from './content/bosses' -import { abilitiesForHealer } from './content/healerAbilities' +import { IWT2_BOSS_METADATA, type Iwt2BossId } from './content/bosses' +import { abilitiesForHealer, IWT2_HEALER_METADATA } from './content/healerAbilities' import { buildIwt2OpponentDebuffChoices, buildIwt2SelfBuffChoices, @@ -113,6 +120,7 @@ const MENU_ITEMS: Array<{ ] export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: () => void }) { + const { enabled: dualScreenEnabled } = useDualScreen() const [screen, setScreen] = useState('menu') const [save, setSave] = useState(loadIwt2Save) const [selectedIndex, setSelectedIndex] = useState(0) @@ -126,6 +134,18 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: () writeIwt2Save(save) }, [save]) + const setupDualScreenState = useMemo( + () => buildIwt2SetupDualScreenState(screen, selectedBossId, save), + [save, screen, selectedBossId], + ) + const workshopDualScreenState = useMemo( + () => buildIwt2WorkshopDualScreenState(screen, save), + [save, screen], + ) + + useDualScreenSetupPublisher(setupDualScreenState, dualScreenEnabled) + useDualScreenWorkshopPublisher(workshopDualScreenState, dualScreenEnabled) + useGameAction((action, device) => { if (screen !== 'menu' || device !== 'controller') return if (action === 'back') { @@ -478,3 +498,90 @@ function Iwt2Header({ ) } + +function buildIwt2SetupDualScreenState( + screen: Iwt2Screen, + selectedBossId: Iwt2BossId, + save: Iwt2Save, +): DualScreenSetupState | null { + if (screen !== 'dungeons' && screen !== 'raids') return null + const boss = IWT2_BOSS_METADATA[selectedBossId] + const raid = screen === 'raids' + return { + contentType: raid ? 'raid' : 'dungeon', + description: raid + ? `${boss.name} raid assignment. Tank holds aggro while party moves around modular boss mechanics.` + : `${boss.name} arena. Heal the party through melee pressure, telegraphs, hazards, and stun recovery.`, + difficultyName: `IWT2 Level ${save.character.level}`, + experience: 125, + initials: boss.icon, + itemLevel: save.character.level, + stats: { + damage: `${boss.meleeDamage}`, + health: `${boss.maxHealth}`, + loot: 'IWT2', + xp: '125', + }, + subtitle: `${raid ? 'Raid' : 'Dungeon'} | 6 Players | ${IWT2_HEALER_METADATA[save.character.healerStyle].name}`, + title: raid ? `${boss.name} Raid` : `${boss.name} Arena`, + } +} + +function buildIwt2WorkshopDualScreenState( + screen: Iwt2Screen, + save: Iwt2Save, +): DualScreenWorkshopState | null { + if (screen === 'hunter-profile') { + return { + items: [ + { + glyph: 'H', + meta: `${save.character.experience} XP`, + status: `Level ${save.character.level}`, + title: save.character.name, + }, + { + glyph: IWT2_HEALER_METADATA[save.character.healerStyle].icon, + meta: IWT2_HEALER_METADATA[save.character.healerStyle].description, + status: 'Class', + title: IWT2_HEALER_METADATA[save.character.healerStyle].name, + }, + ...(Object.keys(IWT2_BOSS_METADATA) as Iwt2BossId[]).map((bossId) => ({ + glyph: IWT2_BOSS_METADATA[bossId].icon, + meta: `${save.collectionLog.bossKills[bossId] ?? 0} kills`, + status: `${save.collectionLog.dropsFound[bossDropIdForAppSummary(bossId)] ?? 0} drops`, + title: IWT2_BOSS_METADATA[bossId].name, + })), + ], + mode: 'collection', + subtitle: 'IWT2 inventory and collection log', + summary: `${save.inventory.length} inventory slots`, + title: 'Hunter Profile', + } + } + + if (screen === 'customize-character') { + const healer = IWT2_HEALER_METADATA[save.character.healerStyle] + return { + items: abilitiesForHealer(save.character.healerStyle).map((ability) => ({ + glyph: ability.icon, + meta: `${ability.manaCost} Mana | ${ability.cooldownSeconds}s cooldown`, + status: `Slot ${ability.slot}`, + title: ability.name, + })), + mode: 'class', + subtitle: healer.description, + summary: healer.name, + title: 'Customize Character', + } + } + + return null +} + +function bossDropIdForAppSummary(bossId: Iwt2BossId): string { + if (bossId === 'yian-kut-ku') return 'yian-kut-ku-scale' + if (bossId === 'great-jaggi') return 'great-jaggi-hide' + if (bossId === 'khezu') return 'khezu-pearl' + return 'raw-bulldrome-coin' +} diff --git a/src/modes/iwt2/screens/BossArenaScreen.tsx b/src/modes/iwt2/screens/BossArenaScreen.tsx index 0a4738f..0279c97 100644 --- a/src/modes/iwt2/screens/BossArenaScreen.tsx +++ b/src/modes/iwt2/screens/BossArenaScreen.tsx @@ -1,6 +1,12 @@ import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react' import type { MovementVector } from '../../../input' import { useGameAction, useInput, useMovementVectorRef } from '../../../input' +import { + useDualScreen, + useDualScreenPublisher, + type DualScreenCombatState, +} from '../../../dualScreen' +import type { PartyMember, Role, Spell } from '../../../game' import { createInitialIwt2ArenaState, tickIwt2Arena, @@ -16,6 +22,7 @@ import { import { AbilityBar } from '../components/AbilityBar' import { BossHud } from '../components/BossHud' import { PartyFrames } from '../components/PartyFrames' +import { IWT2_CLASS_METADATA } from '../content/classes' import { IWT2_ABILITY_ACTIONS, IWT2_TARGET_ACTIONS } from '../content/controls' import { abilitiesForHealer, type Iwt2HealerAbility } from '../content/healerAbilities' import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses' @@ -77,6 +84,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated directPartyTargeting, lastDevice, } = useInput() + const { enabled: dualScreenEnabled } = useDualScreen() const activeBindings = bindings[lastDevice] useEffect(() => { @@ -292,6 +300,36 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated : roguelikeRun ? 'Leave Roguelike' : `Leave ${modeLabel ?? 'Arena'}` + const dualScreenState = useMemo( + () => buildIwt2DualScreenCombatState({ + abilities, + arenaState, + bindings: activeBindings, + bossMetadata, + cooldowns: abilityCooldowns, + controllerIconStyle, + directPartyTargeting, + modeLabel: modeLabel ?? 'Arena', + playerMana, + selectedPartyId, + status, + }), + [ + abilities, + activeBindings, + arenaState, + abilityCooldowns, + bossMetadata, + controllerIconStyle, + directPartyTargeting, + modeLabel, + playerMana, + selectedPartyId, + status, + ], + ) + + useDualScreenPublisher(dualScreenState, dualScreenEnabled) return (
+ controllerIconStyle: DualScreenCombatState['controllerIconStyle'] + directPartyTargeting: boolean + modeLabel: string + playerMana: number + selectedPartyId: Iwt2EntityId + status: ArenaStatus +}): DualScreenCombatState { + return { + bindings, + contentName: modeLabel, + controllerIconStyle, + difficultyName: 'IWT2', + directPartyTargeting, + dungeonName: `${bossMetadata.name} Arena`, + encounterCount: 1, + encounterDescription: arenaState.boss.attackPhase, + encounterHealth: arenaState.boss.health, + encounterIndex: 0, + encounterIsBoss: true, + encounterMaxHealth: arenaState.boss.maxHealth, + encounterName: bossMetadata.name, + floatingTexts: [], + maxResource: 100, + party: arenaState.party.map(toDualScreenPartyMember), + partySize: arenaState.party.length, + paused: status === 'paused', + playerIsAlive: (arenaState.party.find((member) => member.id === 'player-healer')?.health ?? 0) > 0, + resource: playerMana, + resourceName: 'Mana', + selectedId: selectedPartyId, + speedMultiplier: 1, + spells: abilities.map((ability, slotIndex) => toDualScreenSpell(ability, slotIndex, cooldowns[ability.id] ?? 0)), + status: status === 'victory' ? 'won' : status === 'defeat' ? 'lost' : 'playing', + targetGroup: 0, + } +} + +function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number]): PartyMember { + const metadata = IWT2_CLASS_METADATA[member.classId] + return { + bounceHeals: [], + health: member.health, + hotEffects: member.hotEffects.map((effect) => ({ + id: effect.id, + label: effect.label, + power: effect.power, + spellId: effect.id, + ticks: Math.ceil(effect.remainingSeconds), + })), + hotTicks: member.hotEffects.length, + id: member.id, + maxHealth: member.maxHealth, + name: metadata.name, + role: toDualScreenRole(metadata.role), + shield: member.shield, + } +} + +function toDualScreenRole(role: (typeof IWT2_CLASS_METADATA)[keyof typeof IWT2_CLASS_METADATA]['role']): Role { + if (role === 'tank') return 'Tank' + if (role === 'healer') return 'Healer' + return 'Damage' +} + +function toDualScreenSpell( + ability: Iwt2HealerAbility, + slotIndex: number, + remaining: number, +): Spell & { slotIndex: number; remaining: number } { + return { + cooldown: ability.cooldownSeconds, + cost: ability.manaCost, + description: 'IWT2 arena ability', + effectType: ability.effectType, + glyph: ability.icon, + id: ability.id, + key: String(ability.slot), + kind: ability.kind, + name: ability.name, + power: ability.power, + remaining, + slotIndex, + } +} diff --git a/src/modes/iwt2/screens/Iwt2ShellScreens.tsx b/src/modes/iwt2/screens/Iwt2ShellScreens.tsx index 52245ab..0a81328 100644 --- a/src/modes/iwt2/screens/Iwt2ShellScreens.tsx +++ b/src/modes/iwt2/screens/Iwt2ShellScreens.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, type ReactNode } from 'react' +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { ControllerStylePreview } from '../../../components/ControllerIcons' import { useGameAction, useInput, type ControllerIconStyle } from '../../../input' import { getGameMode } from '../../../gameRepository' @@ -161,6 +161,14 @@ export function Iwt2ScreenShell({ export function Iwt2ActionList({ actions }: Iwt2ActionListProps) { const { selectedIndex, setSelectedIndex } = useIwt2ActionList(actions) + const rowRefs = useRef>([]) + + useEffect(() => { + rowRefs.current[selectedIndex]?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }) + }, [selectedIndex]) return (
@@ -174,6 +182,9 @@ export function Iwt2ActionList({ actions }: Iwt2ActionListProps) { onPointerDown={() => { if (!action.disabled) setSelectedIndex(index) }} + ref={(element) => { + rowRefs.current[index] = element + }} type="button" >