import { Paths } from "@paths"; import { Character, CharacterStats, CharName, CharacterClass, ClassKey, Nation, UserKey, AccountData, AccountMap, CLASSES, } from "@types"; import { Store } from "@systems/store"; import { Runtime } from "@systems/runtime"; // ─── Runtime ────────────────────────────────────────────────────────────────── Runtime.phase("load", () => Char.load(), { name: "Char.load" }); let _chars: CharacterMap = {}; let _accounts: AccountMap = {}; /** Raw shape stored in characters.json */ interface SerializableCharacter { name: CharName; class: ClassKey; // "FB" — serialized as string level: number; nation: Nation; active?: boolean; sharedWith?: UserKey[]; stats?: CharacterStats; } interface CharacterMap { [userKey: string]: { characters: SerializableCharacter[]; }; } export function loadCharacters(): void { _chars = Store.readOrDefault(Paths.data("characters.json"), {}); _accounts = Store.readOrDefault(Paths.data("accounts.json"), {}); } function saveCharacters(): void { // Dehydrate all characters before saving const raw: CharacterMap = {}; for (const [userKey, data] of Object.entries(_chars)) { raw[userKey] = { characters: data.characters.map((c) => "ownerKey" in c ? Char.dehydrate(c as unknown as Character) : c ), }; } Store.write(Paths.data("characters.json"), raw); } function saveAccounts(): void { Store.write(Paths.data("accounts.json"), _accounts); } // ─── Helpers ────────────────────────────────────────────────────────────────── /** Get hydrated characters for a user */ export function getCharacters(userKey: UserKey): Character[] { return (_chars[userKey]?.characters ?? []).map((c) => Char.hydrate(c as SerializableCharacter, userKey) ); } export function getActiveCharacter(userKey: UserKey): Character | null { return getCharacters(userKey).find((c) => c.active) ?? null; } export function getCharacterByName(userKey: UserKey, name: string): Character | null { return getCharacters(userKey).find( (c) => c.name.toLowerCase() === name.toLowerCase() ) ?? null; } export function getCharacterByClass(userKey: UserKey, cls: ClassKey): Character | null { const chars = getCharacters(userKey).filter((c) => c.class.key === cls); return chars.find((c) => c.active) ?? chars[0] ?? null; } export function isCharacterOwner(userKey: UserKey | null, charName: string): boolean { if (!userKey) return false; return getCharacters(userKey).some((c) => c.name === charName); } // ─── Mutations ──────────────────────────────────────────────────────────────── export function addCharacter(userKey: UserKey, char: Omit): boolean { if (!_chars[userKey]) _chars[userKey] = { characters: [] }; const exists = _chars[userKey].characters.some( (c) => c.name.toLowerCase() === char.name.toLowerCase() ); if (exists) return false; const hasActive = _chars[userKey].characters.some((c) => c.active); _chars[userKey].characters.push({ ...char, active: !hasActive }); saveCharacters(); return true; } export function removeCharacter(userKey: UserKey, name: string): boolean { if (!_chars[userKey]) return false; const before = _chars[userKey].characters.length; _chars[userKey].characters = _chars[userKey].characters.filter( (c) => c.name.toLowerCase() !== name.toLowerCase() ); if (_chars[userKey].characters.length === before) return false; if (!_chars[userKey].characters.some((c) => c.active) && _chars[userKey].characters.length > 0) { _chars[userKey].characters[0].active = true; } saveCharacters(); return true; } export function setActiveCharacter(userKey: UserKey, name: string): boolean { const chars = _chars[userKey]?.characters; if (!chars) return false; const target = chars.find((c) => c.name.toLowerCase() === name.toLowerCase()); if (!target) return false; chars.forEach((c) => (c.active = false)); target.active = true; saveCharacters(); return true; } export function setCharacterNation(userKey: UserKey, name: string, nation: Nation): boolean { const raw = _chars[userKey]?.characters.find( (c) => c.name.toLowerCase() === name.toLowerCase() ); if (!raw) return false; raw.nation = nation; saveCharacters(); return true; } export function setCharacterStats( userKey: UserKey, name: string, stats: { atk?: number; def?: number; heal?: number } ): boolean { const raw = _chars[userKey]?.characters.find( (c) => c.name.toLowerCase() === name.toLowerCase() ); if (!raw) return false; if (!raw.stats) raw.stats = {}; Object.assign(raw.stats, stats); saveCharacters(); return true; } export function shareCharacter(ownerKey: UserKey, charName: string, targetKey: UserKey): boolean { const raw = _chars[ownerKey]?.characters.find( (c) => c.name.toLowerCase() === charName.toLowerCase() ); if (!raw) return false; if (!raw.sharedWith) raw.sharedWith = []; if (raw.sharedWith.includes(targetKey)) return false; raw.sharedWith.push(targetKey); saveCharacters(); return true; } export function unshareCharacter(ownerKey: UserKey, charName: string, targetKey: UserKey): boolean { const raw = _chars[ownerKey]?.characters.find( (c) => c.name.toLowerCase() === charName.toLowerCase() ); if (!raw || !raw.sharedWith) return false; raw.sharedWith = raw.sharedWith.filter((k) => k !== targetKey); saveCharacters(); return true; } // ─── Account data ───────────────────────────────────────────────────────────── export function getAccountData(userKey: UserKey): AccountData { return _accounts[userKey] ?? {}; } export function setAccountData(userKey: UserKey, data: Partial): void { if (!_accounts[userKey]) _accounts[userKey] = {}; Object.assign(_accounts[userKey], data); saveAccounts(); } export const Char = { load() { _chars = Store.readOrDefault(Paths.data("characters.json"), {}); _accounts = Store.readOrDefault(Paths.data("accounts.json"), {}); }, byName({ user, name }: { user: UserKey, name: CharName }): Character|null { return getCharacterByName(user, name); }, byClass({ owner, charClass }: { owner: string, charClass: CharacterClass }): Character|null { return getCharacterByClass(owner, charClass.key); }, isOwner({ user, charName }: { user: UserKey, charName: string }): boolean { return isCharacterOwner(user, charName); }, add({ user, char }: { user: UserKey, char: Omit }): boolean { return addCharacter(user, char); }, remove({ user, name }: { user: UserKey, name: string }): boolean { return removeCharacter(user, name); }, active({ user }: { user: UserKey }): Character|null { return getActiveCharacter(user); }, setActive({ user, name }: { user: UserKey, name: string }): boolean { return setActiveCharacter(user, name); }, setNation({ user, name, nation }: { user: UserKey, name: string, nation: Nation }): boolean { return setCharacterNation(user, name, nation); }, // setStats({ user, name, stats }: { user: UserKey, name: string, stats: CharacterStats }): boolean { // return setCharacterStats(user, name, stats); // }, share({ user, charName, targetUser }: { user: UserKey, charName: string, targetUser: UserKey }): boolean { return shareCharacter(user, charName, targetUser); }, unshare({ user, charName, targetUser }: { user: UserKey, charName: string, targetUser: UserKey }): boolean { return unshareCharacter(user, charName, targetUser); }, hydrate(raw: SerializableCharacter, ownerKey: UserKey): Character { return { ...raw, class: CLASSES[raw.class] ?? { key: raw.class as ClassKey, name: raw.class, shortName: raw.class }, ownerKey, }; }, dehydrate(char: Character): SerializableCharacter { const { ownerKey, ...rest } = char; return { ...rest, class: char.class.key }; }, };