Android build v1.1.15
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user