Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2a14bac7d |
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"
|
applicationId "com.warren.iwanttoheal"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 95
|
versionCode 96
|
||||||
versionName "1.1.16"
|
versionName "1.1.17"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// 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 {
|
.combat-touch-lock-status {
|
||||||
background: #101216;
|
background: #101216;
|
||||||
border: 2px solid #d9b55a;
|
border: 2px solid #d9b55a;
|
||||||
@@ -11576,3 +11855,57 @@ h2 {
|
|||||||
height: 20px;
|
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 { useGameAction } from '../../input'
|
||||||
import { summarizeChoiceStacks } from '../../combat/roguelikeUpgrades'
|
import { summarizeChoiceStacks } from '../../combat/roguelikeUpgrades'
|
||||||
|
import {
|
||||||
|
useDualScreen,
|
||||||
|
useDualScreenSetupPublisher,
|
||||||
|
useDualScreenWorkshopPublisher,
|
||||||
|
type DualScreenSetupState,
|
||||||
|
type DualScreenWorkshopState,
|
||||||
|
} from '../../dualScreen'
|
||||||
import { BossArenaScreen } from './screens/BossArenaScreen'
|
import { BossArenaScreen } from './screens/BossArenaScreen'
|
||||||
import {
|
import {
|
||||||
Iwt2CloudSaveScreen,
|
Iwt2CloudSaveScreen,
|
||||||
@@ -17,8 +24,8 @@ import {
|
|||||||
writeIwt2Save,
|
writeIwt2Save,
|
||||||
type Iwt2Save,
|
type Iwt2Save,
|
||||||
} from './save/iwt2Repository'
|
} from './save/iwt2Repository'
|
||||||
import type { Iwt2BossId } from './content/bosses'
|
import { IWT2_BOSS_METADATA, type Iwt2BossId } from './content/bosses'
|
||||||
import { abilitiesForHealer } from './content/healerAbilities'
|
import { abilitiesForHealer, IWT2_HEALER_METADATA } from './content/healerAbilities'
|
||||||
import {
|
import {
|
||||||
buildIwt2OpponentDebuffChoices,
|
buildIwt2OpponentDebuffChoices,
|
||||||
buildIwt2SelfBuffChoices,
|
buildIwt2SelfBuffChoices,
|
||||||
@@ -113,6 +120,7 @@ const MENU_ITEMS: Array<{
|
|||||||
]
|
]
|
||||||
|
|
||||||
export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: () => void }) {
|
export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: () => void }) {
|
||||||
|
const { enabled: dualScreenEnabled } = useDualScreen()
|
||||||
const [screen, setScreen] = useState<Iwt2Screen>('menu')
|
const [screen, setScreen] = useState<Iwt2Screen>('menu')
|
||||||
const [save, setSave] = useState<Iwt2Save>(loadIwt2Save)
|
const [save, setSave] = useState<Iwt2Save>(loadIwt2Save)
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
@@ -126,6 +134,18 @@ export function IWantToHeal2App({ onBackToGameSelect }: { onBackToGameSelect: ()
|
|||||||
writeIwt2Save(save)
|
writeIwt2Save(save)
|
||||||
}, [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) => {
|
useGameAction((action, device) => {
|
||||||
if (screen !== 'menu' || device !== 'controller') return
|
if (screen !== 'menu' || device !== 'controller') return
|
||||||
if (action === 'back') {
|
if (action === 'back') {
|
||||||
@@ -478,3 +498,90 @@ function Iwt2Header({
|
|||||||
</header>
|
</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 { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
|
||||||
import type { MovementVector } from '../../../input'
|
import type { MovementVector } from '../../../input'
|
||||||
import { useGameAction, useInput, useMovementVectorRef } from '../../../input'
|
import { useGameAction, useInput, useMovementVectorRef } from '../../../input'
|
||||||
|
import {
|
||||||
|
useDualScreen,
|
||||||
|
useDualScreenPublisher,
|
||||||
|
type DualScreenCombatState,
|
||||||
|
} from '../../../dualScreen'
|
||||||
|
import type { PartyMember, Role, Spell } from '../../../game'
|
||||||
import {
|
import {
|
||||||
createInitialIwt2ArenaState,
|
createInitialIwt2ArenaState,
|
||||||
tickIwt2Arena,
|
tickIwt2Arena,
|
||||||
@@ -16,6 +22,7 @@ import {
|
|||||||
import { AbilityBar } from '../components/AbilityBar'
|
import { AbilityBar } from '../components/AbilityBar'
|
||||||
import { BossHud } from '../components/BossHud'
|
import { BossHud } from '../components/BossHud'
|
||||||
import { PartyFrames } from '../components/PartyFrames'
|
import { PartyFrames } from '../components/PartyFrames'
|
||||||
|
import { IWT2_CLASS_METADATA } from '../content/classes'
|
||||||
import { IWT2_ABILITY_ACTIONS, IWT2_TARGET_ACTIONS } from '../content/controls'
|
import { IWT2_ABILITY_ACTIONS, IWT2_TARGET_ACTIONS } from '../content/controls'
|
||||||
import { abilitiesForHealer, type Iwt2HealerAbility } from '../content/healerAbilities'
|
import { abilitiesForHealer, type Iwt2HealerAbility } from '../content/healerAbilities'
|
||||||
import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
|
import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
|
||||||
@@ -77,6 +84,7 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
|
|||||||
directPartyTargeting,
|
directPartyTargeting,
|
||||||
lastDevice,
|
lastDevice,
|
||||||
} = useInput()
|
} = useInput()
|
||||||
|
const { enabled: dualScreenEnabled } = useDualScreen()
|
||||||
const activeBindings = bindings[lastDevice]
|
const activeBindings = bindings[lastDevice]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -292,6 +300,36 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
|
|||||||
: roguelikeRun
|
: roguelikeRun
|
||||||
? 'Leave Roguelike'
|
? 'Leave Roguelike'
|
||||||
: `Leave ${modeLabel ?? 'Arena'}`
|
: `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 (
|
return (
|
||||||
<main
|
<main
|
||||||
@@ -507,3 +545,106 @@ function arenaPauseTitle(
|
|||||||
}
|
}
|
||||||
return roguelikeRun.contentType === 'raid' ? 'Raid Roguelike' : 'Dungeon Roguelike'
|
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 { ControllerStylePreview } from '../../../components/ControllerIcons'
|
||||||
import { useGameAction, useInput, type ControllerIconStyle } from '../../../input'
|
import { useGameAction, useInput, type ControllerIconStyle } from '../../../input'
|
||||||
import { getGameMode } from '../../../gameRepository'
|
import { getGameMode } from '../../../gameRepository'
|
||||||
@@ -161,6 +161,14 @@ export function Iwt2ScreenShell({
|
|||||||
|
|
||||||
export function Iwt2ActionList({ actions }: Iwt2ActionListProps) {
|
export function Iwt2ActionList({ actions }: Iwt2ActionListProps) {
|
||||||
const { selectedIndex, setSelectedIndex } = useIwt2ActionList(actions)
|
const { selectedIndex, setSelectedIndex } = useIwt2ActionList(actions)
|
||||||
|
const rowRefs = useRef<Array<HTMLButtonElement | null>>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rowRefs.current[selectedIndex]?.scrollIntoView({
|
||||||
|
block: 'nearest',
|
||||||
|
inline: 'nearest',
|
||||||
|
})
|
||||||
|
}, [selectedIndex])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="iwt2-action-list">
|
<div className="iwt2-action-list">
|
||||||
@@ -174,6 +182,9 @@ export function Iwt2ActionList({ actions }: Iwt2ActionListProps) {
|
|||||||
onPointerDown={() => {
|
onPointerDown={() => {
|
||||||
if (!action.disabled) setSelectedIndex(index)
|
if (!action.disabled) setSelectedIndex(index)
|
||||||
}}
|
}}
|
||||||
|
ref={(element) => {
|
||||||
|
rowRefs.current[index] = element
|
||||||
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
Reference in New Issue
Block a user