Android build v1.1.23
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
Iwt2CloudSaveScreen,
|
||||
Iwt2CustomizeCharacterScreen,
|
||||
Iwt2DungeonsScreen,
|
||||
Iwt2GearUpgradeScreen,
|
||||
Iwt2HunterProfileScreen,
|
||||
Iwt2ModeScreen,
|
||||
Iwt2RoguelikeScreen,
|
||||
@@ -27,6 +28,13 @@ import {
|
||||
type Iwt2Save,
|
||||
} from './save/iwt2Repository'
|
||||
import { IWT2_BOSS_METADATA, type Iwt2BossId } from './content/bosses'
|
||||
import { iwt2BossCoinRewardFor } from './content/bossRewards'
|
||||
import {
|
||||
findIwt2Difficulty,
|
||||
IWT2_DUNGEON_DIFFICULTIES,
|
||||
IWT2_RAID_DIFFICULTIES,
|
||||
type Iwt2Difficulty,
|
||||
} from './content/difficulties'
|
||||
import { abilitiesForHealer, IWT2_HEALER_METADATA } from './content/healerAbilities'
|
||||
import {
|
||||
buildIwt2OpponentDebuffChoices,
|
||||
@@ -48,12 +56,14 @@ type Iwt2Screen =
|
||||
| 'roguelike'
|
||||
| 'roguelike-arena'
|
||||
| 'roguelike-upgrade'
|
||||
| 'gear-upgrade'
|
||||
| 'hunter-profile'
|
||||
| 'customize-character'
|
||||
| 'settings'
|
||||
|
||||
const IWT2_MENU_COLUMNS = 4
|
||||
const IWT2_MENU_COLUMNS = 2
|
||||
const IWT2_ROGUELIKE_CHOICE_COUNT = 3
|
||||
const IWT2_PVP_FIRST_BUFF_EXTRA_TARGET_CHANCE = 0.65
|
||||
|
||||
type Iwt2RoguelikeRunState = {
|
||||
bossIds: Iwt2BossId[]
|
||||
@@ -72,12 +82,6 @@ const MENU_ITEMS: Array<{
|
||||
description: string
|
||||
glyph: string
|
||||
}> = [
|
||||
{
|
||||
screen: 'cloud-save',
|
||||
title: 'Backup Slot',
|
||||
description: 'Save or restore the isolated IWT2 progress slot.',
|
||||
glyph: 'C',
|
||||
},
|
||||
{
|
||||
screen: 'dungeons',
|
||||
title: 'Dungeons',
|
||||
@@ -102,6 +106,12 @@ const MENU_ITEMS: Array<{
|
||||
description: 'Race another healer through roguelike encounters with buffs and sabotage.',
|
||||
glyph: 'P',
|
||||
},
|
||||
{
|
||||
screen: 'gear-upgrade',
|
||||
title: 'Gear Upgrade',
|
||||
description: 'Spend boss coins on class gear slots and infusion abilities.',
|
||||
glyph: 'G',
|
||||
},
|
||||
{
|
||||
screen: 'hunter-profile',
|
||||
title: 'Hunter Profile',
|
||||
@@ -114,6 +124,12 @@ const MENU_ITEMS: Array<{
|
||||
description: 'Choose healer kit, armor palette, and IWT2 hunter callsign.',
|
||||
glyph: 'K',
|
||||
},
|
||||
{
|
||||
screen: 'cloud-save',
|
||||
title: 'Backup Slot',
|
||||
description: 'Choose local, online, or fresh IWT2 progress.',
|
||||
glyph: 'C',
|
||||
},
|
||||
{
|
||||
screen: 'settings',
|
||||
title: 'Settings',
|
||||
@@ -122,17 +138,27 @@ const MENU_ITEMS: Array<{
|
||||
},
|
||||
]
|
||||
|
||||
export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: () => void }) {
|
||||
export function IWantToHeal2App({
|
||||
onlineBackupsAvailable,
|
||||
onBackToGameSelect,
|
||||
}: {
|
||||
onlineBackupsAvailable: boolean
|
||||
onBackToGameSelect: () => void
|
||||
}) {
|
||||
const { enabled: dualScreenEnabled } = useDualScreen()
|
||||
const [screen, setScreen] = useState<Iwt2Screen>('menu')
|
||||
const [save, setSave] = useState<Iwt2Save>(loadIwt2Save)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [selectedBossId, setSelectedBossId] = useState<Iwt2BossId>('bulldrome')
|
||||
const [arenaModeLabel, setArenaModeLabel] = useState('Dungeon')
|
||||
const [arenaDifficulty, setArenaDifficulty] = useState<Iwt2Difficulty>(IWT2_DUNGEON_DIFFICULTIES[0])
|
||||
const [selectedDungeonDifficultySlug, setSelectedDungeonDifficultySlug] = useState(IWT2_DUNGEON_DIFFICULTIES[0].slug)
|
||||
const [selectedRaidDifficultySlug, setSelectedRaidDifficultySlug] = useState(IWT2_RAID_DIFFICULTIES[0].slug)
|
||||
const [roguelikeVariant, setRoguelikeVariant] = useState<Iwt2RoguelikeVariant>('pve')
|
||||
const [roguelikeContentType, setRoguelikeContentType] = useState<Iwt2RoguelikeContentType>('dungeon')
|
||||
const [roguelikeRun, setRoguelikeRun] = useState<Iwt2RoguelikeRunState | null>(null)
|
||||
const [pvpQueueMessage, setPvpQueueMessage] = useState('')
|
||||
const [pvpQueueing, setPvpQueueing] = useState(false)
|
||||
const cancelPvpQueueRef = useRef<(() => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -143,9 +169,16 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
cancelPvpQueueRef.current?.()
|
||||
}, [])
|
||||
|
||||
function cancelIwt2PvpQueue() {
|
||||
cancelPvpQueueRef.current?.()
|
||||
cancelPvpQueueRef.current = null
|
||||
setPvpQueueing(false)
|
||||
setPvpQueueMessage('')
|
||||
}
|
||||
|
||||
const setupDualScreenState = useMemo<DualScreenSetupState | null>(
|
||||
() => buildIwt2SetupDualScreenState(screen, selectedBossId, save),
|
||||
[save, screen, selectedBossId],
|
||||
() => buildIwt2SetupDualScreenState(screen, selectedBossId, save, currentSetupDifficulty(screen, selectedDungeonDifficultySlug, selectedRaidDifficultySlug)),
|
||||
[save, screen, selectedBossId, selectedDungeonDifficultySlug, selectedRaidDifficultySlug],
|
||||
)
|
||||
const workshopDualScreenState = useMemo<DualScreenWorkshopState | null>(
|
||||
() => buildIwt2WorkshopDualScreenState(screen, save),
|
||||
@@ -191,7 +224,8 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
return (
|
||||
<BossArenaScreen
|
||||
bossId={selectedBossId}
|
||||
key={`arena-${selectedBossId}-${arenaModeLabel}`}
|
||||
difficulty={arenaDifficulty}
|
||||
key={`arena-${selectedBossId}-${arenaModeLabel}-${arenaDifficulty.slug}`}
|
||||
modeLabel={arenaModeLabel}
|
||||
save={save}
|
||||
onBack={() => setScreen('menu')}
|
||||
@@ -224,6 +258,10 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
}}
|
||||
save={save}
|
||||
onBack={() => setScreen('roguelike')}
|
||||
onPvpRequeue={() => {
|
||||
setScreen('roguelike')
|
||||
startIwt2RoguelikeRun()
|
||||
}}
|
||||
onSaveUpdated={setSave}
|
||||
/>
|
||||
)
|
||||
@@ -258,12 +296,17 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
<main className="game-shell iwt2-shell">
|
||||
<Iwt2Header save={save} onBackToGameSelect={onBackToGameSelect} />
|
||||
<Iwt2DungeonsScreen
|
||||
difficultySlug={selectedDungeonDifficultySlug}
|
||||
save={save}
|
||||
onBack={() => setScreen('menu')}
|
||||
onOpenBoss={(bossId) => {
|
||||
setArenaModeLabel('Dungeon')
|
||||
onDifficultyChange={setSelectedDungeonDifficultySlug}
|
||||
onOpenBoss={(bossId, difficulty) => {
|
||||
setArenaModeLabel(`${difficulty.name} Dungeon`)
|
||||
setArenaDifficulty(difficulty)
|
||||
setSelectedBossId(bossId)
|
||||
setScreen('arena')
|
||||
}}
|
||||
onPreviewBoss={setSelectedBossId}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
@@ -276,20 +319,23 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
<Iwt2RoguelikeScreen
|
||||
contentType={roguelikeContentType}
|
||||
variant={roguelikeVariant}
|
||||
onBack={() => setScreen('menu')}
|
||||
onBack={() => {
|
||||
cancelIwt2PvpQueue()
|
||||
setScreen('menu')
|
||||
}}
|
||||
onCancelQueue={cancelIwt2PvpQueue}
|
||||
onContentTypeChange={setRoguelikeContentType}
|
||||
onStart={() => {
|
||||
startIwt2RoguelikeRun()
|
||||
}}
|
||||
onVariantChange={(nextVariant) => {
|
||||
cancelPvpQueueRef.current?.()
|
||||
cancelPvpQueueRef.current = null
|
||||
setPvpQueueMessage('')
|
||||
cancelIwt2PvpQueue()
|
||||
setRoguelikeVariant(nextVariant)
|
||||
if (nextVariant === 'pve' && roguelikeContentType === 'stadium') {
|
||||
setRoguelikeContentType('dungeon')
|
||||
}
|
||||
}}
|
||||
queueing={pvpQueueing}
|
||||
queueMessage={pvpQueueMessage}
|
||||
/>
|
||||
</main>
|
||||
@@ -301,13 +347,18 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
<main className="game-shell iwt2-shell">
|
||||
<Iwt2Header save={save} onBackToGameSelect={onBackToGameSelect} />
|
||||
<Iwt2ModeScreen
|
||||
difficultySlug={selectedRaidDifficultySlug}
|
||||
mode="Raids"
|
||||
save={save}
|
||||
onBack={() => setScreen('menu')}
|
||||
onOpenBoss={(bossId) => {
|
||||
setArenaModeLabel('Raid')
|
||||
onDifficultyChange={setSelectedRaidDifficultySlug}
|
||||
onOpenBoss={(bossId, difficulty) => {
|
||||
setArenaModeLabel(`${difficulty.name} Raid`)
|
||||
setArenaDifficulty(difficulty)
|
||||
setSelectedBossId(bossId)
|
||||
setScreen('arena')
|
||||
}}
|
||||
onPreviewBoss={setSelectedBossId}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
@@ -321,6 +372,24 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
)
|
||||
}
|
||||
|
||||
if (screen === 'gear-upgrade') {
|
||||
return (
|
||||
<main className="game-shell iwt2-shell">
|
||||
<Iwt2Header
|
||||
save={save}
|
||||
title="Gear Upgrade"
|
||||
onBack={() => setScreen('menu')}
|
||||
onBackToGameSelect={onBackToGameSelect}
|
||||
/>
|
||||
<Iwt2GearUpgradeScreen
|
||||
save={save}
|
||||
onBack={() => setScreen('menu')}
|
||||
onSaveUpdated={setSave}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (screen === 'customize-character') {
|
||||
return (
|
||||
<main className="game-shell iwt2-shell">
|
||||
@@ -340,6 +409,7 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
<Iwt2Header save={save} onBackToGameSelect={onBackToGameSelect} />
|
||||
<Iwt2CloudSaveScreen
|
||||
save={save}
|
||||
onlineBackupsAvailable={onlineBackupsAvailable}
|
||||
onBack={() => setScreen('menu')}
|
||||
onSaveUpdated={setSave}
|
||||
/>
|
||||
@@ -362,14 +432,11 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
|
||||
{screen === 'menu' && (
|
||||
<section className="iwt2-menu-screen" data-game-nav-active="true">
|
||||
<div className="iwt2-menu-heading">
|
||||
<p className="eyebrow">I Want To Heal 2</p>
|
||||
<h1>Mode Select</h1>
|
||||
</div>
|
||||
<div className="iwt2-menu-grid">
|
||||
{MENU_ITEMS.map((item, index) => (
|
||||
<button
|
||||
className={`iwt2-menu-card ${selectedIndex === index ? 'game-selected' : ''}`}
|
||||
aria-label={`${item.title}. ${item.description}`}
|
||||
data-controller-nav="skip"
|
||||
data-game-selected={selectedIndex === index ? 'true' : undefined}
|
||||
key={`${item.screen}-${item.title}`}
|
||||
@@ -391,28 +458,33 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
||||
)
|
||||
|
||||
function startIwt2RoguelikeRun() {
|
||||
cancelPvpQueueRef.current?.()
|
||||
cancelPvpQueueRef.current = null
|
||||
setPvpQueueMessage('')
|
||||
cancelIwt2PvpQueue()
|
||||
if (roguelikeVariant !== 'pvp') {
|
||||
beginIwt2RoguelikeArena(roguelikeVariant, roguelikeContentType)
|
||||
return
|
||||
}
|
||||
const startStage = 1
|
||||
setPvpQueueing(true)
|
||||
setPvpQueueMessage('Queuing for PvP...')
|
||||
cancelPvpQueueRef.current = startPvpQueueWithCpuFallback<unknown>({
|
||||
contentType: roguelikeContentType,
|
||||
startStage,
|
||||
gameMode: getGameMode(),
|
||||
liveMatchActive: () => false,
|
||||
onSearching: setPvpQueueMessage,
|
||||
onSearching: (message) => {
|
||||
setPvpQueueing(true)
|
||||
setPvpQueueMessage(message)
|
||||
},
|
||||
onCpuMatch: (_difficulty, message) => {
|
||||
cancelPvpQueueRef.current = null
|
||||
setPvpQueueing(false)
|
||||
setPvpQueueMessage(message)
|
||||
beginIwt2RoguelikeArena('pvp', roguelikeContentType)
|
||||
},
|
||||
onLiveMatch: (...liveMatchArgs) => {
|
||||
const message = liveMatchArgs[2]
|
||||
cancelPvpQueueRef.current = null
|
||||
setPvpQueueing(false)
|
||||
setPvpQueueMessage(message)
|
||||
beginIwt2RoguelikeArena('pvp', roguelikeContentType)
|
||||
},
|
||||
@@ -464,7 +536,14 @@ function buildRoguelikeChoices(save: Iwt2Save, variant: Iwt2RoguelikeVariant) {
|
||||
const selfCatalog = [IWT2_REVIVE_PARTY_CHOICE, ...buildIwt2SelfBuffChoices(abilities)]
|
||||
const debuffCatalog = buildIwt2OpponentDebuffChoices(abilities)
|
||||
return {
|
||||
selfChoices: chooseRunChoices(selfCatalog, IWT2_ROGUELIKE_CHOICE_COUNT),
|
||||
selfChoices: variant === 'pvp'
|
||||
? chooseRunChoicesWithPreferredFirst(
|
||||
selfCatalog,
|
||||
IWT2_ROGUELIKE_CHOICE_COUNT,
|
||||
isExtraTargetBuff,
|
||||
IWT2_PVP_FIRST_BUFF_EXTRA_TARGET_CHANCE,
|
||||
)
|
||||
: chooseRunChoices(selfCatalog, IWT2_ROGUELIKE_CHOICE_COUNT),
|
||||
debuffChoices: variant === 'pvp'
|
||||
? chooseRunChoices(debuffCatalog, IWT2_ROGUELIKE_CHOICE_COUNT)
|
||||
: [],
|
||||
@@ -532,23 +611,63 @@ function chooseRunChoices<T>(items: readonly T[], count: number): T[] {
|
||||
return choices
|
||||
}
|
||||
|
||||
function chooseRunChoicesWithPreferredFirst<T>(
|
||||
items: readonly T[],
|
||||
count: number,
|
||||
preferred: (item: T) => boolean,
|
||||
preferredChance: number,
|
||||
): T[] {
|
||||
if (count <= 0) return []
|
||||
const pool = [...items]
|
||||
const choices: T[] = []
|
||||
const preferredPool = pool.filter(preferred)
|
||||
if (preferredPool.length > 0 && Math.random() < preferredChance) {
|
||||
const preferredChoice = preferredPool[Math.floor(Math.random() * preferredPool.length)]
|
||||
const preferredIndex = pool.indexOf(preferredChoice)
|
||||
if (preferredIndex >= 0) {
|
||||
const [choice] = pool.splice(preferredIndex, 1)
|
||||
if (choice) choices.push(choice)
|
||||
}
|
||||
}
|
||||
while (pool.length > 0 && choices.length < count) {
|
||||
const index = Math.floor(Math.random() * pool.length)
|
||||
const [choice] = pool.splice(index, 1)
|
||||
if (choice) choices.push(choice)
|
||||
}
|
||||
return choices
|
||||
}
|
||||
|
||||
function isExtraTargetBuff(choice: Iwt2RoguelikeChoice<Iwt2RoguelikeSelfBuffId>) {
|
||||
return choice.id.endsWith('-extra-target')
|
||||
}
|
||||
|
||||
function Iwt2Header({
|
||||
onBack,
|
||||
onBackToGameSelect,
|
||||
save,
|
||||
title,
|
||||
}: {
|
||||
onBack?: () => void
|
||||
onBackToGameSelect: () => void
|
||||
save: Iwt2Save
|
||||
title?: string
|
||||
}) {
|
||||
return (
|
||||
<header className="topbar app-header">
|
||||
<button className="brand-button" onClick={onBackToGameSelect} type="button">
|
||||
<strong>Games</strong>
|
||||
</button>
|
||||
{title && <strong className="iwt2-header-title">{title}</strong>}
|
||||
<div className="character-summary">
|
||||
<strong>{save.character.name}</strong>
|
||||
<small>IWT2 Level {save.character.level}</small>
|
||||
<small>{save.character.experience} XP</small>
|
||||
</div>
|
||||
{onBack && (
|
||||
<button className="back-button iwt2-header-back" data-controller-nav="skip" onClick={onBack} type="button">
|
||||
Back
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -557,30 +676,44 @@ function buildIwt2SetupDualScreenState(
|
||||
screen: Iwt2Screen,
|
||||
selectedBossId: Iwt2BossId,
|
||||
save: Iwt2Save,
|
||||
difficulty: Iwt2Difficulty,
|
||||
): DualScreenSetupState | null {
|
||||
if (screen !== 'dungeons' && screen !== 'raids') return null
|
||||
const boss = IWT2_BOSS_METADATA[selectedBossId]
|
||||
const raid = screen === 'raids'
|
||||
const coinReward = iwt2BossCoinRewardFor(selectedBossId, difficulty.slug)
|
||||
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,
|
||||
? `${boss.name} raid assignment. Tank holds aggro while party moves around modular boss mechanics. Reward: ${coinReward.name}.`
|
||||
: `${boss.name} arena. Heal the party through melee pressure, telegraphs, hazards, and stun recovery. Reward: ${coinReward.name}.`,
|
||||
difficultyName: difficulty.name,
|
||||
experience: Math.round(125 * difficulty.experienceMultiplier),
|
||||
initials: boss.icon,
|
||||
itemLevel: save.character.level,
|
||||
itemLevel: difficulty.droppedItemLevel,
|
||||
lockedReason: undefined,
|
||||
stats: {
|
||||
damage: `${boss.meleeDamage}`,
|
||||
health: `${boss.maxHealth}`,
|
||||
loot: 'IWT2',
|
||||
xp: '125',
|
||||
damage: `${difficulty.damageMultiplier.toFixed(2)}x`,
|
||||
health: `${difficulty.healthMultiplier.toFixed(2)}x`,
|
||||
loot: coinReward.name,
|
||||
xp: `${difficulty.experienceMultiplier.toFixed(1)}x`,
|
||||
},
|
||||
subtitle: `${raid ? 'Raid' : 'Dungeon'} | 6 Players | ${IWT2_HEALER_METADATA[save.character.healerStyle].name}`,
|
||||
title: raid ? `${boss.name} Raid` : `${boss.name} Arena`,
|
||||
}
|
||||
}
|
||||
|
||||
function currentSetupDifficulty(
|
||||
screen: Iwt2Screen,
|
||||
selectedDungeonDifficultySlug: string,
|
||||
selectedRaidDifficultySlug: string,
|
||||
): Iwt2Difficulty {
|
||||
if (screen === 'raids') {
|
||||
return findIwt2Difficulty(IWT2_RAID_DIFFICULTIES, selectedRaidDifficultySlug)
|
||||
}
|
||||
return findIwt2Difficulty(IWT2_DUNGEON_DIFFICULTIES, selectedDungeonDifficultySlug)
|
||||
}
|
||||
|
||||
function buildIwt2WorkshopDualScreenState(
|
||||
screen: Iwt2Screen,
|
||||
save: Iwt2Save,
|
||||
|
||||
Reference in New Issue
Block a user