Android build v1.0.39
This commit is contained in:
+53
@@ -1801,6 +1801,35 @@ h2 {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.activity-pager {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.activity-pager button {
|
||||
background: #15161c;
|
||||
border: 2px solid #090a0d;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
min-height: 34px;
|
||||
outline: 2px solid #41404a;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.activity-pager button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.activity-pager span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tier-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -2069,6 +2098,16 @@ h2 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activity-pager button {
|
||||
font-size: 9px;
|
||||
min-height: 28px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.activity-pager span {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dungeon-run-screen .eyebrow {
|
||||
font-size: 7px;
|
||||
margin-bottom: 5px;
|
||||
@@ -2273,6 +2312,20 @@ h2 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.activity-pager {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.activity-pager button {
|
||||
font-size: 8px;
|
||||
min-height: 24px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.activity-pager span {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.dungeon-choice-grid {
|
||||
grid-auto-rows: minmax(52px, max-content);
|
||||
}
|
||||
|
||||
+74
-48
@@ -55,6 +55,7 @@ const MENU_ITEMS: Array<{
|
||||
|
||||
const LAST_DIFFICULTY_KEY = 'i-want-to-heal:last-difficulty'
|
||||
const SHOW_LEADERBOARDS = false
|
||||
const ACTIVITY_PAGE_SIZE = 6
|
||||
|
||||
function activityInitials(name: string) {
|
||||
return name
|
||||
@@ -81,14 +82,15 @@ function App() {
|
||||
return Number.isFinite(saved) && saved > 0 ? saved : 1
|
||||
})
|
||||
const [selectedDungeonId, setSelectedDungeonId] = useState(1)
|
||||
const [selectedRaidId, setSelectedRaidId] = useState(2)
|
||||
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 [selectedPart, setSelectedPart] = useState(1)
|
||||
const [selectedHardMode, setSelectedHardMode] = useState(false)
|
||||
const [selectedMarathonMode, setSelectedMarathonMode] = useState(false)
|
||||
const [activityPage, setActivityPage] = useState(0)
|
||||
const [combatContentId, setCombatContentId] = useState(1)
|
||||
const [leaderboardCategory, setLeaderboardCategory] = useState<'part_1' | 'part_2' | 'part_3' | 'full_run'>('part_1')
|
||||
const [showLoot, setShowLoot] = useState(false)
|
||||
@@ -236,7 +238,8 @@ function App() {
|
||||
<CombatScreen
|
||||
difficulty={difficulty}
|
||||
dungeon={dungeon}
|
||||
hardMode={selectedHardMode && combatContentId > 0}
|
||||
hardMode={false}
|
||||
marathonMode={selectedMarathonMode && combatContentId > 0}
|
||||
profile={profile}
|
||||
roguelikeMode={combatContentId < 0 ? roguelikeKind : undefined}
|
||||
roguelikeUpgradeTiming={combatContentId < 0 ? roguelikeUpgradeTiming : undefined}
|
||||
@@ -296,7 +299,7 @@ function App() {
|
||||
setSelectedDifficultyId(baseDungeon?.difficulties[0]?.id ?? 1)
|
||||
}
|
||||
setSelectedPart(1)
|
||||
setSelectedHardMode(false)
|
||||
setSelectedMarathonMode(false)
|
||||
setScreen('combat')
|
||||
}
|
||||
const tierOptions = activityOptions
|
||||
@@ -318,6 +321,12 @@ function App() {
|
||||
const tierActivityOptions = activityOptions.filter((option) =>
|
||||
option.difficulties.some((difficulty) => difficulty.droppedItemLevel === selectedTierItemLevel),
|
||||
)
|
||||
const activityPageCount = Math.max(1, Math.ceil(tierActivityOptions.length / ACTIVITY_PAGE_SIZE))
|
||||
const currentActivityPage = Math.min(activityPage, activityPageCount - 1)
|
||||
const pagedActivityOptions = tierActivityOptions.slice(
|
||||
currentActivityPage * ACTIVITY_PAGE_SIZE,
|
||||
currentActivityPage * ACTIVITY_PAGE_SIZE + ACTIVITY_PAGE_SIZE,
|
||||
)
|
||||
const selectedActivityId = screen === 'raids' && raid ? raid.id : dungeon.id
|
||||
const activity = tierActivityOptions.find((candidate) => candidate.id === selectedActivityId)
|
||||
?? tierActivityOptions[0]
|
||||
@@ -326,15 +335,9 @@ function App() {
|
||||
(candidate) => candidate.droppedItemLevel === selectedTierItemLevel,
|
||||
) ?? activity.difficulties[0]
|
||||
const difficultyLocked = profile.character.level < selectedDifficulty.unlockLevel
|
||||
const completedSections = activity.contentType === 'raid'
|
||||
? profile.completedRaidPhases
|
||||
: profile.completedDungeonParts
|
||||
const sectionName = activity.contentType === 'raid' ? 'Phase' : 'Part'
|
||||
const parts = [
|
||||
{ part: 1, name: `${sectionName} 1`, encounterCount: 3, unlocked: true, hardUnlocked: completedSections >= 1 },
|
||||
{ part: 2, name: `${sectionName} 2`, encounterCount: 3, unlocked: completedSections >= 1, hardUnlocked: completedSections >= 2 },
|
||||
{ part: 3, name: `${sectionName} 3`, encounterCount: 3, unlocked: completedSections >= 2, hardUnlocked: completedSections >= 3 },
|
||||
]
|
||||
const activityCompletionCount = activity.completionCount ?? 0
|
||||
const marathonUnlocked = activityCompletionCount >= 10
|
||||
const cloudSync = getCloudSyncStatus()
|
||||
const canShowCloudSync = account.id !== -1 && cloudSync.available
|
||||
const lootPreviewEncounters = [...activity.encounters]
|
||||
@@ -640,10 +643,30 @@ function App() {
|
||||
<p className="eyebrow">Pick Run</p>
|
||||
<h2>{screen === 'raids' ? 'Raid' : 'Dungeon'}</h2>
|
||||
</div>
|
||||
<small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
|
||||
{activityPageCount > 1 ? (
|
||||
<div className="activity-pager" aria-label={`${screen === 'raids' ? 'Raid' : 'Dungeon'} pages`}>
|
||||
<button
|
||||
disabled={currentActivityPage === 0}
|
||||
onClick={() => setActivityPage((page) => Math.max(0, page - 1))}
|
||||
type="button"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<span>{currentActivityPage + 1}/{activityPageCount}</span>
|
||||
<button
|
||||
disabled={currentActivityPage >= activityPageCount - 1}
|
||||
onClick={() => setActivityPage((page) => Math.min(activityPageCount - 1, page + 1))}
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="activity-card-grid dungeon-choice-grid">
|
||||
{tierActivityOptions.map((candidate) => {
|
||||
{pagedActivityOptions.map((candidate) => {
|
||||
const difficulty = candidate.difficulties.find(
|
||||
(option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel,
|
||||
) ?? candidate.difficulties[0]
|
||||
@@ -695,6 +718,7 @@ function App() {
|
||||
disabled={locked}
|
||||
key={difficulty.id}
|
||||
onClick={() => {
|
||||
setActivityPage(0)
|
||||
const nextActivity = activity.difficulties.some(
|
||||
(candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel,
|
||||
)
|
||||
@@ -725,43 +749,45 @@ function App() {
|
||||
<div className="run-setup-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Start</p>
|
||||
<h2>{sectionName}</h2>
|
||||
<h2>Run</h2>
|
||||
</div>
|
||||
<small>{difficultyLocked ? `Unlocks at level ${selectedDifficulty.unlockLevel}` : 'Choose a section to launch.'}</small>
|
||||
<small>
|
||||
{difficultyLocked
|
||||
? `Unlocks at level ${selectedDifficulty.unlockLevel}`
|
||||
: marathonUnlocked
|
||||
? 'Marathon keeps health and mana between boss kills.'
|
||||
: `Marathon unlocks after 10 clears (${activityCompletionCount}/10).`}
|
||||
</small>
|
||||
</div>
|
||||
<div className="part-picker">
|
||||
{parts.map((p) => (
|
||||
<div className="part-start-row" key={p.part}>
|
||||
<button
|
||||
className={`primary-button ${selectedPart === p.part && !selectedHardMode ? 'selected-part' : ''} ${!p.unlocked ? 'locked' : ''}`}
|
||||
disabled={difficultyLocked || !p.unlocked}
|
||||
onClick={() => {
|
||||
setSelectedPart(p.part)
|
||||
setSelectedHardMode(false)
|
||||
setCombatContentId(activity.id)
|
||||
setSelectedDifficultyId(selectedDifficulty.id)
|
||||
setScreen('combat')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
<button
|
||||
className={`primary-button hard-mode-button ${selectedPart === p.part && selectedHardMode ? 'selected-part' : ''} ${!p.hardUnlocked ? 'locked' : ''}`}
|
||||
disabled={difficultyLocked || !p.hardUnlocked}
|
||||
onClick={() => {
|
||||
setSelectedPart(p.part)
|
||||
setSelectedHardMode(true)
|
||||
setCombatContentId(activity.id)
|
||||
setSelectedDifficultyId(selectedDifficulty.id)
|
||||
setScreen('combat')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Hard
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="primary-button selected-part"
|
||||
disabled={difficultyLocked}
|
||||
onClick={() => {
|
||||
setSelectedPart(1)
|
||||
setSelectedMarathonMode(false)
|
||||
setCombatContentId(activity.id)
|
||||
setSelectedDifficultyId(selectedDifficulty.id)
|
||||
setScreen('combat')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Start Hunt
|
||||
</button>
|
||||
<button
|
||||
className={`primary-button ${selectedMarathonMode ? 'selected-part' : ''} ${!marathonUnlocked ? 'locked' : ''}`}
|
||||
disabled={difficultyLocked || !marathonUnlocked}
|
||||
onClick={() => {
|
||||
setSelectedPart(1)
|
||||
setSelectedMarathonMode(true)
|
||||
setCombatContentId(activity.id)
|
||||
setSelectedDifficultyId(selectedDifficulty.id)
|
||||
setScreen('combat')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Marathon
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -341,6 +341,7 @@ export function CombatScreen({
|
||||
difficulty,
|
||||
dungeon,
|
||||
hardMode = false,
|
||||
marathonMode = false,
|
||||
profile,
|
||||
startPart = 1,
|
||||
roguelikeMode,
|
||||
@@ -353,6 +354,7 @@ export function CombatScreen({
|
||||
difficulty: Difficulty
|
||||
dungeon: Dungeon
|
||||
hardMode?: boolean
|
||||
marathonMode?: boolean
|
||||
profile: CharacterProfile
|
||||
startPart?: number
|
||||
roguelikeMode?: RoguelikeMode
|
||||
@@ -416,7 +418,7 @@ export function CombatScreen({
|
||||
const [combatState, setCombatState] = useState<SinglePlayerCombatState>(() => initialCombatState)
|
||||
const [selectedId, setSelectedId] = useState(partyTemplate[0].id)
|
||||
const [encounterIndex, setEncounterIndex] = useState(initialEncounterIndex)
|
||||
const [status, setStatus] = useState<'playing' | 'won' | 'lost' | 'part-complete' | 'upgrade-choice'>('playing')
|
||||
const [status, setStatus] = useState<'playing' | 'won' | 'lost' | 'part-complete' | 'marathon-choice' | 'upgrade-choice'>('playing')
|
||||
const [paused, setPaused] = useState(false)
|
||||
const [speedMultiplier, setSpeedMultiplier] = useState<1 | 2>(1)
|
||||
const [targetGroup, setTargetGroup] = useState<0 | 1 | 2>(0)
|
||||
@@ -430,6 +432,7 @@ export function CombatScreen({
|
||||
const [floatingTexts, setFloatingTexts] = useState<FloatingCombatText[]>([])
|
||||
const [roguelikeUpgrades, setRoguelikeUpgrades] = useState<RoguelikeUpgrade[]>([])
|
||||
const [upgradeChoices, setUpgradeChoices] = useState<RoguelikeUpgrade[]>([])
|
||||
const [marathonBossesDefeated, setMarathonBossesDefeated] = useState(0)
|
||||
const rewardClaimedRef = useRef(false)
|
||||
const profileRefreshedRef = useRef(false)
|
||||
const rolledEncounterIdsRef = useRef(new Set<string>())
|
||||
@@ -439,6 +442,7 @@ export function CombatScreen({
|
||||
const partStartTimesRef = useRef<Record<number, number>>({})
|
||||
const nextLogId = useRef(2)
|
||||
const nextFloatingTextId = useRef(1)
|
||||
const marathonBossesDefeatedRef = useRef(0)
|
||||
const combatRef = useRef(initialCombatState)
|
||||
const selectedIdRef = useRef(partyTemplate[0].id)
|
||||
const runCombatTickRef = useRef<() => void>(() => {})
|
||||
@@ -553,10 +557,10 @@ export function CombatScreen({
|
||||
|
||||
const requestLootRoll = useCallback(
|
||||
(encounterId: number, rollIndex = 0) => {
|
||||
const rollKey = `${encounterId}:${rollIndex}`
|
||||
const rollKey = `${encounterId}:${rollIndex}:${marathonBossesDefeatedRef.current}`
|
||||
if (rolledEncounterIdsRef.current.has(rollKey)) return
|
||||
rolledEncounterIdsRef.current.add(rollKey)
|
||||
const runToken = rollIndex === 0 ? runTokenRef.current : `${runTokenRef.current}-hard-${rollIndex}`
|
||||
const runToken = `${runTokenRef.current}-${marathonBossesDefeatedRef.current}-${rollIndex}`
|
||||
rollEncounterLoot(encounterId, difficulty.id, runToken)
|
||||
.then((result) => {
|
||||
setLootRolls((current) => [...current, result])
|
||||
@@ -609,10 +613,12 @@ export function CombatScreen({
|
||||
setFloatingTexts([])
|
||||
setRoguelikeUpgrades([])
|
||||
setUpgradeChoices([])
|
||||
setMarathonBossesDefeated(0)
|
||||
rewardClaimedRef.current = false
|
||||
profileRefreshedRef.current = false
|
||||
rolledEncounterIdsRef.current = new Set()
|
||||
runTokenRef.current = crypto.randomUUID()
|
||||
marathonBossesDefeatedRef.current = 0
|
||||
resourceSpentRef.current = 0
|
||||
runStartedAtRef.current = Date.now()
|
||||
partStartTimesRef.current = { [startPart]: runStartedAtRef.current }
|
||||
@@ -1201,6 +1207,9 @@ export function CombatScreen({
|
||||
}
|
||||
|
||||
if (isPartBoss && !isFinalBoss) {
|
||||
const nextMarathonKills = marathonBossesDefeatedRef.current + 1
|
||||
marathonBossesDefeatedRef.current = nextMarathonKills
|
||||
setMarathonBossesDefeated(nextMarathonKills)
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
@@ -1209,12 +1218,28 @@ export function CombatScreen({
|
||||
elapsedTicks: nextElapsedTicks,
|
||||
enemyHealth: 0,
|
||||
})
|
||||
setStatus('part-complete')
|
||||
setStatus(marathonMode && encounter.isBoss ? 'marathon-choice' : 'part-complete')
|
||||
addLog(`${encounter.enemyName} is defeated.`, 'loot')
|
||||
return
|
||||
}
|
||||
|
||||
if (encounterIndex === encounters.length - 1) {
|
||||
if (marathonMode && encounter.isBoss) {
|
||||
const nextMarathonKills = marathonBossesDefeatedRef.current + 1
|
||||
marathonBossesDefeatedRef.current = nextMarathonKills
|
||||
setMarathonBossesDefeated(nextMarathonKills)
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: nextResource,
|
||||
cooldowns: nextCooldowns,
|
||||
elapsedTicks: nextElapsedTicks,
|
||||
enemyHealth: 0,
|
||||
})
|
||||
setStatus('marathon-choice')
|
||||
addLog(`${encounter.enemyName} is defeated. Continue marathon or end the hunt.`, 'loot')
|
||||
return
|
||||
}
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
@@ -1267,6 +1292,7 @@ export function CombatScreen({
|
||||
isPartBoss,
|
||||
isFinalBoss,
|
||||
isRoguelike,
|
||||
marathonMode,
|
||||
upgradesEveryEncounter,
|
||||
roguelikeUpgradeCatalog,
|
||||
roguelikeUpgrades,
|
||||
@@ -1620,7 +1646,7 @@ export function CombatScreen({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status !== 'playing' && status !== 'part-complete' && status !== 'upgrade-choice' && (
|
||||
{status !== 'playing' && status !== 'part-complete' && status !== 'marathon-choice' && status !== 'upgrade-choice' && (
|
||||
<div className="result-screen">
|
||||
<div>
|
||||
<p className="eyebrow">{status === 'won' ? `${contentName} Complete` : 'Party Defeated'}</p>
|
||||
@@ -1735,6 +1761,36 @@ export function CombatScreen({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{status === 'marathon-choice' && (
|
||||
<div className="result-screen">
|
||||
<div>
|
||||
<p className="eyebrow">Marathon</p>
|
||||
<h2>{encounter.enemyName} Defeated</h2>
|
||||
<p>
|
||||
{marathonBossesDefeated} boss{marathonBossesDefeated === 1 ? '' : 'es'} defeated.
|
||||
Continue with current health and {gameClass.resourceName}, or end the hunt.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
const current = combatRef.current
|
||||
setCombat({
|
||||
...current,
|
||||
enemyHealth: encounter.maxHealth * enemyCount,
|
||||
elapsedTicks: 0,
|
||||
})
|
||||
setStatus('playing')
|
||||
addLog(`Marathon continues. Another ${encounter.enemyName} appears.`, 'danger')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Continue Marathon
|
||||
</button>
|
||||
<button className="secondary-result-button" onClick={() => finishRun(currentPart, startPart)} type="button">
|
||||
End
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{status === 'part-complete' && (
|
||||
<div className="result-screen">
|
||||
<div>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export type DualScreenCombatState = {
|
||||
partySize: number
|
||||
selectedId: string
|
||||
log: CombatLogEntry[]
|
||||
status: 'playing' | 'won' | 'lost' | 'part-complete' | 'upgrade-choice'
|
||||
status: 'playing' | 'won' | 'lost' | 'part-complete' | 'marathon-choice' | 'upgrade-choice'
|
||||
resource: number
|
||||
maxResource: number
|
||||
resourceName: string
|
||||
|
||||
@@ -70,6 +70,7 @@ type OfflineSave = {
|
||||
activeClassId: number
|
||||
completedDungeonParts: number
|
||||
completedRaidPhases: number
|
||||
dungeonCompletions?: Record<string, number>
|
||||
characters: Record<number, CharacterData>
|
||||
lootRolls: Record<string, LootRoll>
|
||||
}
|
||||
@@ -159,6 +160,9 @@ function upgradeV1Save(v1: { profile: CharacterProfile; lootRolls: Record<string
|
||||
activeClassId: p.character.classId,
|
||||
completedDungeonParts: p.completedDungeonParts,
|
||||
completedRaidPhases: p.completedRaidPhases ?? 0,
|
||||
dungeonCompletions: Object.fromEntries(
|
||||
p.dungeons.map((dungeon) => [String(dungeon.id), dungeon.completionCount ?? 0]),
|
||||
),
|
||||
characters,
|
||||
lootRolls: v1.lootRolls ?? {},
|
||||
}
|
||||
@@ -314,6 +318,10 @@ function buildProfile(save: OfflineSave): CharacterProfile {
|
||||
updateCraftingRecipes(static_)
|
||||
static_.completedDungeonParts = save.completedDungeonParts
|
||||
static_.completedRaidPhases = save.completedRaidPhases
|
||||
static_.dungeons = static_.dungeons.map((dungeon) => ({
|
||||
...dungeon,
|
||||
completionCount: save.dungeonCompletions?.[String(dungeon.id)] ?? dungeon.completionCount ?? 0,
|
||||
}))
|
||||
|
||||
return static_
|
||||
}
|
||||
@@ -491,6 +499,9 @@ function mergeProfileIntoSave(profile: CharacterProfile, existingSave?: OfflineS
|
||||
activeClassId: profile.character.classId,
|
||||
completedDungeonParts: profile.completedDungeonParts,
|
||||
completedRaidPhases: profile.completedRaidPhases,
|
||||
dungeonCompletions: Object.fromEntries(
|
||||
profile.dungeons.map((dungeon) => [String(dungeon.id), dungeon.completionCount ?? 0]),
|
||||
),
|
||||
characters,
|
||||
lootRolls: clone(existingSave?.lootRolls ?? {}),
|
||||
}
|
||||
@@ -958,6 +969,10 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
} else {
|
||||
save.completedDungeonParts = Math.max(save.completedDungeonParts, partCount)
|
||||
}
|
||||
save.dungeonCompletions = {
|
||||
...(save.dungeonCompletions ?? {}),
|
||||
[String(dungeonId)]: (save.dungeonCompletions?.[String(dungeonId)] ?? 0) + 1,
|
||||
}
|
||||
|
||||
let bonusItem: DungeonReward['bonusItem'] = null
|
||||
if ((startPart ?? 1) === 1 && partCount >= 3 && dungeon.completionLoot.length > 0) {
|
||||
|
||||
+3633
-2602
File diff suppressed because it is too large
Load Diff
@@ -168,6 +168,7 @@ export type Dungeon = {
|
||||
difficulties: Difficulty[]
|
||||
encounters: DungeonEncounter[]
|
||||
completionLoot: Array<Omit<Item, 'quantity' | 'equipped'>>
|
||||
completionCount?: number
|
||||
leaderboard: LeaderboardEntry[]
|
||||
leaderboards: {
|
||||
part_1: LeaderboardEntry[]
|
||||
|
||||
Reference in New Issue
Block a user