tg-bot-ts/src/types.ts

306 lines
No EOL
11 KiB
TypeScript

// ─── 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;
}