Android build v1.1.14
This commit is contained in:
+108
-14
@@ -86,6 +86,7 @@ type CharacterData = {
|
||||
|
||||
type OfflineSave = {
|
||||
version: 4
|
||||
updatedAt: number
|
||||
characterName: string
|
||||
activeClassId: number
|
||||
completedDungeonParts: number
|
||||
@@ -117,6 +118,22 @@ export type CloudSyncStatus = {
|
||||
dirty: boolean
|
||||
}
|
||||
|
||||
export type CloudSaveAge = 'server-newer' | 'local-newer' | 'same' | 'unknown'
|
||||
|
||||
export type CloudSaveSummary = {
|
||||
updatedAt: number | null
|
||||
characterName: string
|
||||
activeClassId: number
|
||||
}
|
||||
|
||||
export type CloudSyncComparison = {
|
||||
relation: CloudSaveAge
|
||||
local: CloudSaveSummary
|
||||
server: CloudSaveSummary
|
||||
}
|
||||
|
||||
export type CloudSyncChoice = 'local' | 'server'
|
||||
|
||||
type RepositoryMode = 'online' | 'offline-local' | 'offline-cached'
|
||||
|
||||
type NetworkError = Error & {
|
||||
@@ -144,6 +161,30 @@ function clone<T>(value: T): T {
|
||||
return structuredClone(value)
|
||||
}
|
||||
|
||||
function normalizedSaveTimestamp(value: unknown): number {
|
||||
const timestamp = Number(value)
|
||||
return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : 0
|
||||
}
|
||||
|
||||
function stampSave(save: OfflineSave, updatedAt = Date.now()): OfflineSave {
|
||||
save.updatedAt = updatedAt
|
||||
return save
|
||||
}
|
||||
|
||||
function saveSummary(save: OfflineSave): CloudSaveSummary {
|
||||
return {
|
||||
updatedAt: save.updatedAt > 0 ? save.updatedAt : null,
|
||||
characterName: save.characterName,
|
||||
activeClassId: save.activeClassId,
|
||||
}
|
||||
}
|
||||
|
||||
function compareSaveAge(local: OfflineSave, server: OfflineSave): CloudSaveAge {
|
||||
if (local.updatedAt <= 0 || server.updatedAt <= 0) return 'unknown'
|
||||
if (Math.abs(local.updatedAt - server.updatedAt) < 1000) return 'same'
|
||||
return server.updatedAt > local.updatedAt ? 'server-newer' : 'local-newer'
|
||||
}
|
||||
|
||||
function toGameMode(mode: RepositoryMode): GameMode {
|
||||
return mode === 'online' ? 'online' : 'offline'
|
||||
}
|
||||
@@ -190,6 +231,7 @@ function upgradeV1Save(v1: { profile: CharacterProfile; lootRolls: Record<string
|
||||
}
|
||||
return {
|
||||
version: 4,
|
||||
updatedAt: 0,
|
||||
characterName: p.character.name,
|
||||
activeClassId: p.character.classId,
|
||||
completedDungeonParts: p.completedDungeonParts,
|
||||
@@ -210,6 +252,7 @@ function upgradeV2Save(v2: Omit<OfflineSave, 'version' | 'completedRaidPhases' |
|
||||
return normalizeSaveAbilitySlots({
|
||||
...v2,
|
||||
version: 4,
|
||||
updatedAt: normalizedSaveTimestamp((v2 as { updatedAt?: unknown }).updatedAt),
|
||||
completedRaidPhases: 0,
|
||||
bossKills: {},
|
||||
bossPets: {},
|
||||
@@ -222,6 +265,7 @@ function upgradeV3Save(v3: Omit<OfflineSave, 'version' | 'bossKills' | 'bossPets
|
||||
return normalizeSaveAbilitySlots({
|
||||
...v3,
|
||||
version: 4,
|
||||
updatedAt: normalizedSaveTimestamp((v3 as { updatedAt?: unknown }).updatedAt),
|
||||
bossKills: {},
|
||||
bossPets: {},
|
||||
pvpMatchesPlayed: 0,
|
||||
@@ -244,6 +288,7 @@ function normalizeAbilitySlots(abilitySlots: unknown): Array<number | null> {
|
||||
}
|
||||
|
||||
function normalizeSaveAbilitySlots(save: OfflineSave): OfflineSave {
|
||||
save.updatedAt = normalizedSaveTimestamp(save.updatedAt)
|
||||
for (const character of Object.values(save.characters)) {
|
||||
character.abilitySlots = normalizeAbilitySlots(character.abilitySlots)
|
||||
}
|
||||
@@ -590,6 +635,7 @@ function mergeProfileIntoSave(profile: CharacterProfile, existingSave?: OfflineS
|
||||
}
|
||||
return {
|
||||
version: 4,
|
||||
updatedAt: existingSave?.updatedAt ?? Date.now(),
|
||||
characterName: profile.character.name,
|
||||
activeClassId: profile.character.classId,
|
||||
completedDungeonParts: profile.completedDungeonParts,
|
||||
@@ -1008,6 +1054,10 @@ function requireStoredSave(store: LocalSaveStore): OfflineSave {
|
||||
return save
|
||||
}
|
||||
|
||||
function writeStoreSave(store: LocalSaveStore, save: OfflineSave) {
|
||||
store.writeSave(stampSave(save))
|
||||
}
|
||||
|
||||
function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
return {
|
||||
async loadSession() {
|
||||
@@ -1055,7 +1105,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
}
|
||||
save.characters[classId].abilitySlots = slots
|
||||
save.activeClassId = classId
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async completeDungeon(dungeonId, difficultyId, resourceSpent, durationSeconds, completedPart, startPart, partDurationSeconds, hardMode) {
|
||||
@@ -1161,7 +1211,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
const updatedProfile = buildProfile(save)
|
||||
|
||||
return {
|
||||
@@ -1284,7 +1334,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
)
|
||||
cd.inventory = profile.inventory
|
||||
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
const updatedProfile = buildProfile(save)
|
||||
|
||||
return {
|
||||
@@ -1339,7 +1389,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
}
|
||||
cd.talentRanks[String(talentId)] = 1
|
||||
}
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
}
|
||||
if (cd.talentPoints <= 0) {
|
||||
@@ -1367,7 +1417,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
}
|
||||
cd.talentRanks[String(talentId)] = (cd.talentRanks[String(talentId)] ?? 0) + 1
|
||||
cd.talentPoints -= 1
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async resetTalents() {
|
||||
@@ -1390,7 +1440,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
cd.talentPoints + refunded,
|
||||
)
|
||||
}
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async equipItem(itemId) {
|
||||
@@ -1402,7 +1452,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
if (candidate.slot === item.slot) candidate.equipped = candidate.id === item.id
|
||||
}
|
||||
save.characters[save.activeClassId].inventory = profile.inventory
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async discardExtraItem(itemId) {
|
||||
@@ -1413,7 +1463,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
if (item.quantity <= 1) throw new Error('Only extra copies can be discarded.')
|
||||
item.quantity -= 1
|
||||
save.characters[save.activeClassId].inventory = profile.inventory
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async breakdownItem(itemId) {
|
||||
@@ -1456,7 +1506,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
}
|
||||
|
||||
save.characters[save.activeClassId].inventory = profile.inventory
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async craftItem(recipeId) {
|
||||
@@ -1484,7 +1534,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
|
||||
addInventoryItem(profile.inventory, recipe.item, 1)
|
||||
save.characters[save.activeClassId].inventory = profile.inventory
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async upgradeItem(itemId) {
|
||||
@@ -1527,7 +1577,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
if (upgraded) upgraded.equipped = true
|
||||
}
|
||||
save.characters[save.activeClassId].inventory = profile.inventory
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
async rollEncounterLoot(encounterId, difficultyId, runToken) {
|
||||
@@ -1610,7 +1660,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
}
|
||||
save.lootRolls[rollKey] = result
|
||||
save.characters[save.activeClassId].inventory = profile.inventory
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return clone(result)
|
||||
},
|
||||
async recordBossKill(encounterId, options) {
|
||||
@@ -1623,7 +1673,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
const petAwarded = recordBossKillInSave(save, encounter, {
|
||||
petVariant: options?.petVariant ?? 'normal',
|
||||
})
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return {
|
||||
profile: buildProfile(save),
|
||||
petAwarded,
|
||||
@@ -1633,7 +1683,7 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
|
||||
const save = requireStoredSave(store)
|
||||
save.pvpMatchesPlayed = (save.pvpMatchesPlayed ?? 0) + 1
|
||||
if (won) save.pvpMatchesWon = (save.pvpMatchesWon ?? 0) + 1
|
||||
store.writeSave(save)
|
||||
writeStoreSave(store, save)
|
||||
return buildProfile(save)
|
||||
},
|
||||
}
|
||||
@@ -1750,6 +1800,49 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
|
||||
return buildProfile(synced.save)
|
||||
}
|
||||
|
||||
export async function previewCloudSaveSync(): Promise<CloudSyncComparison> {
|
||||
const cache = readOnlineCache()
|
||||
if (!cache) {
|
||||
throw new Error('No signed-in save is available for cloud sync.')
|
||||
}
|
||||
await refreshCatalogFromServer()
|
||||
const serverSave = await loadServerSyncSave()
|
||||
return {
|
||||
relation: compareSaveAge(cache.save, serverSave),
|
||||
local: saveSummary(cache.save),
|
||||
server: saveSummary(serverSave),
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyCloudSaveSync(choice: CloudSyncChoice): Promise<CharacterProfile> {
|
||||
const cache = readOnlineCache()
|
||||
if (!cache) {
|
||||
throw new Error('No signed-in save is available for cloud sync.')
|
||||
}
|
||||
await refreshCatalogFromServer()
|
||||
if (choice === 'server') {
|
||||
const serverSave = await loadServerSyncSave()
|
||||
writeOnlineCache({
|
||||
version: 1,
|
||||
account: cache.account,
|
||||
save: serverSave,
|
||||
dirty: false,
|
||||
})
|
||||
writeMode('online')
|
||||
return buildProfile(serverSave)
|
||||
}
|
||||
const synced = await pushServerSyncSave(cache.save)
|
||||
await refreshCatalogFromServer()
|
||||
writeOnlineCache({
|
||||
version: 1,
|
||||
account: cache.account,
|
||||
save: synced.save,
|
||||
dirty: false,
|
||||
})
|
||||
writeMode('online')
|
||||
return buildProfile(synced.save)
|
||||
}
|
||||
|
||||
export function selectOnlineMode() {
|
||||
writeMode('online')
|
||||
}
|
||||
@@ -1765,6 +1858,7 @@ export function createOfflineCharacter(characterName: string): AuthSession {
|
||||
}
|
||||
const save: OfflineSave = {
|
||||
version: 4,
|
||||
updatedAt: Date.now(),
|
||||
characterName: name,
|
||||
activeClassId: 1,
|
||||
completedDungeonParts: 0,
|
||||
|
||||
Reference in New Issue
Block a user