Android build v1.1.17
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
|
||||
+333
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Iwt2Screen>('menu')
|
||||
const [save, setSave] = useState<Iwt2Save>(loadIwt2Save)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
@@ -126,6 +134,18 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
writeIwt2Save(save)
|
||||
}, [save])
|
||||
|
||||
const setupDualScreenState = useMemo<DualScreenSetupState | null>(
|
||||
() => buildIwt2SetupDualScreenState(screen, selectedBossId, save),
|
||||
[save, screen, selectedBossId],
|
||||
)
|
||||
const workshopDualScreenState = useMemo<DualScreenWorkshopState | null>(
|
||||
() => 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({
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<main
|
||||
@@ -507,3 +545,106 @@ function arenaPauseTitle(
|
||||
}
|
||||
return roguelikeRun.contentType === 'raid' ? 'Raid Roguelike' : 'Dungeon Roguelike'
|
||||
}
|
||||
|
||||
function buildIwt2DualScreenCombatState({
|
||||
abilities,
|
||||
arenaState,
|
||||
bindings,
|
||||
bossMetadata,
|
||||
cooldowns,
|
||||
controllerIconStyle,
|
||||
directPartyTargeting,
|
||||
modeLabel,
|
||||
playerMana,
|
||||
selectedPartyId,
|
||||
status,
|
||||
}: {
|
||||
abilities: Iwt2HealerAbility[]
|
||||
arenaState: Iwt2ArenaState
|
||||
bindings: DualScreenCombatState['bindings']
|
||||
bossMetadata: (typeof IWT2_BOSS_METADATA)[Iwt2BossId]
|
||||
cooldowns: Record<string, number>
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Array<HTMLButtonElement | null>>([])
|
||||
|
||||
useEffect(() => {
|
||||
rowRefs.current[selectedIndex]?.scrollIntoView({
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
})
|
||||
}, [selectedIndex])
|
||||
|
||||
return (
|
||||
<div className="iwt2-action-list">
|
||||
@@ -174,6 +182,9 @@ export function Iwt2ActionList({ actions }: Iwt2ActionListProps) {
|
||||
onPointerDown={() => {
|
||||
if (!action.disabled) setSelectedIndex(index)
|
||||
}}
|
||||
ref={(element) => {
|
||||
rowRefs.current[index] = element
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
|
||||
Reference in New Issue
Block a user