Last active 1 month ago

characters.ts Raw
1import { Paths } from "@paths";
2import {
3 Character, CharacterStats, CharName,
4 CharacterClass, ClassKey, Nation, UserKey, AccountData, AccountMap,
5 CLASSES,
6} from "@types";
7import { Store } from "@systems/store";
8import { Runtime } from "@systems/runtime";
9
10// ─── Runtime ──────────────────────────────────────────────────────────────────
11Runtime.phase("load", () => Char.load(), { name: "Char.load" });
12
13let _chars: CharacterMap = {};
14let _accounts: AccountMap = {};
15
16/** Raw shape stored in characters.json */
17interface SerializableCharacter {
18 name: CharName;
19 class: ClassKey; // "FB" — serialized as string
20 level: number;
21 nation: Nation;
22 active?: boolean;
23 sharedWith?: UserKey[];
24 stats?: CharacterStats;
25}
26
27interface CharacterMap {
28 [userKey: string]: {
29 characters: SerializableCharacter[];
30 };
31}
32
33export function loadCharacters(): void {
34 _chars = Store.readOrDefault<CharacterMap>(Paths.data("characters.json"), {});
35 _accounts = Store.readOrDefault<AccountMap>(Paths.data("accounts.json"), {});
36}
37
38function saveCharacters(): void {
39 // Dehydrate all characters before saving
40 const raw: CharacterMap = {};
41 for (const [userKey, data] of Object.entries(_chars)) {
42 raw[userKey] = {
43 characters: data.characters.map((c) =>
44 "ownerKey" in c ? Char.dehydrate(c as unknown as Character) : c
45 ),
46 };
47 }
48 Store.write(Paths.data("characters.json"), raw);
49}
50
51function saveAccounts(): void {
52 Store.write(Paths.data("accounts.json"), _accounts);
53}
54
55// ─── Helpers ──────────────────────────────────────────────────────────────────
56
57/** Get hydrated characters for a user */
58export function getCharacters(userKey: UserKey): Character[] {
59 return (_chars[userKey]?.characters ?? []).map((c) =>
60 Char.hydrate(c as SerializableCharacter, userKey)
61 );
62}
63
64export function getActiveCharacter(userKey: UserKey): Character | null {
65 return getCharacters(userKey).find((c) => c.active) ?? null;
66}
67
68export function getCharacterByName(userKey: UserKey, name: string): Character | null {
69 return getCharacters(userKey).find(
70 (c) => c.name.toLowerCase() === name.toLowerCase()
71 ) ?? null;
72}
73
74export function getCharacterByClass(userKey: UserKey, cls: ClassKey): Character | null {
75 const chars = getCharacters(userKey).filter((c) => c.class.key === cls);
76 return chars.find((c) => c.active) ?? chars[0] ?? null;
77}
78
79export function isCharacterOwner(userKey: UserKey | null, charName: string): boolean {
80 if (!userKey) return false;
81 return getCharacters(userKey).some((c) => c.name === charName);
82}
83
84// ─── Mutations ────────────────────────────────────────────────────────────────
85
86export function addCharacter(userKey: UserKey, char: Omit<SerializableCharacter, "active">): boolean {
87 if (!_chars[userKey]) _chars[userKey] = { characters: [] };
88 const exists = _chars[userKey].characters.some(
89 (c) => c.name.toLowerCase() === char.name.toLowerCase()
90 );
91 if (exists) return false;
92 const hasActive = _chars[userKey].characters.some((c) => c.active);
93 _chars[userKey].characters.push({ ...char, active: !hasActive });
94 saveCharacters();
95 return true;
96}
97
98export function removeCharacter(userKey: UserKey, name: string): boolean {
99 if (!_chars[userKey]) return false;
100 const before = _chars[userKey].characters.length;
101 _chars[userKey].characters = _chars[userKey].characters.filter(
102 (c) => c.name.toLowerCase() !== name.toLowerCase()
103 );
104 if (_chars[userKey].characters.length === before) return false;
105 if (!_chars[userKey].characters.some((c) => c.active) &&
106 _chars[userKey].characters.length > 0) {
107 _chars[userKey].characters[0].active = true;
108 }
109 saveCharacters();
110 return true;
111}
112
113export function setActiveCharacter(userKey: UserKey, name: string): boolean {
114 const chars = _chars[userKey]?.characters;
115 if (!chars) return false;
116 const target = chars.find((c) => c.name.toLowerCase() === name.toLowerCase());
117 if (!target) return false;
118 chars.forEach((c) => (c.active = false));
119 target.active = true;
120 saveCharacters();
121 return true;
122}
123
124export function setCharacterNation(userKey: UserKey, name: string, nation: Nation): boolean {
125 const raw = _chars[userKey]?.characters.find(
126 (c) => c.name.toLowerCase() === name.toLowerCase()
127 );
128 if (!raw) return false;
129 raw.nation = nation;
130 saveCharacters();
131 return true;
132}
133
134export function setCharacterStats(
135 userKey: UserKey,
136 name: string,
137 stats: { atk?: number; def?: number; heal?: number }
138): boolean {
139 const raw = _chars[userKey]?.characters.find(
140 (c) => c.name.toLowerCase() === name.toLowerCase()
141 );
142 if (!raw) return false;
143 if (!raw.stats) raw.stats = {};
144 Object.assign(raw.stats, stats);
145 saveCharacters();
146 return true;
147}
148
149export function shareCharacter(ownerKey: UserKey, charName: string, targetKey: UserKey): boolean {
150 const raw = _chars[ownerKey]?.characters.find(
151 (c) => c.name.toLowerCase() === charName.toLowerCase()
152 );
153 if (!raw) return false;
154 if (!raw.sharedWith) raw.sharedWith = [];
155 if (raw.sharedWith.includes(targetKey)) return false;
156 raw.sharedWith.push(targetKey);
157 saveCharacters();
158 return true;
159}
160
161export function unshareCharacter(ownerKey: UserKey, charName: string, targetKey: UserKey): boolean {
162 const raw = _chars[ownerKey]?.characters.find(
163 (c) => c.name.toLowerCase() === charName.toLowerCase()
164 );
165 if (!raw || !raw.sharedWith) return false;
166 raw.sharedWith = raw.sharedWith.filter((k) => k !== targetKey);
167 saveCharacters();
168 return true;
169}
170
171// ─── Account data ─────────────────────────────────────────────────────────────
172export function getAccountData(userKey: UserKey): AccountData {
173 return _accounts[userKey] ?? {};
174}
175
176export function setAccountData(userKey: UserKey, data: Partial<AccountData>): void {
177 if (!_accounts[userKey]) _accounts[userKey] = {};
178 Object.assign(_accounts[userKey], data);
179 saveAccounts();
180}
181
182
183export const Char = {
184 load() {
185 _chars = Store.readOrDefault<CharacterMap>(Paths.data("characters.json"), {});
186 _accounts = Store.readOrDefault<AccountMap>(Paths.data("accounts.json"), {});
187 },
188
189 byName({ user, name }: { user: UserKey, name: CharName }): Character|null {
190 return getCharacterByName(user, name);
191 },
192
193 byClass({ owner, charClass }: { owner: string, charClass: CharacterClass }): Character|null {
194 return getCharacterByClass(owner, charClass.key);
195 },
196
197 isOwner({ user, charName }: { user: UserKey, charName: string }): boolean {
198 return isCharacterOwner(user, charName);
199 },
200
201 add({ user, char }: { user: UserKey, char: Omit<SerializableCharacter, "active"> }): boolean {
202 return addCharacter(user, char);
203 },
204
205 remove({ user, name }: { user: UserKey, name: string }): boolean {
206 return removeCharacter(user, name);
207 },
208
209 active({ user }: { user: UserKey }): Character|null {
210 return getActiveCharacter(user);
211 },
212
213 setActive({ user, name }: { user: UserKey, name: string }): boolean {
214 return setActiveCharacter(user, name);
215 },
216
217 setNation({ user, name, nation }: { user: UserKey, name: string, nation: Nation }): boolean {
218 return setCharacterNation(user, name, nation);
219 },
220
221 // setStats({ user, name, stats }: { user: UserKey, name: string, stats: CharacterStats }): boolean {
222 // return setCharacterStats(user, name, stats);
223 // },
224
225 share({ user, charName, targetUser }: { user: UserKey, charName: string, targetUser: UserKey }): boolean {
226 return shareCharacter(user, charName, targetUser);
227 },
228
229 unshare({ user, charName, targetUser }: { user: UserKey, charName: string, targetUser: UserKey }): boolean {
230 return unshareCharacter(user, charName, targetUser);
231 },
232
233 hydrate(raw: SerializableCharacter, ownerKey: UserKey): Character {
234 return {
235 ...raw,
236 class: CLASSES[raw.class] ?? { key: raw.class as ClassKey, name: raw.class, shortName: raw.class },
237 ownerKey,
238 };
239 },
240
241 dehydrate(char: Character): SerializableCharacter {
242 const { ownerKey, ...rest } = char;
243 return { ...rest, class: char.class.key };
244 },
245};