Android build v1.1.2
This commit is contained in:
+222
-126
@@ -1,13 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { lazy, Suspense, useEffect, useMemo, useState } from 'react'
|
||||
import './App.css'
|
||||
import { CombatScreen } from './components/CombatScreen'
|
||||
import { AuthScreen } from './components/AuthScreen'
|
||||
import { CustomizeScreen } from './components/CustomizeScreen'
|
||||
import { EquipmentScreen } from './components/EquipmentScreen'
|
||||
import { PvPRoguelikeScreen } from './components/PvpRoguelikeScreen'
|
||||
import { PvpStadiumScreen } from './components/PvpStadiumScreen'
|
||||
import { TalentScreen } from './components/TalentScreen'
|
||||
import { SettingsScreen } from './components/SettingsScreen'
|
||||
import {
|
||||
loadCpuPvpLeaderboard,
|
||||
type CpuPvpLeaderboardEntry,
|
||||
@@ -28,6 +21,14 @@ import {
|
||||
} from './gameRepository'
|
||||
import { focusFirstControl } from './input.tsx'
|
||||
|
||||
const CombatScreen = lazy(() => import('./components/CombatScreen').then((module) => ({ default: module.CombatScreen })))
|
||||
const CustomizeScreen = lazy(() => import('./components/CustomizeScreen').then((module) => ({ default: module.CustomizeScreen })))
|
||||
const EquipmentScreen = lazy(() => import('./components/EquipmentScreen').then((module) => ({ default: module.EquipmentScreen })))
|
||||
const PvPRoguelikeScreen = lazy(() => import('./components/PvpRoguelikeScreen').then((module) => ({ default: module.PvPRoguelikeScreen })))
|
||||
const PvpStadiumScreen = lazy(() => import('./components/PvpStadiumScreen').then((module) => ({ default: module.PvpStadiumScreen })))
|
||||
const TalentScreen = lazy(() => import('./components/TalentScreen').then((module) => ({ default: module.TalentScreen })))
|
||||
const SettingsScreen = lazy(() => import('./components/SettingsScreen').then((module) => ({ default: module.SettingsScreen })))
|
||||
|
||||
type Screen =
|
||||
| 'menu'
|
||||
| 'dungeons'
|
||||
@@ -67,6 +68,17 @@ function activityInitials(name: string) {
|
||||
.join('')
|
||||
}
|
||||
|
||||
function ScreenLoading() {
|
||||
return (
|
||||
<main className="game-shell">
|
||||
<section className="message-panel">
|
||||
<p className="eyebrow">Opening Chronicle</p>
|
||||
<h1>Loading...</h1>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
type RoguelikeUpgradeTiming = 'boss' | 'encounter'
|
||||
type RoguelikeVariant = 'pve' | 'pvp'
|
||||
type RoguelikeAbilityLabelMode = 'ability' | 'slot'
|
||||
@@ -148,9 +160,138 @@ function App() {
|
||||
const [cpuLeaderboard, setCpuLeaderboard] = useState<CpuPvpLeaderboardEntry[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setCpuLeaderboard(loadCpuPvpLeaderboard(pvpContentType))
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
setCpuLeaderboard(loadCpuPvpLeaderboard(pvpContentType))
|
||||
})
|
||||
return () => window.cancelAnimationFrame(frame)
|
||||
}, [pvpContentType, screen, roguelikeVariant])
|
||||
|
||||
const profileDungeons = profile?.dungeons
|
||||
const dungeonOptions = useMemo(
|
||||
() => profileDungeons?.filter((candidate) => candidate.contentType === 'dungeon') ?? [],
|
||||
[profileDungeons],
|
||||
)
|
||||
const raidOptions = useMemo(
|
||||
() => profileDungeons?.filter((candidate) => candidate.contentType === 'raid') ?? [],
|
||||
[profileDungeons],
|
||||
)
|
||||
const selectedDungeonOption = useMemo(
|
||||
() => dungeonOptions.find((candidate) => candidate.id === selectedDungeonId) ?? dungeonOptions[0],
|
||||
[dungeonOptions, selectedDungeonId],
|
||||
)
|
||||
const selectedRaidOption = useMemo(
|
||||
() => raidOptions.find((candidate) => candidate.id === selectedRaidId) ?? raidOptions[0],
|
||||
[raidOptions, selectedRaidId],
|
||||
)
|
||||
const activityOptions = useMemo(
|
||||
() => screen === 'raids' ? raidOptions : dungeonOptions,
|
||||
[dungeonOptions, raidOptions, screen],
|
||||
)
|
||||
const combatDungeonOption = useMemo(
|
||||
() => {
|
||||
if (!profileDungeons) return undefined
|
||||
return combatContentId < 0
|
||||
? profileDungeons.find((candidate) => candidate.contentType === roguelikeKind) ?? profileDungeons[0]
|
||||
: profileDungeons.find((candidate) => candidate.id === combatContentId) ?? profileDungeons[0]
|
||||
},
|
||||
[combatContentId, profileDungeons, roguelikeKind],
|
||||
)
|
||||
const combatDifficultyOption = useMemo(
|
||||
() => combatDungeonOption?.difficulties.find(
|
||||
(candidate) => candidate.id === selectedDifficultyId,
|
||||
) ?? combatDungeonOption?.difficulties[0],
|
||||
[combatDungeonOption, selectedDifficultyId],
|
||||
)
|
||||
const roguelikePool = useMemo(
|
||||
() => profileDungeons
|
||||
?.filter((candidate) => candidate.contentType === roguelikeKind)
|
||||
.flatMap((candidate) => candidate.encounters) ?? [],
|
||||
[profileDungeons, roguelikeKind],
|
||||
)
|
||||
const pvpPool = useMemo(
|
||||
() => profileDungeons
|
||||
?.filter((candidate) => candidate.contentType === pvpContentType)
|
||||
.flatMap((candidate) => candidate.encounters) ?? [],
|
||||
[profileDungeons, pvpContentType],
|
||||
)
|
||||
const tierOptions = useMemo(
|
||||
() => activityOptions
|
||||
.flatMap((option) => option.difficulties)
|
||||
.filter((difficulty, index, all) => (
|
||||
all.findIndex((candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel) === index
|
||||
))
|
||||
.sort((a, b) => a.droppedItemLevel - b.droppedItemLevel),
|
||||
[activityOptions],
|
||||
)
|
||||
const savedDifficulty = useMemo(
|
||||
() => profileDungeons
|
||||
?.flatMap((option) => option.difficulties)
|
||||
.find((candidate) => candidate.id === selectedDifficultyId),
|
||||
[profileDungeons, selectedDifficultyId],
|
||||
)
|
||||
const selectedTier = useMemo(
|
||||
() => {
|
||||
const characterLevel = profile?.character.level ?? 0
|
||||
const savedTier = tierOptions.find((candidate) => (
|
||||
candidate.droppedItemLevel === savedDifficulty?.droppedItemLevel
|
||||
&& characterLevel >= candidate.unlockLevel
|
||||
))
|
||||
if (savedTier) return savedTier
|
||||
for (let index = tierOptions.length - 1; index >= 0; index -= 1) {
|
||||
if (characterLevel >= tierOptions[index].unlockLevel) return tierOptions[index]
|
||||
}
|
||||
return tierOptions[0]
|
||||
},
|
||||
[profile?.character.level, savedDifficulty?.droppedItemLevel, tierOptions],
|
||||
)
|
||||
const selectedTierItemLevel = selectedTier?.droppedItemLevel ?? 0
|
||||
const activityPageCount = Math.max(1, Math.ceil(activityOptions.length / ACTIVITY_PAGE_SIZE))
|
||||
const currentActivityPage = Math.min(activityPage, activityPageCount - 1)
|
||||
const pagedActivityOptions = useMemo(
|
||||
() => activityOptions.slice(
|
||||
currentActivityPage * ACTIVITY_PAGE_SIZE,
|
||||
currentActivityPage * ACTIVITY_PAGE_SIZE + ACTIVITY_PAGE_SIZE,
|
||||
),
|
||||
[activityOptions, currentActivityPage],
|
||||
)
|
||||
const selectedActivityOption = useMemo(
|
||||
() => {
|
||||
const fallback = screen === 'raids' && selectedRaidOption ? selectedRaidOption : selectedDungeonOption
|
||||
const selectedActivityId = screen === 'raids' && selectedRaidOption ? selectedRaidOption.id : selectedDungeonOption?.id
|
||||
return activityOptions.find((candidate) => candidate.id === selectedActivityId)
|
||||
?? activityOptions[0]
|
||||
?? fallback
|
||||
},
|
||||
[activityOptions, screen, selectedDungeonOption, selectedRaidOption],
|
||||
)
|
||||
const selectedDifficultyOption = useMemo(
|
||||
() => selectedActivityOption?.difficulties.find(
|
||||
(candidate) => candidate.droppedItemLevel === selectedTierItemLevel,
|
||||
) ?? selectedActivityOption?.difficulties[0],
|
||||
[selectedActivityOption, selectedTierItemLevel],
|
||||
)
|
||||
const lootPreviewEncounters = useMemo(
|
||||
() => [...(selectedActivityOption?.encounters ?? [])]
|
||||
.filter((encounter) => encounter.isBoss)
|
||||
.sort((a, b) => lootSort === 'boss'
|
||||
? a.enemyName.localeCompare(b.enemyName) || a.sequence - b.sequence
|
||||
: a.sequence - b.sequence),
|
||||
[lootSort, selectedActivityOption?.encounters],
|
||||
)
|
||||
const lootPreviewByEncounterId = useMemo(
|
||||
() => new Map(lootPreviewEncounters.map((encounter) => [
|
||||
encounter.id,
|
||||
encounter.lootTables.filter((entry) => entry.difficultyId === selectedDifficultyOption?.id),
|
||||
])),
|
||||
[lootPreviewEncounters, selectedDifficultyOption?.id],
|
||||
)
|
||||
const leaderboardEntries = useMemo(
|
||||
() => selectedActivityOption?.leaderboards[leaderboardCategory].filter(
|
||||
(entry) => entry.difficultyId === selectedDifficultyOption?.id,
|
||||
) ?? [],
|
||||
[leaderboardCategory, selectedActivityOption?.leaderboards, selectedDifficultyOption?.id],
|
||||
)
|
||||
|
||||
function acceptSession(session: AuthSession) {
|
||||
setAccount(session.account)
|
||||
setProfile(session.profile)
|
||||
@@ -224,65 +365,61 @@ function App() {
|
||||
}
|
||||
|
||||
if (screen === 'combat') {
|
||||
const dungeon = combatContentId < 0
|
||||
? profile.dungeons.find((candidate) => candidate.contentType === roguelikeKind) ?? profile.dungeons[0]
|
||||
: profile.dungeons.find((candidate) => candidate.id === combatContentId) ?? profile.dungeons[0]
|
||||
const difficulty = dungeon.difficulties.find(
|
||||
(candidate) => candidate.id === selectedDifficultyId,
|
||||
) ?? dungeon.difficulties[0]
|
||||
const roguelikePool = profile.dungeons
|
||||
.filter((candidate) => candidate.contentType === roguelikeKind)
|
||||
.flatMap((candidate) => candidate.encounters)
|
||||
const dungeon = combatDungeonOption ?? profile.dungeons[0]
|
||||
const difficulty = combatDifficultyOption ?? dungeon.difficulties[0]
|
||||
return (
|
||||
<CombatScreen
|
||||
difficulty={difficulty}
|
||||
dungeon={dungeon}
|
||||
hardMode={false}
|
||||
marathonMode={selectedMarathonMode && combatContentId > 0}
|
||||
profile={profile}
|
||||
roguelikeMode={combatContentId < 0 ? roguelikeKind : undefined}
|
||||
roguelikeUpgradeTiming={combatContentId < 0 ? roguelikeUpgradeTiming : undefined}
|
||||
roguelikeAbilityLabelMode={combatContentId < 0 ? roguelikeAbilityLabelMode : undefined}
|
||||
roguelikeEncounterPool={combatContentId < 0 ? roguelikePool : undefined}
|
||||
startPart={1}
|
||||
onExit={() => {
|
||||
setScreen(combatContentId < 0 ? 'roguelike' : dungeon.contentType === 'raid' ? 'raids' : 'dungeons')
|
||||
}}
|
||||
onProfileUpdated={setProfile}
|
||||
/>
|
||||
<Suspense fallback={<ScreenLoading />}>
|
||||
<CombatScreen
|
||||
difficulty={difficulty}
|
||||
dungeon={dungeon}
|
||||
hardMode={false}
|
||||
marathonMode={selectedMarathonMode && combatContentId > 0}
|
||||
profile={profile}
|
||||
roguelikeMode={combatContentId < 0 ? roguelikeKind : undefined}
|
||||
roguelikeUpgradeTiming={combatContentId < 0 ? roguelikeUpgradeTiming : undefined}
|
||||
roguelikeAbilityLabelMode={combatContentId < 0 ? roguelikeAbilityLabelMode : undefined}
|
||||
roguelikeEncounterPool={combatContentId < 0 ? roguelikePool : undefined}
|
||||
startPart={1}
|
||||
onExit={() => {
|
||||
setScreen(combatContentId < 0 ? 'roguelike' : dungeon.contentType === 'raid' ? 'raids' : 'dungeons')
|
||||
}}
|
||||
onProfileUpdated={setProfile}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
if (screen === 'pvp') {
|
||||
if (pvpContentType === 'stadium') {
|
||||
return (
|
||||
<PvpStadiumScreen
|
||||
<Suspense fallback={<ScreenLoading />}>
|
||||
<PvpStadiumScreen
|
||||
gameMode={gameMode}
|
||||
onExit={() => {
|
||||
setRoguelikeVariant('pvp')
|
||||
setScreen('roguelike')
|
||||
}}
|
||||
onProfileUpdated={setProfile}
|
||||
profile={profile}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Suspense fallback={<ScreenLoading />}>
|
||||
<PvPRoguelikeScreen
|
||||
contentType={pvpContentType}
|
||||
encounterPool={pvpPool}
|
||||
gameMode={gameMode}
|
||||
onExit={() => {
|
||||
setCpuLeaderboard(loadCpuPvpLeaderboard(pvpContentType))
|
||||
setRoguelikeVariant('pvp')
|
||||
setScreen('roguelike')
|
||||
}}
|
||||
onProfileUpdated={setProfile}
|
||||
profile={profile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const pvpPool = profile.dungeons
|
||||
.filter((candidate) => candidate.contentType === pvpContentType)
|
||||
.flatMap((candidate) => candidate.encounters)
|
||||
return (
|
||||
<PvPRoguelikeScreen
|
||||
contentType={pvpContentType}
|
||||
encounterPool={pvpPool}
|
||||
gameMode={gameMode}
|
||||
onExit={() => {
|
||||
setCpuLeaderboard(loadCpuPvpLeaderboard(pvpContentType))
|
||||
setRoguelikeVariant('pvp')
|
||||
setScreen('roguelike')
|
||||
}}
|
||||
onProfileUpdated={setProfile}
|
||||
profile={profile}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -293,13 +430,7 @@ function App() {
|
||||
const experiencePercent = profile.character.level >= profile.maxLevel
|
||||
? 100
|
||||
: Math.min(100, (experienceIntoLevel / experienceForLevel) * 100)
|
||||
const dungeonOptions = profile.dungeons.filter((candidate) => candidate.contentType === 'dungeon')
|
||||
const raidOptions = profile.dungeons.filter((candidate) => candidate.contentType === 'raid')
|
||||
const dungeon = dungeonOptions.find((candidate) => candidate.id === selectedDungeonId)
|
||||
?? dungeonOptions[0]!
|
||||
const raid = raidOptions.find((candidate) => candidate.id === selectedRaidId)
|
||||
?? raidOptions[0]
|
||||
const activityOptions = screen === 'raids' ? raidOptions : dungeonOptions
|
||||
const dungeon = selectedDungeonOption ?? profile.dungeons[0]!
|
||||
const startPveRoguelike = () => {
|
||||
const baseDungeon = dungeonOptions[0]
|
||||
const baseRaid = raidOptions[0]
|
||||
@@ -313,47 +444,15 @@ function App() {
|
||||
setSelectedMarathonMode(false)
|
||||
setScreen('combat')
|
||||
}
|
||||
const tierOptions = activityOptions
|
||||
.flatMap((option) => option.difficulties)
|
||||
.filter((difficulty, index, all) => (
|
||||
all.findIndex((candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel) === index
|
||||
))
|
||||
.sort((a, b) => a.droppedItemLevel - b.droppedItemLevel)
|
||||
const savedDifficulty = profile.dungeons
|
||||
.flatMap((option) => option.difficulties)
|
||||
.find((candidate) => candidate.id === selectedDifficultyId)
|
||||
const selectedTier = tierOptions.find((candidate) => (
|
||||
candidate.droppedItemLevel === savedDifficulty?.droppedItemLevel
|
||||
&& profile.character.level >= candidate.unlockLevel
|
||||
))
|
||||
?? tierOptions.slice().reverse().find((candidate) => profile.character.level >= candidate.unlockLevel)
|
||||
?? tierOptions[0]
|
||||
const selectedTierItemLevel = selectedTier?.droppedItemLevel ?? 0
|
||||
const activityPageCount = Math.max(1, Math.ceil(activityOptions.length / ACTIVITY_PAGE_SIZE))
|
||||
const currentActivityPage = Math.min(activityPage, activityPageCount - 1)
|
||||
const pagedActivityOptions = activityOptions.slice(
|
||||
currentActivityPage * ACTIVITY_PAGE_SIZE,
|
||||
currentActivityPage * ACTIVITY_PAGE_SIZE + ACTIVITY_PAGE_SIZE,
|
||||
)
|
||||
const activityPageStart = activityOptions.length === 0
|
||||
? 0
|
||||
: currentActivityPage * ACTIVITY_PAGE_SIZE + 1
|
||||
const activityPageEnd = Math.min(activityOptions.length, (currentActivityPage + 1) * ACTIVITY_PAGE_SIZE)
|
||||
const selectedActivityId = screen === 'raids' && raid ? raid.id : dungeon.id
|
||||
const activity = activityOptions.find((candidate) => candidate.id === selectedActivityId)
|
||||
?? activityOptions[0]
|
||||
?? (screen === 'raids' && raid ? raid : dungeon)
|
||||
const selectedDifficulty = activity.difficulties.find(
|
||||
(candidate) => candidate.droppedItemLevel === selectedTierItemLevel,
|
||||
) ?? activity.difficulties[0]
|
||||
const activity = selectedActivityOption ?? dungeon
|
||||
const selectedDifficulty = selectedDifficultyOption ?? activity.difficulties[0]
|
||||
const difficultyLocked = profile.character.level < selectedDifficulty.unlockLevel
|
||||
const cloudSync = getCloudSyncStatus()
|
||||
const canShowCloudSync = account.id !== -1 && cloudSync.available
|
||||
const lootPreviewEncounters = [...activity.encounters]
|
||||
.filter((encounter) => encounter.isBoss)
|
||||
.sort((a, b) => lootSort === 'boss'
|
||||
? a.enemyName.localeCompare(b.enemyName) || a.sequence - b.sequence
|
||||
: a.sequence - b.sequence)
|
||||
|
||||
return (
|
||||
<main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''} ${screen === 'customize' ? 'workshop-shell' : ''}`}>
|
||||
@@ -851,9 +950,7 @@ function App() {
|
||||
</p>
|
||||
<div className="loot-preview-grid">
|
||||
{lootPreviewEncounters.map((encounter) => {
|
||||
const loot = encounter.lootTables.filter(
|
||||
(entry) => entry.difficultyId === selectedDifficulty.id,
|
||||
)
|
||||
const loot = lootPreviewByEncounterId.get(encounter.id) ?? []
|
||||
return (
|
||||
<article key={encounter.id}>
|
||||
<div className="loot-encounter-title">
|
||||
@@ -937,8 +1034,7 @@ function App() {
|
||||
<span>Resource</span>
|
||||
<span>Time</span>
|
||||
</div>
|
||||
{activity.leaderboards[leaderboardCategory]
|
||||
.filter((entry) => entry.difficultyId === selectedDifficulty.id)
|
||||
{leaderboardEntries
|
||||
.map((entry) => (
|
||||
<div className="leaderboard-row" key={`${entry.rank}-${entry.completedAt}`}>
|
||||
<strong>#{entry.rank}</strong>
|
||||
@@ -950,9 +1046,7 @@ function App() {
|
||||
<span>{entry.durationSeconds}s</span>
|
||||
</div>
|
||||
))}
|
||||
{activity.leaderboards[leaderboardCategory].filter(
|
||||
(entry) => entry.difficultyId === selectedDifficulty.id,
|
||||
).length === 0 && (
|
||||
{leaderboardEntries.length === 0 && (
|
||||
<div className="leaderboard-empty">
|
||||
{gameMode === 'offline'
|
||||
? 'Connect with an online character to compete in rankings.'
|
||||
@@ -971,33 +1065,35 @@ function App() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{screen === 'customize' && (
|
||||
<CustomizeScreen
|
||||
profile={profile}
|
||||
onBack={() => setScreen('menu')}
|
||||
onSaved={setProfile}
|
||||
/>
|
||||
)}
|
||||
<Suspense fallback={<ScreenLoading />}>
|
||||
{screen === 'customize' && (
|
||||
<CustomizeScreen
|
||||
profile={profile}
|
||||
onBack={() => setScreen('menu')}
|
||||
onSaved={setProfile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === 'talents' && (
|
||||
<TalentScreen
|
||||
profile={profile}
|
||||
onBack={() => setScreen('menu')}
|
||||
onUpdated={setProfile}
|
||||
/>
|
||||
)}
|
||||
{screen === 'talents' && (
|
||||
<TalentScreen
|
||||
profile={profile}
|
||||
onBack={() => setScreen('menu')}
|
||||
onUpdated={setProfile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === 'equipment' && (
|
||||
<EquipmentScreen
|
||||
profile={profile}
|
||||
onBack={() => setScreen('menu')}
|
||||
onUpdated={setProfile}
|
||||
/>
|
||||
)}
|
||||
{screen === 'equipment' && (
|
||||
<EquipmentScreen
|
||||
profile={profile}
|
||||
onBack={() => setScreen('menu')}
|
||||
onUpdated={setProfile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === 'settings' && (
|
||||
<SettingsScreen onBack={() => setScreen('menu')} />
|
||||
)}
|
||||
{screen === 'settings' && (
|
||||
<SettingsScreen onBack={() => setScreen('menu')} />
|
||||
)}
|
||||
</Suspense>
|
||||
|
||||
</main>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user