Android build v1.1.17
This commit is contained in:
@@ -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