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 dropsFound: Record } 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 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>, ): 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 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 ?? {}, }, } }