Android build v1.1.15

This commit is contained in:
Warren H
2026-07-04 12:43:59 -04:00
parent a849cd2c69
commit 99e97e5208
47 changed files with 8470 additions and 1610 deletions
+337
View File
@@ -0,0 +1,337 @@
import { useMemo, useState, type ReactNode } from 'react'
import { ControllerStylePreview } from '../../../components/ControllerIcons'
import { useGameAction, useInput, type ControllerIconStyle } from '../../../input'
import type { Iwt2Save } from '../save/iwt2Repository'
import type { Iwt2BossId } from '../content/bosses'
type Iwt2NavAction = {
key: string
label: string
detail?: string
value?: string
onConfirm: () => void
}
type Iwt2ScreenShellProps = {
title: string
eyebrow?: string
onBack: () => void
children: ReactNode
}
type Iwt2ActionListProps = {
actions: Iwt2NavAction[]
}
function useIwt2ActionList(actions: Iwt2NavAction[]) {
const [selectedIndex, setSelectedIndex] = useState(0)
const activeIndex = Math.min(selectedIndex, Math.max(0, actions.length - 1))
useGameAction((action, device) => {
if (device !== 'controller' || actions.length === 0) return
if (action === 'navigateUp' || action === 'navigateLeft') {
setSelectedIndex((current) => Math.max(0, current - 1))
} else if (action === 'navigateDown' || action === 'navigateRight') {
setSelectedIndex((current) => Math.min(actions.length - 1, current + 1))
} else if (action === 'confirm') {
actions[activeIndex]?.onConfirm()
}
})
return { selectedIndex: activeIndex, setSelectedIndex }
}
const MODE_DESCRIPTIONS = {
Raids: 'Large-party boss fights will use the same 2D arena contract with larger rosters and raid-scale mechanics.',
Roguelike: 'Runs will chain rooms, rewards, healer builds, and IWT2-only inventory progression.',
PvP: 'Player-vs-player healing scenarios will reuse controller targeting and class identity without IWT1 state bleed.',
} satisfies Record<'Raids' | 'Roguelike' | 'PvP', string>
const CONTROLLER_ICON_OPTIONS: ControllerIconStyle[] = ['playstation', 'xbox', 'nintendo']
const CONTROLLER_ICON_LABELS: Record<ControllerIconStyle, string> = {
xbox: 'Xbox',
playstation: 'PlayStation',
nintendo: 'Nintendo',
}
export function Iwt2ScreenShell({
children,
eyebrow = 'I Want To Heal 2',
onBack,
title,
}: Iwt2ScreenShellProps) {
useGameAction((action, device) => {
if (device === 'controller' && action === 'back') onBack()
})
return (
<section className="content-screen iwt2-screen-shell" data-game-nav-active="true">
<div className="screen-heading">
<div>
<p className="eyebrow">{eyebrow}</p>
<h1>{title}</h1>
</div>
<button className="back-button" data-controller-nav="skip" onClick={onBack} type="button">
Back
</button>
</div>
{children}
</section>
)
}
export function Iwt2ActionList({ actions }: Iwt2ActionListProps) {
const { selectedIndex, setSelectedIndex } = useIwt2ActionList(actions)
return (
<div className="iwt2-action-list">
{actions.map((action, index) => (
<button
className={`iwt2-action-row ${selectedIndex === index ? 'game-selected selected' : ''}`}
data-controller-nav="skip"
key={action.key}
onClick={action.onConfirm}
onPointerDown={() => setSelectedIndex(index)}
type="button"
>
<span>
<strong>{action.label}</strong>
{action.detail && <small>{action.detail}</small>}
</span>
{action.value && <em>{action.value}</em>}
</button>
))}
</div>
)
}
export function Iwt2DungeonsScreen({
onBack,
onOpenBoss,
}: {
onBack: () => void
onOpenBoss: (bossId: Iwt2BossId) => void
}) {
const actions = useMemo<Iwt2NavAction[]>(() => [
{
key: 'bulldrome',
label: 'Bulldrome Arena',
detail: 'Level 1 field boss. Charge lanes, tank pressure, and periodic ground slam.',
value: 'Ready',
onConfirm: () => onOpenBoss('bulldrome'),
},
{
key: 'yian-kut-ku',
label: 'Yian Kut Ku Arena',
detail: 'Bouncing fireballs, capped fire puddles, and bird waves at 90% and 40% health.',
value: 'Ready',
onConfirm: () => onOpenBoss('yian-kut-ku'),
},
{
key: 'queue',
label: 'Dungeon Queue',
detail: 'Future matchmaking shell for multi-boss dungeon routes.',
value: 'Planned',
onConfirm: () => {},
},
], [onOpenBoss])
return (
<Iwt2ScreenShell title="Dungeons" onBack={onBack}>
<Iwt2ActionList actions={actions} />
</Iwt2ScreenShell>
)
}
export function Iwt2ModePlaceholderScreen({
mode,
onBack,
}: {
mode: 'Raids' | 'Roguelike' | 'PvP'
onBack: () => void
}) {
const actions = useMemo<Iwt2NavAction[]>(() => [
{
key: 'status',
label: `${mode} Prototype`,
detail: MODE_DESCRIPTIONS[mode],
value: 'Shell',
onConfirm: () => {},
},
], [mode])
return (
<Iwt2ScreenShell title={mode} onBack={onBack}>
<Iwt2ActionList actions={actions} />
</Iwt2ScreenShell>
)
}
export function Iwt2HunterProfileScreen({
onBack,
save,
}: {
onBack: () => void
save: Iwt2Save
}) {
const bulldromeKills = save.collectionLog.bossKills.bulldrome ?? 0
const yianKills = save.collectionLog.bossKills['yian-kut-ku'] ?? 0
const bulldromeDrops = save.collectionLog.dropsFound['raw-bulldrome-coin'] ?? 0
const yianDrops = save.collectionLog.dropsFound['yian-kut-ku-scale'] ?? 0
return (
<Iwt2ScreenShell title="Hunter Profile" onBack={onBack}>
<div className="iwt2-profile-grid">
<div className="iwt2-list-panel">
<div className="iwt2-list-row">
<strong>Name</strong>
<span>{save.character.name}</span>
</div>
<div className="iwt2-list-row">
<strong>Level</strong>
<span>{save.character.level}</span>
</div>
<div className="iwt2-list-row">
<strong>Experience</strong>
<span>{save.character.experience} XP</span>
</div>
</div>
<div className="iwt2-list-panel">
<div className="iwt2-list-row">
<strong>Bulldrome Kills</strong>
<span>{bulldromeKills}</span>
</div>
<div className="iwt2-list-row">
<strong>Raw Bulldrome Coins</strong>
<span>{bulldromeDrops}</span>
</div>
<div className="iwt2-list-row">
<strong>Yian Kut Ku Kills</strong>
<span>{yianKills}</span>
</div>
<div className="iwt2-list-row">
<strong>Yian Kut Ku Scales</strong>
<span>{yianDrops}</span>
</div>
<div className="iwt2-list-row">
<strong>IWT2 Inventory</strong>
<span>{save.inventory.length} slots</span>
</div>
</div>
</div>
</Iwt2ScreenShell>
)
}
export function Iwt2CustomizeCharacterScreen({
onBack,
save,
}: {
onBack: () => void
save: Iwt2Save
}) {
const actions = useMemo<Iwt2NavAction[]>(() => [
{
key: 'class',
label: 'Healer Style',
detail: 'Current vertical slice uses Field Medic abilities based on IWT1 dungeon spells.',
value: 'Field Medic',
onConfirm: () => {},
},
{
key: 'appearance',
label: 'Armor Palette',
detail: 'Character cosmetics will stay scoped to IWT2 save data.',
value: 'Default',
onConfirm: () => {},
},
{
key: 'name',
label: 'Character Name',
detail: 'Name editing shell for the IWT2 hunter profile.',
value: save.character.name,
onConfirm: () => {},
},
], [save.character.name])
return (
<Iwt2ScreenShell title="Customize Character" onBack={onBack}>
<Iwt2ActionList actions={actions} />
</Iwt2ScreenShell>
)
}
export function Iwt2CloudSaveScreen({
onBack,
save,
}: {
onBack: () => void
save: Iwt2Save
}) {
const savedAt = new Date(save.updatedAt).toLocaleString()
const actions = useMemo<Iwt2NavAction[]>(() => [
{
key: 'status',
label: 'Cloud Save Slot',
detail: 'Local IWT2 save is isolated and ready for cloud sync integration.',
value: 'Not linked',
onConfirm: () => {},
},
{
key: 'last-save',
label: 'Last Local Save',
detail: savedAt,
value: 'Local',
onConfirm: () => {},
},
], [savedAt])
return (
<Iwt2ScreenShell title="Cloud Save" onBack={onBack}>
<Iwt2ActionList actions={actions} />
</Iwt2ScreenShell>
)
}
export function Iwt2SettingsScreen({ onBack }: { onBack: () => void }) {
const {
controllerIconStyle,
directPartyTargeting,
setControllerIconStyle,
setDirectPartyTargeting,
} = useInput()
const actions = useMemo<Iwt2NavAction[]>(() => [
{
key: 'direct-targeting',
label: 'Target Party Members With Bindings',
detail: 'When enabled, party frames show direct target button icons instead of D-pad cycling.',
value: directPartyTargeting ? 'On' : 'Off',
onConfirm: () => setDirectPartyTargeting(!directPartyTargeting),
},
...CONTROLLER_ICON_OPTIONS.map((style) => ({
key: `icons-${style}`,
label: `${CONTROLLER_ICON_LABELS[style]} Button Icons`,
detail: 'Changes controller glyphs shown on IWT2 frames and spell controls.',
value: controllerIconStyle === style ? 'Selected' : '',
onConfirm: () => setControllerIconStyle(style),
})),
], [
controllerIconStyle,
directPartyTargeting,
setControllerIconStyle,
setDirectPartyTargeting,
])
return (
<Iwt2ScreenShell title="Settings" onBack={onBack}>
<div className="iwt2-settings-panel">
<Iwt2ActionList actions={actions} />
<div className="iwt2-controller-preview" aria-label="Current controller icon style">
<span>{CONTROLLER_ICON_LABELS[controllerIconStyle]}</span>
<ControllerStylePreview iconStyle={controllerIconStyle} />
</div>
</div>
</Iwt2ScreenShell>
)
}