221 lines
6.5 KiB
TypeScript
221 lines
6.5 KiB
TypeScript
import { asIwt2HealerId, type Iwt2HealerId } from '../content/healerAbilities'
|
|
|
|
export type Iwt2InventoryItem = {
|
|
id: string
|
|
name: string
|
|
quantity: number
|
|
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'
|
|
}
|
|
|
|
export type Iwt2ArmorPaletteId = 'guild_green' | 'sun_gold' | 'ember_red' | 'moon_blue'
|
|
|
|
export type Iwt2CollectionLog = {
|
|
bossKills: Record<string, number>
|
|
dropsFound: Record<string, number>
|
|
}
|
|
|
|
export type Iwt2Character = {
|
|
name: string
|
|
level: number
|
|
experience: number
|
|
healerStyle: Iwt2HealerId
|
|
armorPalette: Iwt2ArmorPaletteId
|
|
}
|
|
|
|
export type Iwt2CloudSlot = {
|
|
savedAt: number
|
|
character: Iwt2Character
|
|
inventory: Iwt2InventoryItem[]
|
|
collectionLog: Iwt2CollectionLog
|
|
}
|
|
|
|
export type Iwt2Save = {
|
|
version: 1
|
|
updatedAt: number
|
|
character: Iwt2Character
|
|
inventory: Iwt2InventoryItem[]
|
|
collectionLog: Iwt2CollectionLog
|
|
cloudSlot?: Iwt2CloudSlot
|
|
}
|
|
|
|
const IWT2_SAVE_KEY = 'i-want-to-heal-2:save:v1'
|
|
|
|
export function createDefaultIwt2Save(): Iwt2Save {
|
|
return {
|
|
version: 1,
|
|
updatedAt: Date.now(),
|
|
character: {
|
|
name: 'Healer',
|
|
level: 1,
|
|
experience: 0,
|
|
healerStyle: 'dawnweaver',
|
|
armorPalette: 'guild_green',
|
|
},
|
|
inventory: [],
|
|
collectionLog: {
|
|
bossKills: {},
|
|
dropsFound: {},
|
|
},
|
|
}
|
|
}
|
|
|
|
function normalizeSave(value: unknown): Iwt2Save {
|
|
if (!value || typeof value !== 'object') return createDefaultIwt2Save()
|
|
const candidate = value as Partial<Iwt2Save>
|
|
if (candidate.version !== 1) return createDefaultIwt2Save()
|
|
return {
|
|
version: 1,
|
|
updatedAt: typeof candidate.updatedAt === 'number' ? candidate.updatedAt : Date.now(),
|
|
character: {
|
|
name: candidate.character?.name || 'Healer',
|
|
level: Math.max(1, Math.floor(candidate.character?.level ?? 1)),
|
|
experience: Math.max(0, Math.floor(candidate.character?.experience ?? 0)),
|
|
healerStyle: asIwt2HealerId(candidate.character?.healerStyle),
|
|
armorPalette: asIwt2ArmorPaletteId(candidate.character?.armorPalette),
|
|
},
|
|
inventory: Array.isArray(candidate.inventory) ? candidate.inventory : [],
|
|
collectionLog: {
|
|
bossKills: candidate.collectionLog?.bossKills ?? {},
|
|
dropsFound: candidate.collectionLog?.dropsFound ?? {},
|
|
},
|
|
cloudSlot: normalizeCloudSlot(candidate.cloudSlot),
|
|
}
|
|
}
|
|
|
|
export function loadIwt2Save(): Iwt2Save {
|
|
try {
|
|
return normalizeSave(JSON.parse(window.localStorage.getItem(IWT2_SAVE_KEY) ?? 'null'))
|
|
} catch {
|
|
return createDefaultIwt2Save()
|
|
}
|
|
}
|
|
|
|
export function writeIwt2Save(save: Iwt2Save) {
|
|
window.localStorage.setItem(IWT2_SAVE_KEY, JSON.stringify({
|
|
...save,
|
|
updatedAt: Date.now(),
|
|
}))
|
|
}
|
|
|
|
export function recordIwt2BossKill(save: Iwt2Save, bossId: string): Iwt2Save {
|
|
const drop = bossDropFor(bossId)
|
|
return {
|
|
...save,
|
|
updatedAt: Date.now(),
|
|
character: {
|
|
...save.character,
|
|
experience: save.character.experience + 125,
|
|
level: Math.max(save.character.level, 1 + Math.floor((save.character.experience + 125) / 500)),
|
|
},
|
|
inventory: [
|
|
...save.inventory,
|
|
{
|
|
id: `${drop.id}-${Date.now()}`,
|
|
name: drop.name,
|
|
quantity: 1,
|
|
rarity: 'common',
|
|
},
|
|
],
|
|
collectionLog: {
|
|
...save.collectionLog,
|
|
bossKills: {
|
|
...save.collectionLog.bossKills,
|
|
[bossId]: (save.collectionLog.bossKills[bossId] ?? 0) + 1,
|
|
},
|
|
dropsFound: {
|
|
...save.collectionLog.dropsFound,
|
|
[drop.id]: (save.collectionLog.dropsFound[drop.id] ?? 0) + 1,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
export function updateIwt2CharacterSettings(
|
|
save: Iwt2Save,
|
|
settings: Partial<Pick<Iwt2Save['character'], 'name' | 'healerStyle' | 'armorPalette'>>,
|
|
): Iwt2Save {
|
|
return {
|
|
...save,
|
|
updatedAt: Date.now(),
|
|
character: {
|
|
...save.character,
|
|
...settings,
|
|
name: normalizeCharacterName(settings.name ?? save.character.name),
|
|
healerStyle: asIwt2HealerId(settings.healerStyle ?? save.character.healerStyle),
|
|
armorPalette: asIwt2ArmorPaletteId(settings.armorPalette ?? save.character.armorPalette),
|
|
},
|
|
}
|
|
}
|
|
|
|
export function snapshotIwt2CloudSlot(save: Iwt2Save): Iwt2Save {
|
|
return {
|
|
...save,
|
|
updatedAt: Date.now(),
|
|
cloudSlot: {
|
|
savedAt: Date.now(),
|
|
character: { ...save.character },
|
|
inventory: save.inventory.map((item) => ({ ...item })),
|
|
collectionLog: {
|
|
bossKills: { ...save.collectionLog.bossKills },
|
|
dropsFound: { ...save.collectionLog.dropsFound },
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
export function restoreIwt2CloudSlot(save: Iwt2Save): Iwt2Save {
|
|
if (!save.cloudSlot) return save
|
|
return {
|
|
...save,
|
|
updatedAt: Date.now(),
|
|
character: { ...save.cloudSlot.character },
|
|
inventory: save.cloudSlot.inventory.map((item) => ({ ...item })),
|
|
collectionLog: {
|
|
bossKills: { ...save.cloudSlot.collectionLog.bossKills },
|
|
dropsFound: { ...save.cloudSlot.collectionLog.dropsFound },
|
|
},
|
|
}
|
|
}
|
|
|
|
function bossDropFor(bossId: string): { id: string, name: string } {
|
|
if (bossId === 'yian-kut-ku') return { id: 'yian-kut-ku-scale', name: 'Yian Kut Ku Scale' }
|
|
if (bossId === 'great-jaggi') return { id: 'great-jaggi-hide', name: 'Great Jaggi Hide' }
|
|
if (bossId === 'khezu') return { id: 'khezu-pearl', name: 'Khezu Pearl' }
|
|
return { id: 'raw-bulldrome-coin', name: 'Raw Bulldrome Coin' }
|
|
}
|
|
|
|
function asIwt2ArmorPaletteId(value: unknown): Iwt2ArmorPaletteId {
|
|
return value === 'sun_gold'
|
|
|| value === 'ember_red'
|
|
|| value === 'moon_blue'
|
|
|| value === 'guild_green'
|
|
? value
|
|
: 'guild_green'
|
|
}
|
|
|
|
function normalizeCharacterName(name: string): string {
|
|
const trimmed = name.trim().slice(0, 18)
|
|
return trimmed || 'Healer'
|
|
}
|
|
|
|
function normalizeCloudSlot(value: unknown): Iwt2CloudSlot | undefined {
|
|
if (!value || typeof value !== 'object') return undefined
|
|
const candidate = value as Partial<Iwt2CloudSlot>
|
|
if (!candidate.character || !candidate.collectionLog) return undefined
|
|
return {
|
|
savedAt: typeof candidate.savedAt === 'number' ? candidate.savedAt : Date.now(),
|
|
character: {
|
|
name: normalizeCharacterName(candidate.character.name ?? 'Healer'),
|
|
level: Math.max(1, Math.floor(candidate.character.level ?? 1)),
|
|
experience: Math.max(0, Math.floor(candidate.character.experience ?? 0)),
|
|
healerStyle: asIwt2HealerId(candidate.character.healerStyle),
|
|
armorPalette: asIwt2ArmorPaletteId(candidate.character.armorPalette),
|
|
},
|
|
inventory: Array.isArray(candidate.inventory) ? candidate.inventory : [],
|
|
collectionLog: {
|
|
bossKills: candidate.collectionLog.bossKills ?? {},
|
|
dropsFound: candidate.collectionLog.dropsFound ?? {},
|
|
},
|
|
}
|
|
}
|