- ATK/DEF/Heal now accept K/M shorthand (500K, 1.4M, case-insensitive) via
a shared parser (@helpers/stat-value), used identically by the Submit
Score modal, /tg score set, and /tg-admin score-inject
- /tg-admin score-modal — officers can open the Submit Score modal on
behalf of any player; the modal's customId now carries the target
userKey so submission always resolves to the intended player rather
than whoever's Discord client is submitting it
- fix: /tg call now actually reveals the Submit Score button (was
recording the call but never opening submission)
- fix: /tg poll reload no longer resurrects Submit Score / Yes-No buttons
that had already been removed — poll state now tracks submission-open
and buttons-removed explicitly instead of re-deriving it from
locked/confirmed on every render
- fix: midnight cleanup now fully removes all poll buttons (not just
Submit Score) instead of leaving them disabled
- Sleep Check — players on a configured role get flagged (💤 shown on
their poll row) the moment they vote Yes; a scheduled per-slot job DMs
everyone currently flagged at a configurable time before TG (default
20 min). State changes and DM sending are intentionally decoupled —
flagging never itself sends a message. Officers can also flag/clear
manually via /tg-admin sleep-check set|clear
- data/updates/v0.9.2 and new v0.10 changelog entries for the above
- merged the two .claude reference docs into one, removed stale/fixed
items, documented the new systems and known deferred gaps
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||
import { Config } from "@systems/config";
|
||
import { hasOfficerRole } from "@systems/users";
|
||
import { polls, updatePollMessage } from "@systems/poll";
|
||
import { persist } from "@systems/pollPersistence";
|
||
import { VoteEntry, PollState } from "@types";
|
||
import { Discord } from "@discord";
|
||
import { replyAndDelete } from "@utils";
|
||
|
||
interface FoundEntry {
|
||
slot: number;
|
||
state: PollState;
|
||
entry: VoteEntry;
|
||
}
|
||
|
||
function findYesEntry(userKey: string): FoundEntry | null {
|
||
const slot = [...polls.keys()][0];
|
||
if (slot === undefined) return null;
|
||
const state = polls.get(slot)!;
|
||
const entry = [...state.yes.values()].find((e) => e.userKey === userKey);
|
||
return entry ? { slot, state, entry } : null;
|
||
}
|
||
|
||
export async function handleSleepCheckSet(interaction: ChatInputCommandInteraction): Promise<void> {
|
||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||
return void replyAndDelete(interaction, "❌ Officer only.", true);
|
||
}
|
||
|
||
const opts = Discord.Interaction.options(interaction);
|
||
const userKey = opts.string({ key: "name", required: true })!;
|
||
|
||
const found = findYesEntry(userKey);
|
||
if (!found) {
|
||
return void replyAndDelete(interaction, `❌ **${userKey}** hasn't voted Yes in the active poll.`, true);
|
||
}
|
||
|
||
found.entry.sleepCheckPending = true;
|
||
persist.save(polls);
|
||
|
||
const channel = await interaction.client.channels.fetch(
|
||
Config.get({ section: "channels", key: "poll" })
|
||
) as TextChannel;
|
||
await updatePollMessage(channel, found.slot, undefined, found.state.scoreSubmitOpen === true);
|
||
|
||
return void replyAndDelete(interaction, `💤 **${userKey}** flagged for sleep check. The confirmation DM goes out at the usual scheduled time.`, true);
|
||
}
|
||
|
||
export async function handleSleepCheckClear(interaction: ChatInputCommandInteraction): Promise<void> {
|
||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||
return void replyAndDelete(interaction, "❌ Officer only.", true);
|
||
}
|
||
|
||
const opts = Discord.Interaction.options(interaction);
|
||
const userKey = opts.string({ key: "name", required: true })!;
|
||
|
||
const found = findYesEntry(userKey);
|
||
if (!found) {
|
||
return void replyAndDelete(interaction, `❌ **${userKey}** hasn't voted Yes in the active poll.`, true);
|
||
}
|
||
if (!found.entry.sleepCheckPending) {
|
||
return void replyAndDelete(interaction, `ℹ️ **${userKey}** doesn't have a pending sleep check.`, true);
|
||
}
|
||
|
||
found.entry.sleepCheckPending = false;
|
||
persist.save(polls);
|
||
|
||
const channel = await interaction.client.channels.fetch(
|
||
Config.get({ section: "channels", key: "poll" })
|
||
) as TextChannel;
|
||
await updatePollMessage(channel, found.slot, undefined, found.state.scoreSubmitOpen === true);
|
||
|
||
return void replyAndDelete(interaction, `✅ Sleep check cleared for **${userKey}**.`, true);
|
||
}
|
||
|
||
export const SleepCheckCommands = {
|
||
set: handleSleepCheckSet,
|
||
clear: handleSleepCheckClear,
|
||
};
|