Android build v1.1.12

This commit is contained in:
Warren H
2026-07-03 00:28:38 -04:00
parent c6e2c61d0a
commit b7cb572631
33 changed files with 1772 additions and 305 deletions
+284 -172
View File
@@ -20,6 +20,11 @@ import {
type GameMode,
} from './gameRepository'
import { focusFirstControl, useGameAction } from './input.tsx'
import {
useDualScreen,
useDualScreenSetupPublisher,
type DualScreenSetupState,
} from './dualScreen'
import { barFillStyle } from './components/barStyles'
const CombatScreen = lazy(() => import('./components/CombatScreen').then((module) => ({ default: module.CombatScreen })))
@@ -77,6 +82,23 @@ type DungeonNavEntry =
| { kind: 'loot'; disabled?: boolean }
| { kind: 'lootSort'; disabled?: boolean }
type RoguelikeNavEntry =
| { kind: 'back' }
| { kind: 'variantPve' }
| { kind: 'variantPvp' }
| { kind: 'pveDungeon' }
| { kind: 'pveRaid' }
| { kind: 'pveStart' }
| { kind: 'pvpDungeon' }
| { kind: 'pvpRaid' }
| { kind: 'pvpStadium' }
| { kind: 'pvpStart' }
type RoguelikeNavPosition = {
row: number
column: number
}
function activityInitials(name: string) {
return name
.split(/\s+/)
@@ -97,11 +119,10 @@ function ScreenLoading() {
)
}
type RoguelikeUpgradeTiming = 'boss' | 'encounter'
type RoguelikeVariant = 'pve' | 'pvp'
type RoguelikeAbilityLabelMode = 'ability' | 'slot'
function App() {
const { enabled: dualScreenEnabled } = useDualScreen()
const [screen, setScreen] = useState<Screen>('menu')
const [account, setAccount] = useState<Account | null>(null)
const [profile, setProfile] = useState<CharacterProfile | null>(null)
@@ -116,8 +137,6 @@ function App() {
const [selectedRaidId, setSelectedRaidId] = useState(20)
const [roguelikeKind, setRoguelikeKind] = useState<'dungeon' | 'raid'>('dungeon')
const [roguelikeVariant, setRoguelikeVariant] = useState<RoguelikeVariant>('pve')
const [roguelikeUpgradeTiming, setRoguelikeUpgradeTiming] = useState<RoguelikeUpgradeTiming>('encounter')
const [roguelikeAbilityLabelMode, setRoguelikeAbilityLabelMode] = useState<RoguelikeAbilityLabelMode>('ability')
const [pvpContentType, setPvpContentType] = useState<PvpContentType>('dungeon')
const [selectedMarathonMode, setSelectedMarathonMode] = useState(false)
const [activityPage, setActivityPage] = useState(0)
@@ -131,6 +150,7 @@ function App() {
const [syncMessage, setSyncMessage] = useState('')
const [homeSelectedIndex, setHomeSelectedIndex] = useState(0)
const [dungeonSelectedIndex, setDungeonSelectedIndex] = useState(0)
const [roguelikeSelectedIndex, setRoguelikeSelectedIndex] = useState(1)
useEffect(() => {
loadAuthSession()
@@ -367,9 +387,13 @@ function App() {
if (!item) return
if (item.screen === 'pvp') {
setRoguelikeVariant('pvp')
setRoguelikeSelectedIndex(2)
setScreen('roguelike')
return
}
if (item.screen === 'roguelike') {
setRoguelikeSelectedIndex(1)
}
if (item.screen === 'dungeons' || item.screen === 'raids') {
const nextOptions = item.screen === 'raids' ? raidOptions : dungeonOptions
setActivityPage(0)
@@ -407,6 +431,10 @@ function App() {
disabled: !profile || profile.character.level < candidateDifficulty.unlockLevel,
})
})
entries.push(
{ kind: 'start', disabled: locked },
{ kind: 'marathon', disabled: locked },
)
tierOptions.forEach((difficultyOption, index) => {
entries.push({
kind: 'tier',
@@ -414,11 +442,7 @@ function App() {
disabled: !profile || profile.character.level < difficultyOption.unlockLevel,
})
})
entries.push(
{ kind: 'start', disabled: locked },
{ kind: 'marathon', disabled: locked },
{ kind: 'loot' },
)
entries.push({ kind: 'loot' })
if (showLoot) entries.push({ kind: 'lootSort' })
return entries
}
@@ -524,6 +548,109 @@ function App() {
else if (entry.kind === 'lootSort') setLootSort((current) => current === 'sequence' ? 'boss' : 'sequence')
}
function startPveRoguelike() {
const baseDungeon = dungeonOptions[0]
const baseRaid = raidOptions[0]
if (roguelikeKind === 'raid') {
setCombatContentId(-2)
setSelectedDifficultyId(baseRaid?.difficulties[0]?.id ?? 101)
} else {
setCombatContentId(-1)
setSelectedDifficultyId(baseDungeon?.difficulties[0]?.id ?? 1)
}
setSelectedMarathonMode(false)
setScreen('combat')
}
function roguelikeEntries() {
const entries: RoguelikeNavEntry[] = [
{ kind: 'back' },
{ kind: 'variantPve' },
{ kind: 'variantPvp' },
]
if (roguelikeVariant === 'pve') {
entries.push(
{ kind: 'pveDungeon' },
{ kind: 'pveRaid' },
{ kind: 'pveStart' },
)
} else {
entries.push(
{ kind: 'pvpDungeon' },
{ kind: 'pvpRaid' },
{ kind: 'pvpStadium' },
{ kind: 'pvpStart' },
)
}
return entries
}
function activeRoguelikeEntry(entries = roguelikeEntries()) {
return entries[Math.min(roguelikeSelectedIndex, entries.length - 1)] ?? entries[0]
}
function roguelikeEntrySelected(kind: RoguelikeNavEntry['kind']) {
return activeRoguelikeEntry()?.kind === kind
}
function roguelikeEntryPosition(entry: RoguelikeNavEntry): RoguelikeNavPosition {
if (entry.kind === 'back') return { row: 0, column: 0 }
if (entry.kind === 'variantPve') return { row: 1, column: 0 }
if (entry.kind === 'variantPvp') return { row: 1, column: 1 }
if (entry.kind === 'pveDungeon' || entry.kind === 'pvpDungeon') return { row: 2, column: 0 }
if (entry.kind === 'pveRaid' || entry.kind === 'pvpRaid') return { row: 2, column: 1 }
if (entry.kind === 'pvpStart') return { row: 3, column: 1 }
return { row: 3, column: 0 }
}
function moveRoguelikeSelection(action: string) {
const entries = roguelikeEntries()
setRoguelikeSelectedIndex((current) => {
const bounded = Math.min(current, entries.length - 1)
const active = entries[bounded]
if (!active) return bounded
const activePosition = roguelikeEntryPosition(active)
const candidates = entries
.map((entry, index) => ({ entry, index, position: roguelikeEntryPosition(entry) }))
.filter(({ index, position }) => {
if (index === bounded) return false
if (action === 'navigateLeft') return position.row === activePosition.row && position.column < activePosition.column
if (action === 'navigateRight') return position.row === activePosition.row && position.column > activePosition.column
if (action === 'navigateUp') return position.row < activePosition.row
return position.row > activePosition.row
})
if (candidates.length === 0) return bounded
candidates.sort((a, b) => {
const aPrimary = Math.abs(a.position.row - activePosition.row)
+ Math.abs(a.position.column - activePosition.column)
const bPrimary = Math.abs(b.position.row - activePosition.row)
+ Math.abs(b.position.column - activePosition.column)
const aSecondary = action === 'navigateLeft' || action === 'navigateRight'
? Math.abs(a.position.column - activePosition.column)
: Math.abs(a.position.row - activePosition.row)
const bSecondary = action === 'navigateLeft' || action === 'navigateRight'
? Math.abs(b.position.column - activePosition.column)
: Math.abs(b.position.row - activePosition.row)
return aPrimary - bPrimary || aSecondary - bSecondary || a.index - b.index
})
return candidates[0]?.index ?? bounded
})
}
function openRoguelikeEntry(entry: RoguelikeNavEntry | undefined) {
if (!entry) return
if (entry.kind === 'back') setScreen('menu')
else if (entry.kind === 'variantPve') setRoguelikeVariant('pve')
else if (entry.kind === 'variantPvp') setRoguelikeVariant('pvp')
else if (entry.kind === 'pveDungeon') setRoguelikeKind('dungeon')
else if (entry.kind === 'pveRaid') setRoguelikeKind('raid')
else if (entry.kind === 'pveStart') startPveRoguelike()
else if (entry.kind === 'pvpDungeon') setPvpContentType('dungeon')
else if (entry.kind === 'pvpRaid') setPvpContentType('raid')
else if (entry.kind === 'pvpStadium') setPvpContentType('stadium')
else if (entry.kind === 'pvpStart') setScreen('pvp')
}
useGameAction((action, device) => {
if (device !== 'controller') return
if (screen === 'menu') {
@@ -558,8 +685,48 @@ function App() {
}
if (action.startsWith('navigate')) moveDungeonSelection(action)
}
if (screen === 'roguelike') {
if (action === 'back') {
setScreen('menu')
return
}
if (action === 'confirm') {
openRoguelikeEntry(activeRoguelikeEntry())
return
}
if (action.startsWith('navigate')) moveRoguelikeSelection(action)
}
})
const setupDualScreenState = useMemo<DualScreenSetupState | null>(() => {
if (
!(screen === 'dungeons' || screen === 'raids')
|| !profile
|| !selectedActivityOption
|| !selectedDifficultyOption
) return null
const locked = profile.character.level < selectedDifficultyOption.unlockLevel
return {
contentType: selectedActivityOption.contentType,
title: selectedActivityOption.name,
subtitle: `${selectedActivityOption.locationName} | Level ${selectedActivityOption.recommendedLevel} | ${selectedActivityOption.partySize} Players`,
description: selectedActivityOption.description,
initials: activityInitials(selectedActivityOption.name),
difficultyName: selectedDifficultyOption.name,
itemLevel: selectedDifficultyOption.droppedItemLevel,
experience: Math.round(selectedActivityOption.experienceReward * selectedDifficultyOption.experienceMultiplier),
lockedReason: locked ? `Unlocks at level ${selectedDifficultyOption.unlockLevel}` : undefined,
stats: {
health: `${selectedDifficultyOption.healthMultiplier.toFixed(2)}x`,
damage: `${selectedDifficultyOption.damageMultiplier.toFixed(2)}x`,
xp: `${selectedDifficultyOption.experienceMultiplier.toFixed(1)}x`,
loot: `iLvl ${selectedDifficultyOption.droppedItemLevel}`,
},
}
}, [profile, screen, selectedActivityOption, selectedDifficultyOption])
useDualScreenSetupPublisher(setupDualScreenState, dualScreenEnabled)
if (error) {
return (
<main className="game-shell">
@@ -604,8 +771,8 @@ function App() {
marathonMode={selectedMarathonMode && combatContentId > 0}
profile={profile}
roguelikeMode={combatContentId < 0 ? roguelikeKind : undefined}
roguelikeUpgradeTiming={combatContentId < 0 ? roguelikeUpgradeTiming : undefined}
roguelikeAbilityLabelMode={combatContentId < 0 ? roguelikeAbilityLabelMode : undefined}
roguelikeUpgradeTiming={combatContentId < 0 ? 'encounter' : undefined}
roguelikeAbilityLabelMode={combatContentId < 0 ? 'ability' : undefined}
roguelikeEncounterPool={combatContentId < 0 ? roguelikePool : undefined}
startPart={1}
onExit={() => {
@@ -659,19 +826,6 @@ function App() {
? 100
: Math.min(100, (experienceIntoLevel / experienceForLevel) * 100)
const dungeon = selectedDungeonOption ?? profile.dungeons[0]!
const startPveRoguelike = () => {
const baseDungeon = dungeonOptions[0]
const baseRaid = raidOptions[0]
if (roguelikeKind === 'raid') {
setCombatContentId(-2)
setSelectedDifficultyId(baseRaid?.difficulties[0]?.id ?? 101)
} else {
setCombatContentId(-1)
setSelectedDifficultyId(baseDungeon?.difficulties[0]?.id ?? 1)
}
setSelectedMarathonMode(false)
setScreen('combat')
}
const activityPageStart = activityOptions.length === 0
? 0
: currentActivityPage * ACTIVITY_PAGE_SIZE + 1
@@ -759,23 +913,30 @@ function App() {
)}
{screen === 'roguelike' && (
<section className="content-screen">
<section className="content-screen roguelike-screen" data-game-nav-active="true">
<ScreenHeading
backClassName={roguelikeEntrySelected('back') ? 'game-selected' : ''}
backControllerSkip
eyebrow="Endless Draft"
title="Roguelike"
onBack={() => setScreen('menu')}
onBackPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'back'))}
/>
<div className="roguelike-variant-row">
<button
className={`text-button ${roguelikeVariant === 'pve' ? 'active' : ''}`}
className={`text-button ${roguelikeVariant === 'pve' ? 'active' : ''} ${roguelikeEntrySelected('variantPve') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setRoguelikeVariant('pve')}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'variantPve'))}
type="button"
>
PvE
</button>
<button
className={`text-button ${roguelikeVariant === 'pvp' ? 'active' : ''}`}
className={`text-button ${roguelikeVariant === 'pvp' ? 'active' : ''} ${roguelikeEntrySelected('variantPvp') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setRoguelikeVariant('pvp')}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'variantPvp'))}
type="button"
>
PvP
@@ -790,65 +951,25 @@ function App() {
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${roguelikeKind === 'dungeon' ? 'active' : ''}`}
className={`text-button ${roguelikeKind === 'dungeon' ? 'active' : ''} ${roguelikeEntrySelected('pveDungeon') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setRoguelikeKind('dungeon')}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'pveDungeon'))}
type="button"
>
Dungeon
</button>
<button
className={`text-button ${roguelikeKind === 'raid' ? 'active' : ''}`}
className={`text-button ${roguelikeKind === 'raid' ? 'active' : ''} ${roguelikeEntrySelected('pveRaid') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setRoguelikeKind('raid')}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'pveRaid'))}
type="button"
>
Raid
</button>
</div>
</div>
<div className="roguelike-option-panel">
<div>
<p className="eyebrow">Upgrade Timing</p>
<h2>Buff Drafts</h2>
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${roguelikeUpgradeTiming === 'encounter' ? 'active' : ''}`}
onClick={() => setRoguelikeUpgradeTiming('encounter')}
type="button"
>
Every Encounter
</button>
<button
className={`text-button ${roguelikeUpgradeTiming === 'boss' ? 'active' : ''}`}
onClick={() => setRoguelikeUpgradeTiming('boss')}
type="button"
>
Boss Only
</button>
</div>
</div>
<div className="roguelike-option-panel">
<div>
<p className="eyebrow">Upgrade Labels</p>
<h2>Display Mode</h2>
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${roguelikeAbilityLabelMode === 'ability' ? 'active' : ''}`}
onClick={() => setRoguelikeAbilityLabelMode('ability')}
type="button"
>
Ability Names
</button>
<button
className={`text-button ${roguelikeAbilityLabelMode === 'slot' ? 'active' : ''}`}
onClick={() => setRoguelikeAbilityLabelMode('slot')}
type="button"
>
Slot Names
</button>
</div>
</div>
<div className="menu-card pvp-queue-panel">
<span>{roguelikeKind === 'raid' ? 'R' : 'D'}</span>
<div>
@@ -860,8 +981,10 @@ function App() {
</small>
</div>
<button
className="text-button"
className={`text-button ${roguelikeEntrySelected('pveStart') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={startPveRoguelike}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'pveStart'))}
type="button"
>
Start Run
@@ -878,22 +1001,28 @@ function App() {
</div>
<div className="roguelike-timing-row">
<button
className={`text-button ${pvpContentType === 'dungeon' ? 'active' : ''}`}
className={`text-button ${pvpContentType === 'dungeon' ? 'active' : ''} ${roguelikeEntrySelected('pvpDungeon') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setPvpContentType('dungeon')}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'pvpDungeon'))}
type="button"
>
Dungeon
</button>
<button
className={`text-button ${pvpContentType === 'raid' ? 'active' : ''}`}
className={`text-button ${pvpContentType === 'raid' ? 'active' : ''} ${roguelikeEntrySelected('pvpRaid') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setPvpContentType('raid')}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'pvpRaid'))}
type="button"
>
Raid
</button>
<button
className={`text-button ${pvpContentType === 'stadium' ? 'active' : ''}`}
className={`text-button ${pvpContentType === 'stadium' ? 'active' : ''} ${roguelikeEntrySelected('pvpStadium') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setPvpContentType('stadium')}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'pvpStadium'))}
type="button"
>
Stadium
@@ -913,8 +1042,10 @@ function App() {
</small>
</div>
<button
className="text-button"
className={`text-button ${roguelikeEntrySelected('pvpStart') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setScreen('pvp')}
onPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'pvpStart'))}
type="button"
>
Start Match
@@ -962,14 +1093,13 @@ function App() {
<section className="content-screen dungeon-run-screen" data-game-nav-active="true">
<div className="dungeon-run-board">
<div className="dungeon-run-main">
<article className="run-summary-card dungeon-focus-card">
<div className={`dungeon-art ${activity.contentType === 'raid' ? 'raid-art' : ''}`}>
{activityInitials(activity.name)}
</div>
<div className="run-summary-copy">
<p className="eyebrow">Selected Run</p>
<div className="run-title-row">
<h2>{activity.name}</h2>
<section className="run-setup-panel dungeon-choice-panel">
<div className="run-setup-heading">
<div>
<p className="eyebrow">Pick Run</p>
<h2>{screen === 'raids' ? 'Raid' : 'Dungeon'}</h2>
</div>
<div className="dungeon-heading-actions">
<button
className={`back-button inline-back-button ${dungeonEntrySelected('back') ? 'game-selected' : ''}`}
data-controller-nav="skip"
@@ -979,26 +1109,8 @@ function App() {
>
Back
</button>
</div>
<p>{activity.description}</p>
<div className="tag-row">
<span>Level {activity.recommendedLevel}</span>
<span>{activity.partySize} Players</span>
<span>{selectedDifficulty.name}</span>
<span>iLvl {selectedDifficulty.droppedItemLevel}</span>
<span>{Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP</span>
</div>
</div>
</article>
<section className="run-setup-panel dungeon-choice-panel">
<div className="run-setup-heading">
<div>
<p className="eyebrow">Pick Run</p>
<h2>{screen === 'raids' ? 'Raid' : 'Dungeon'}</h2>
</div>
{activityPageCount > 1 ? (
<div className="activity-pager" aria-label={`${screen === 'raids' ? 'Raid' : 'Dungeon'} pages`}>
{activityPageCount > 1 ? (
<div className="activity-pager" aria-label={`${screen === 'raids' ? 'Raid' : 'Dungeon'} pages`}>
<button
className={dungeonEntrySelected('pagePrev') ? 'game-selected' : ''}
data-controller-nav="skip"
@@ -1020,10 +1132,11 @@ function App() {
>
Next
</button>
</div>
) : (
<small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
)}
</div>
) : (
<small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
)}
</div>
</div>
<div className="activity-card-grid dungeon-choice-grid">
{pagedActivityOptions.map((candidate, index) => {
@@ -1062,6 +1175,52 @@ function App() {
</div>
<aside className="dungeon-setup-rail">
<section className="run-setup-panel part-setup-panel">
<div className="run-setup-heading">
<div>
<p className="eyebrow">Start</p>
<h2>Run</h2>
</div>
<small>
{difficultyLocked
? `Unlocks at level ${selectedDifficulty.unlockLevel}`
: 'Pick a hunt or marathon.'}
</small>
</div>
<div className="part-picker">
<button
className={`primary-button selected-part ${dungeonEntrySelected('start') ? 'game-selected' : ''}`}
data-controller-nav="skip"
disabled={difficultyLocked}
onClick={() => {
setSelectedMarathonMode(false)
setCombatContentId(activity.id)
setSelectedDifficultyId(selectedDifficulty.id)
setScreen('combat')
}}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'start'))}
type="button"
>
Start Hunt
</button>
<button
className={`primary-button ${selectedMarathonMode ? 'selected-part' : ''} ${dungeonEntrySelected('marathon') ? 'game-selected' : ''}`}
data-controller-nav="skip"
disabled={difficultyLocked}
onClick={() => {
setSelectedMarathonMode(true)
setCombatContentId(activity.id)
setSelectedDifficultyId(selectedDifficulty.id)
setScreen('combat')
}}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'marathon'))}
type="button"
>
Marathon
</button>
</div>
</section>
<section className="run-setup-panel tier-setup-panel">
<div className="run-setup-heading">
<div>
@@ -1109,67 +1268,6 @@ function App() {
</div>
</section>
<section className="run-setup-panel part-setup-panel">
<div className="run-setup-heading">
<div>
<p className="eyebrow">Start</p>
<h2>Run</h2>
</div>
<small>
{difficultyLocked
? `Unlocks at level ${selectedDifficulty.unlockLevel}`
: 'Marathon keeps health and mana between boss kills.'}
</small>
</div>
<div className="part-picker">
<button
className={`primary-button selected-part ${dungeonEntrySelected('start') ? 'game-selected' : ''}`}
data-controller-nav="skip"
disabled={difficultyLocked}
onClick={() => {
setSelectedMarathonMode(false)
setCombatContentId(activity.id)
setSelectedDifficultyId(selectedDifficulty.id)
setScreen('combat')
}}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'start'))}
type="button"
>
Start Hunt
</button>
<button
className={`primary-button ${selectedMarathonMode ? 'selected-part' : ''} ${dungeonEntrySelected('marathon') ? 'game-selected' : ''}`}
data-controller-nav="skip"
disabled={difficultyLocked}
onClick={() => {
setSelectedMarathonMode(true)
setCombatContentId(activity.id)
setSelectedDifficultyId(selectedDifficulty.id)
setScreen('combat')
}}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'marathon'))}
type="button"
>
Marathon
</button>
</div>
</section>
<div className="difficulty-section compact-difficulty-section">
<div className={`difficulty-summary ${difficultyLocked ? 'locked' : ''}`}>
<div>
<strong>{selectedDifficulty.name}</strong>
<small>{difficultyLocked ? `Unlocks at level ${selectedDifficulty.unlockLevel}` : selectedDifficulty.description}</small>
</div>
<dl>
<div><dt>Health</dt><dd>{selectedDifficulty.healthMultiplier.toFixed(2)}x</dd></div>
<div><dt>Damage</dt><dd>{selectedDifficulty.damageMultiplier.toFixed(2)}x</dd></div>
<div><dt>XP</dt><dd>{selectedDifficulty.experienceMultiplier.toFixed(1)}x</dd></div>
<div><dt>Loot</dt><dd>iLvl {selectedDifficulty.droppedItemLevel}</dd></div>
</dl>
</div>
</div>
<div className="loot-preview-section">
<div className="equipment-heading toggle-heading">
<div>
@@ -1367,13 +1465,19 @@ function App() {
}
function ScreenHeading({
backClassName = '',
backControllerSkip = false,
eyebrow,
title,
onBack,
onBackPointerDown,
}: {
backClassName?: string
backControllerSkip?: boolean
eyebrow: string
title: string
onBack: () => void
onBackPointerDown?: () => void
}) {
return (
<div className="screen-heading">
@@ -1381,7 +1485,15 @@ function ScreenHeading({
<p className="eyebrow">{eyebrow}</p>
<h1>{title}</h1>
</div>
<button className="back-button" onClick={onBack} type="button">Back</button>
<button
className={`back-button ${backClassName}`}
data-controller-nav={backControllerSkip ? 'skip' : undefined}
onClick={onBack}
onPointerDown={onBackPointerDown}
type="button"
>
Back
</button>
</div>
)
}