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