types.ts
· 11 KiB · TypeScript
Raw
// ─── Branded primitive types ─────────────────────────────────────────────────
export type UserKey = string;
export type DiscordId = string;
export type CharName = string;
export type SlotHour = number;
export type VoteType = "yes" | "no";
export type ConfirmType = "yes" | "no";
// ─── Nation ───────────────────────────────────────────────────────────────────
export enum Nation {
Capella = "Capella",
Procyon = "Procyon",
}
// ─── Class ────────────────────────────────────────────────────────────────────
export type ClassKey =
| "BL" // Blader
| "FB" // Force Blader
| "FS" // Force Shielder
| "FA" // Force Archer
| "FG" // Force Gunner
| "GL" // Gladiator
| "DM" // Dark Mage
| "WI" // Wizard
| "WA"; // Warrior
export interface CharacterClass {
key: ClassKey;
name: string; // "Force Blader"
shortName: string; // "FB"
}
export const CLASSES: Record<ClassKey, CharacterClass> = {
FB: { key: "FB", name: "Force Blader", shortName: "FB" },
WI: { key: "WI", name: "Wizard", shortName: "WI" },
BL: { key: "BL", name: "Blader", shortName: "BL" },
FS: { key: "FS", name: "Force Shielder", shortName: "FS" },
FA: { key: "FA", name: "Force Archer", shortName: "FA" },
FG: { key: "FG", name: "Force Gunner", shortName: "FG" },
GL: { key: "GL", name: "Gladiator", shortName: "GL" },
DM: { key: "DM", name: "Dark Mage", shortName: "DM" },
WA: { key: "WA", name: "Warrior", shortName: "WA" },
};
export const CLASS_NAMES: Record<ClassKey, string> = {
BL: "Blader",
FB: "Force Blader",
FS: "Force Shielder",
FA: "Force Archer",
FG: "Force Gunner",
GL: "Gladiator",
DM: "Dark Mage",
WI: "Wizard",
WA: "Warrior",
};
// ─── Character ───────────────────────────────────────────────────────────────
export interface CharacterStats {
str?: number;
int?: number;
dex?: number;
honorRank?: number;
honorPoints?: number;
custom?: Record<string, number>;
}
export interface Character {
name: string;
class: CharacterClass;
level: number;
nation: Nation;
active?: boolean;
ownerKey: UserKey;
stats?: CharacterStats;
sharedWith?: string[]; // usermap keys with permanent access
}
export interface BorrowRequest {
requesterKey: string; // who wants to borrow
ownerKey: string; // who owns the character
charName: string; // which character
requestedAt: number; // timestamp for expiry
}
// ─── Account ─────────────────────────────────────────────────────────────────
export interface AccountData {
collection?: Record<string, number>;
animaMastery?: Record<string, number>;
custom?: Record<string, number>;
}
export interface AccountMap {
[userKey: string]: AccountData;
}
// interface UserIdentity {
// discordId: DiscordId; // never changes — primary key
// discordUsername: string; // current username — may change
// userKey: UserKey | null; // system key
// lookupId: string; // discordId if ID-mapped, discordUsername if legacy
// displayName: string;
// serverNickname: string | null;
// globalNickname: string | null;
// aliases: string[];
// activeCharacter: Character | null;
// }
// ─── Usermap ─────────────────────────────────────────────────────────────────
export interface UsermapEntry {
file: string;
aliases: string[];
}
export interface Usermap {
[discordUsername: string]: UsermapEntry | string; // string = legacy format
}
// ─── TG Slots ────────────────────────────────────────────────────────────────
export interface TGSlot {
tgHour: number; // 20
pollOpens: string; // "10:00"
closesAfter: number; // minutes after tgHour (default 35)
active: boolean;
}
// ─── Poll ────────────────────────────────────────────────────────────────────
// export interface VoteEntry {
// userKey: string;
// displayName: string; // server nickname → global nickname → username
// characterName?: string; // active character name at time of vote
// characterClass?: ClassKey; // snapshotted
// characterLevel?: number; // snapshotted
// characterNation?: Nation; // snapshotted at vote time
// votedAt: string; // HH:MM formatted
// previousYesAt?: string;
// previousNoAt?: string;
// publicMessage?: string; // resolved from message system or officer override
// publicMessageOverride?: string;// set by officer via /tg poll set-message
// ephemeralOverride?: string; // set by officer via /tg poll set-ephemeral
// borrowedFrom?: string // Borrowed character from who
// discordId?: string; // real Discord ID of the voter (for notifications)
// }
export interface VoteEntry {
userKey?: UserKey;
displayName?: string;
characterName?: CharName;
characterClass?: ClassKey;
characterLevel?: number;
characterNation?: Nation;
borrowedFrom?: UserKey;
discordId?: DiscordId;
votedAt?: string;
publicMessage?: string;
previousYesAt?: string;
previousNoAt?: string;
}
export interface PollState {
slot: SlotHour;
messageId?: string | null;
locked: boolean;
confirmed?: "yes" | "no" | null;
yes: Map<string, VoteEntry>;
no: Map<string, VoteEntry>;
lockedYesKeys?: Set<UserKey>;
lockMessage?: string;
confirmMessage?: string;
called?: boolean;
calledAt?: string;
}
// ─── Scores ──────────────────────────────────────────────────────────────────
export interface TGScore {
userKey: UserKey;
playedBy?: UserKey; // if borrowed
characterName: string;
class: string;
nation: Nation;
pts: number;
k?: number;
d?: number;
stats?: TGStats;
submittedAt: string;
slot: SlotHour;
date: string;
submittedByOfficer: boolean;
wRankAtSubmission?: {
rank: number;
delta: number;
};
}
export interface TGStats {
atk?: number;
def?: number;
heal?: number;
}
// ─── TG Result ───────────────────────────────────────────────────────────────
export interface NationKD {
k: number;
d: number;
}
export interface TGResult {
slot: number;
date: string; // YYYY-MM-DD
closedAt?: string; // ISO timestamp
confirmed: boolean;
nationKD: {
source: Nation; // which nation is source of truth
capella: NationKD;
procyon: NationKD;
};
scores: TGScore[];
}
// ─── W.Rank ──────────────────────────────────────────────────────────────────
// export interface WRankEntry {
// character: Character;
// weeklyPoints: number;
// tgCount: number;
// currentRank: number;
// previousRank?: number;
// }
// interface UserIdentity {
// discordId: DiscordId; // never changes — primary key
// discordUsername: string; // current username — may change
// userKey: UserKey | null; // system key
// lookupId: string; // discordId if ID-mapped, discordUsername if legacy
// displayName: string;
// serverNickname: string | null;
// globalNickname: string | null;
// aliases: string[];
// activeCharacter: Character | null;
// }
export interface WRankPosition {
currentRank: number;
previousRank?: number;
}
// ─── Bringer ─────────────────────────────────────────────────────────────────
export interface BringerState {
currentWeek: string; // "2026-W22"
capella: string | null; // userKey
procyon: string | null;
capellaOverride?: string;
procyonOverride?: string;
}
// ─── Messages ────────────────────────────────────────────────────────────────
export interface MessageEntry {
clicks: number;
message?: string; // single message (legacy)
messages?: string[]; // array of messages
random?: boolean;
days?: Record<string, { messages: string[]; random?: boolean }>;
dates?: Record<string, { messages: string[]; random?: boolean }>;
}
export interface MessageBlock {
yes: MessageEntry[];
no: MessageEntry[];
users?: Record<string, { yes: MessageEntry[]; no: MessageEntry[] }>;
}
export interface MessagesFile {
public: MessageBlock;
ephemeral: MessageBlock;
}
// ─── Emojis ──────────────────────────────────────────────────────────────────
export interface EmojiMap {
[key: string]: string; // e.g. "capella" → "<:Capella:1477082112560726238>"
}
// ─── Interaction context ─────────────────────────────────────────────────────
export interface ResolvedUser {
userId: string;
discordUsername: string; // interaction.user.username
userKey: string | null; // resolved from usermap
lookupUsername: string | null;
displayName: string; // server nickname → global nickname → username
serverNickname: string | null;
globalNickname: string | null;
aliases: string[];
activeCharacter: Character | null;
}
| 1 | |
| 2 | // ─── Branded primitive types ───────────────────────────────────────────────── |
| 3 | export type UserKey = string; |
| 4 | export type DiscordId = string; |
| 5 | export type CharName = string; |
| 6 | export type SlotHour = number; |
| 7 | export type VoteType = "yes" | "no"; |
| 8 | export type ConfirmType = "yes" | "no"; |
| 9 | |
| 10 | |
| 11 | // ─── Nation ─────────────────────────────────────────────────────────────────── |
| 12 | export enum Nation { |
| 13 | Capella = "Capella", |
| 14 | Procyon = "Procyon", |
| 15 | } |
| 16 | |
| 17 | // ─── Class ──────────────────────────────────────────────────────────────────── |
| 18 | export 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 | |
| 29 | export interface CharacterClass { |
| 30 | key: ClassKey; |
| 31 | name: string; // "Force Blader" |
| 32 | shortName: string; // "FB" |
| 33 | } |
| 34 | |
| 35 | export 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 | |
| 47 | export 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 | |
| 61 | export interface CharacterStats { |
| 62 | str?: number; |
| 63 | int?: number; |
| 64 | dex?: number; |
| 65 | honorRank?: number; |
| 66 | honorPoints?: number; |
| 67 | custom?: Record<string, number>; |
| 68 | } |
| 69 | |
| 70 | export 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 | |
| 81 | export 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 | |
| 90 | export interface AccountData { |
| 91 | collection?: Record<string, number>; |
| 92 | animaMastery?: Record<string, number>; |
| 93 | custom?: Record<string, number>; |
| 94 | } |
| 95 | |
| 96 | export 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 | |
| 114 | export interface UsermapEntry { |
| 115 | file: string; |
| 116 | aliases: string[]; |
| 117 | } |
| 118 | |
| 119 | export interface Usermap { |
| 120 | [discordUsername: string]: UsermapEntry | string; // string = legacy format |
| 121 | } |
| 122 | |
| 123 | // ─── TG Slots ──────────────────────────────────────────────────────────────── |
| 124 | |
| 125 | export 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 | |
| 151 | export 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 | |
| 166 | export 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 | |
| 182 | export 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 | |
| 202 | export interface TGStats { |
| 203 | atk?: number; |
| 204 | def?: number; |
| 205 | heal?: number; |
| 206 | } |
| 207 | |
| 208 | // ─── TG Result ─────────────────────────────────────────────────────────────── |
| 209 | |
| 210 | export interface NationKD { |
| 211 | k: number; |
| 212 | d: number; |
| 213 | } |
| 214 | |
| 215 | export 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 | |
| 251 | export interface WRankPosition { |
| 252 | currentRank: number; |
| 253 | previousRank?: number; |
| 254 | } |
| 255 | |
| 256 | // ─── Bringer ───────────────────────────────────────────────────────────────── |
| 257 | |
| 258 | export 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 | |
| 268 | export 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 | |
| 277 | export interface MessageBlock { |
| 278 | yes: MessageEntry[]; |
| 279 | no: MessageEntry[]; |
| 280 | users?: Record<string, { yes: MessageEntry[]; no: MessageEntry[] }>; |
| 281 | } |
| 282 | |
| 283 | export interface MessagesFile { |
| 284 | public: MessageBlock; |
| 285 | ephemeral: MessageBlock; |
| 286 | } |
| 287 | |
| 288 | // ─── Emojis ────────────────────────────────────────────────────────────────── |
| 289 | |
| 290 | export interface EmojiMap { |
| 291 | [key: string]: string; // e.g. "capella" → "<:Capella:1477082112560726238>" |
| 292 | } |
| 293 | |
| 294 | // ─── Interaction context ───────────────────────────────────────────────────── |
| 295 | |
| 296 | export 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 | } |