Last active 1 month ago

types.ts Raw
1
2// ─── Branded primitive types ─────────────────────────────────────────────────
3export type UserKey = string;
4export type DiscordId = string;
5export type CharName = string;
6export type SlotHour = number;
7export type VoteType = "yes" | "no";
8export type ConfirmType = "yes" | "no";
9
10
11// ─── Nation ───────────────────────────────────────────────────────────────────
12export enum Nation {
13 Capella = "Capella",
14 Procyon = "Procyon",
15}
16
17// ─── Class ────────────────────────────────────────────────────────────────────
18export type ClassKey =
19 | "BL" // Blader
20 | "FB" // Force Blader
21 | "FS" // Force Shielder
22 | "FA" // Force Archer
23 | "FG" // Force Gunner
24 | "GL" // Gladiator
25 | "DM" // Dark Mage
26 | "WI" // Wizard
27 | "WA"; // Warrior
28
29export interface CharacterClass {
30 key: ClassKey;
31 name: string; // "Force Blader"
32 shortName: string; // "FB"
33}
34
35export const CLASSES: Record<ClassKey, CharacterClass> = {
36 FB: { key: "FB", name: "Force Blader", shortName: "FB" },
37 WI: { key: "WI", name: "Wizard", shortName: "WI" },
38 BL: { key: "BL", name: "Blader", shortName: "BL" },
39 FS: { key: "FS", name: "Force Shielder", shortName: "FS" },
40 FA: { key: "FA", name: "Force Archer", shortName: "FA" },
41 FG: { key: "FG", name: "Force Gunner", shortName: "FG" },
42 GL: { key: "GL", name: "Gladiator", shortName: "GL" },
43 DM: { key: "DM", name: "Dark Mage", shortName: "DM" },
44 WA: { key: "WA", name: "Warrior", shortName: "WA" },
45};
46
47export const CLASS_NAMES: Record<ClassKey, string> = {
48 BL: "Blader",
49 FB: "Force Blader",
50 FS: "Force Shielder",
51 FA: "Force Archer",
52 FG: "Force Gunner",
53 GL: "Gladiator",
54 DM: "Dark Mage",
55 WI: "Wizard",
56 WA: "Warrior",
57};
58
59// ─── Character ───────────────────────────────────────────────────────────────
60
61export interface CharacterStats {
62 str?: number;
63 int?: number;
64 dex?: number;
65 honorRank?: number;
66 honorPoints?: number;
67 custom?: Record<string, number>;
68}
69
70export interface Character {
71 name: string;
72 class: CharacterClass;
73 level: number;
74 nation: Nation;
75 active?: boolean;
76 ownerKey: UserKey;
77 stats?: CharacterStats;
78 sharedWith?: string[]; // usermap keys with permanent access
79}
80
81export interface BorrowRequest {
82 requesterKey: string; // who wants to borrow
83 ownerKey: string; // who owns the character
84 charName: string; // which character
85 requestedAt: number; // timestamp for expiry
86}
87
88// ─── Account ─────────────────────────────────────────────────────────────────
89
90export interface AccountData {
91 collection?: Record<string, number>;
92 animaMastery?: Record<string, number>;
93 custom?: Record<string, number>;
94}
95
96export interface AccountMap {
97 [userKey: string]: AccountData;
98}
99
100// interface UserIdentity {
101// discordId: DiscordId; // never changes — primary key
102// discordUsername: string; // current username — may change
103// userKey: UserKey | null; // system key
104// lookupId: string; // discordId if ID-mapped, discordUsername if legacy
105// displayName: string;
106// serverNickname: string | null;
107// globalNickname: string | null;
108// aliases: string[];
109// activeCharacter: Character | null;
110// }
111
112// ─── Usermap ─────────────────────────────────────────────────────────────────
113
114export interface UsermapEntry {
115 file: string;
116 aliases: string[];
117}
118
119export interface Usermap {
120 [discordUsername: string]: UsermapEntry | string; // string = legacy format
121}
122
123// ─── TG Slots ────────────────────────────────────────────────────────────────
124
125export interface TGSlot {
126 tgHour: number; // 20
127 pollOpens: string; // "10:00"
128 closesAfter: number; // minutes after tgHour (default 35)
129 active: boolean;
130}
131
132// ─── Poll ────────────────────────────────────────────────────────────────────
133
134// export interface VoteEntry {
135// userKey: string;
136// displayName: string; // server nickname → global nickname → username
137// characterName?: string; // active character name at time of vote
138// characterClass?: ClassKey; // snapshotted
139// characterLevel?: number; // snapshotted
140// characterNation?: Nation; // snapshotted at vote time
141// votedAt: string; // HH:MM formatted
142// previousYesAt?: string;
143// previousNoAt?: string;
144// publicMessage?: string; // resolved from message system or officer override
145// publicMessageOverride?: string;// set by officer via /tg poll set-message
146// ephemeralOverride?: string; // set by officer via /tg poll set-ephemeral
147// borrowedFrom?: string // Borrowed character from who
148// discordId?: string; // real Discord ID of the voter (for notifications)
149// }
150
151export interface VoteEntry {
152 userKey?: UserKey;
153 displayName?: string;
154 characterName?: CharName;
155 characterClass?: ClassKey;
156 characterLevel?: number;
157 characterNation?: Nation;
158 borrowedFrom?: UserKey;
159 discordId?: DiscordId;
160 votedAt?: string;
161 publicMessage?: string;
162 previousYesAt?: string;
163 previousNoAt?: string;
164}
165
166export interface PollState {
167 slot: SlotHour;
168 messageId?: string | null;
169 locked: boolean;
170 confirmed?: "yes" | "no" | null;
171 yes: Map<string, VoteEntry>;
172 no: Map<string, VoteEntry>;
173 lockedYesKeys?: Set<UserKey>;
174 lockMessage?: string;
175 confirmMessage?: string;
176 called?: boolean;
177 calledAt?: string;
178}
179
180// ─── Scores ──────────────────────────────────────────────────────────────────
181
182export interface TGScore {
183 userKey: UserKey;
184 playedBy?: UserKey; // if borrowed
185 characterName: string;
186 class: string;
187 nation: Nation;
188 pts: number;
189 k?: number;
190 d?: number;
191 stats?: TGStats;
192 submittedAt: string;
193 slot: SlotHour;
194 date: string;
195 submittedByOfficer: boolean;
196 wRankAtSubmission?: {
197 rank: number;
198 delta: number;
199 };
200}
201
202export interface TGStats {
203 atk?: number;
204 def?: number;
205 heal?: number;
206}
207
208// ─── TG Result ───────────────────────────────────────────────────────────────
209
210export interface NationKD {
211 k: number;
212 d: number;
213}
214
215export interface TGResult {
216 slot: number;
217 date: string; // YYYY-MM-DD
218 closedAt?: string; // ISO timestamp
219 confirmed: boolean;
220 nationKD: {
221 source: Nation; // which nation is source of truth
222 capella: NationKD;
223 procyon: NationKD;
224 };
225 scores: TGScore[];
226}
227
228// ─── W.Rank ──────────────────────────────────────────────────────────────────
229
230// export interface WRankEntry {
231// character: Character;
232// weeklyPoints: number;
233// tgCount: number;
234// currentRank: number;
235// previousRank?: number;
236// }
237
238
239// interface UserIdentity {
240// discordId: DiscordId; // never changes — primary key
241// discordUsername: string; // current username — may change
242// userKey: UserKey | null; // system key
243// lookupId: string; // discordId if ID-mapped, discordUsername if legacy
244// displayName: string;
245// serverNickname: string | null;
246// globalNickname: string | null;
247// aliases: string[];
248// activeCharacter: Character | null;
249// }
250
251export interface WRankPosition {
252 currentRank: number;
253 previousRank?: number;
254}
255
256// ─── Bringer ─────────────────────────────────────────────────────────────────
257
258export interface BringerState {
259 currentWeek: string; // "2026-W22"
260 capella: string | null; // userKey
261 procyon: string | null;
262 capellaOverride?: string;
263 procyonOverride?: string;
264}
265
266// ─── Messages ────────────────────────────────────────────────────────────────
267
268export interface MessageEntry {
269 clicks: number;
270 message?: string; // single message (legacy)
271 messages?: string[]; // array of messages
272 random?: boolean;
273 days?: Record<string, { messages: string[]; random?: boolean }>;
274 dates?: Record<string, { messages: string[]; random?: boolean }>;
275}
276
277export interface MessageBlock {
278 yes: MessageEntry[];
279 no: MessageEntry[];
280 users?: Record<string, { yes: MessageEntry[]; no: MessageEntry[] }>;
281}
282
283export interface MessagesFile {
284 public: MessageBlock;
285 ephemeral: MessageBlock;
286}
287
288// ─── Emojis ──────────────────────────────────────────────────────────────────
289
290export interface EmojiMap {
291 [key: string]: string; // e.g. "capella" → "<:Capella:1477082112560726238>"
292}
293
294// ─── Interaction context ─────────────────────────────────────────────────────
295
296export interface ResolvedUser {
297 userId: string;
298 discordUsername: string; // interaction.user.username
299 userKey: string | null; // resolved from usermap
300 lookupUsername: string | null;
301 displayName: string; // server nickname → global nickname → username
302 serverNickname: string | null;
303 globalNickname: string | null;
304 aliases: string[];
305 activeCharacter: Character | null;
306}