Ostatnio aktywny 1 month ago

nuno zrewidował ten Gist 1 month ago. Przejdź do rewizji

1 file changed, 245 insertions

characters.ts(stworzono plik)

@@ -0,0 +1,245 @@
1 + import { Paths } from "@paths";
2 + import {
3 + Character, CharacterStats, CharName,
4 + CharacterClass, ClassKey, Nation, UserKey, AccountData, AccountMap,
5 + CLASSES,
6 + } from "@types";
7 + import { Store } from "@systems/store";
8 + import { Runtime } from "@systems/runtime";
9 +
10 + // ─── Runtime ──────────────────────────────────────────────────────────────────
11 + Runtime.phase("load", () => Char.load(), { name: "Char.load" });
12 +
13 + let _chars: CharacterMap = {};
14 + let _accounts: AccountMap = {};
15 +
16 + /** Raw shape stored in characters.json */
17 + interface 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 +
27 + interface CharacterMap {
28 + [userKey: string]: {
29 + characters: SerializableCharacter[];
30 + };
31 + }
32 +
33 + export function loadCharacters(): void {
34 + _chars = Store.readOrDefault<CharacterMap>(Paths.data("characters.json"), {});
35 + _accounts = Store.readOrDefault<AccountMap>(Paths.data("accounts.json"), {});
36 + }
37 +
38 + function 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 +
51 + function saveAccounts(): void {
52 + Store.write(Paths.data("accounts.json"), _accounts);
53 + }
54 +
55 + // ─── Helpers ──────────────────────────────────────────────────────────────────
56 +
57 + /** Get hydrated characters for a user */
58 + export function getCharacters(userKey: UserKey): Character[] {
59 + return (_chars[userKey]?.characters ?? []).map((c) =>
60 + Char.hydrate(c as SerializableCharacter, userKey)
61 + );
62 + }
63 +
64 + export function getActiveCharacter(userKey: UserKey): Character | null {
65 + return getCharacters(userKey).find((c) => c.active) ?? null;
66 + }
67 +
68 + export function getCharacterByName(userKey: UserKey, name: string): Character | null {
69 + return getCharacters(userKey).find(
70 + (c) => c.name.toLowerCase() === name.toLowerCase()
71 + ) ?? null;
72 + }
73 +
74 + export 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 +
79 + export 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 +
86 + export 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 +
98 + export 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 +
113 + export 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 +
124 + export 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 +
134 + export 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 +
149 + export 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 +
161 + export 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 ─────────────────────────────────────────────────────────────
172 + export function getAccountData(userKey: UserKey): AccountData {
173 + return _accounts[userKey] ?? {};
174 + }
175 +
176 + export 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 +
183 + export 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 + };
Nowsze Starsze