/** * Wi-Fi chat-plugin. Only works in a room with id 'wifi' * Handles giveaways in the formats: question, lottery, gts * Written by dhelmise and bumbadadabum, based on the original * plugin as written by Codelegend, SilverTactic, DanielCranham */ import { FS, Utils } from '../../lib'; Punishments.addRoomPunishmentType({ type: 'GIVEAWAYBAN', desc: 'banned from giveaways', }); const DAY = 24 * 60 * 60 * 1000; const RECENT_THRESHOLD = 30 * 24 * 60 * 60 * 1000; const DATA_FILE = 'config/chat-plugins/wifi.json'; type Game = 'SwSh' | 'BDSP' | 'SV'; interface GiveawayData { targetUserID: string; ot: string; tid: string; game: Game; prize: PokemonSet; ivs: string[]; ball: string; extraInfo: string; /** Staff handling it. */ claimed?: ID; } interface QuestionGiveawayData extends GiveawayData { question: string; answers: string[]; } interface LotteryGiveawayData extends GiveawayData { winners: number; } interface WifiData { whitelist: string[]; stats: { [k: string]: number[] }; storedGiveaways: { question: QuestionGiveawayData[], lottery: LotteryGiveawayData[] }; submittedGiveaways: { question: QuestionGiveawayData[], lottery: LotteryGiveawayData[] }; } const defaults: WifiData = { whitelist: [], stats: {}, storedGiveaways: { question: [], lottery: [], }, submittedGiveaways: { question: [], lottery: [], }, }; export let wifiData: WifiData = (() => { try { return JSON.parse(FS(DATA_FILE).readSync()); } catch (e: any) { if (e.code !== 'ENOENT') throw e; return defaults; } })(); function saveData() { FS(DATA_FILE).writeUpdate(() => JSON.stringify(wifiData)); } // Convert old file type if (!wifiData.stats && !wifiData.storedGiveaways && !wifiData.submittedGiveaways) { // we cast under the assumption that it's the old file format const stats = { ...wifiData } as unknown as { [k: string]: number[] }; wifiData = { ...defaults, stats }; saveData(); } // ensure the whitelist exists for those who might have the conversion above but not the stats if (!wifiData.whitelist) wifiData.whitelist = []; const statNames = ["HP", "Atk", "Def", "SpA", "SpD", "Spe"]; const gameName: { [k in Game]: string } = { SwSh: 'Sword/Shield', BDSP: 'Brilliant Diamond/Shining Pearl', SV: 'Scarlet/Violet', }; const gameidToGame: { [k: string]: Game } = { swsh: 'SwSh', bdsp: 'BDSP', sv: 'SV', }; abstract class Giveaway extends Rooms.SimpleRoomGame { override readonly gameid = 'giveaway' as ID; abstract type: string; gaNumber: number; host: User; giver: User; room: Room; ot: string; tid: string; game: Game; ivs: string[]; prize: PokemonSet; phase: string; ball: string; extraInfo: string; /** * IP:userid */ joined: Map; timer: NodeJS.Timeout | null; pokemonID: ID; sprite: Chat.VNode; constructor( host: User, giver: User, room: Room, ot: string, tid: string, ivs: string[], prize: PokemonSet, game: Game = 'SV', ball: string, extraInfo: string ) { // Make into a sub-game if the gts ever opens up again super(room); this.gaNumber = room.nextGameNumber(); this.host = host; this.giver = giver; this.room = room; this.ot = ot; this.tid = tid; this.ball = ball; this.extraInfo = extraInfo; this.game = game; this.ivs = ivs; this.prize = prize; this.phase = 'pending'; this.joined = new Map(); this.timer = null; [this.pokemonID, this.sprite] = Giveaway.getSprite(prize); } destroy() { this.clearTimer(); super.destroy(); } generateReminder(joined?: boolean): string | Chat.VNode; generateReminder() { return ''; } getStyle() { const css: { [k: string]: string | { [k: string]: string } } = { class: "broadcast-blue" }; if (this.game === 'BDSP') css.style = { background: '#aa66a9', color: '#fff' }; if (this.game === 'SV') css.style = { background: '#CD5C5C', color: '#fff' }; return css; } sendToUser(user: User, content: string | Chat.VNode) { user.sendTo( this.room, Chat.html`|uhtmlchange|giveaway${this.gaNumber}${this.phase}|${
{content}
}` ); } send(content: string | Chat.VNode, isStart = false) { this.room.add(Chat.html`|uhtml|giveaway${this.gaNumber}${this.phase}|${
{content}
}`); if (isStart) this.room.add(`|c:|${Math.floor(Date.now() / 1000)}|~|It's ${this.game} giveaway time!`); this.room.update(); } changeUhtml(content: string | Chat.VNode) { this.room.uhtmlchange(`giveaway${this.gaNumber}${this.phase}`, Chat.html`${
{content}
}`); this.room.update(); } clearTimer() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } } checkJoined(user: User) { for (const [ip, id] of this.joined) { if (user.latestIp === ip && !Config.noipchecks) return ip; if (user.previousIDs.includes(id)) return id; } return false; } kickUser(user: User) { for (const [ip, id] of this.joined) { if (user.latestIp === ip && !Config.noipchecks || user.previousIDs.includes(id)) { this.sendToUser(user, this.generateReminder()); this.joined.delete(ip); } } } checkExcluded(user: User) { return ( user === this.giver || !Config.noipchecks && this.giver.ips.includes(user.latestIp) || this.giver.previousIDs.includes(toID(user)) ); } static checkCanCreate(context: Chat.CommandContext, targetUser: User, type: string) { const user = context.user; const isCreate = type === 'create'; const isForSelf = targetUser.id === user.id; if (wifiData.whitelist.includes(user.id) && isCreate && isForSelf) { // it being true doesn't matter here, it's just clearer that the user _is_ allowed // and it ensures execution stops here so the creation can proceed return true; } if (isCreate && !(isForSelf && user.can('show', null, context.room!))) { context.checkCan('warn', null, context.room!); } if (!user.can('warn', null, context.room!) && !isCreate && !isForSelf) { throw new Chat.ErrorMessage(`You can't ${type} giveways for other users.`); } } static checkBanned(room: Room, user: User) { return Punishments.hasRoomPunishType(room, toID(user), 'GIVEAWAYBAN'); } static ban(room: Room, user: User, reason: string, duration: number) { Punishments.roomPunish(room, user, { type: 'GIVEAWAYBAN', id: toID(user), expireTime: Date.now() + duration, reason, }); } static unban(room: Room, user: User) { Punishments.roomUnpunish(room, user.id, 'GIVEAWAYBAN', false); } static getSprite(set: PokemonSet): [ID, Chat.VNode] { const species = Dex.species.get(set.species); let spriteid = species.spriteid; if (species.cosmeticFormes) { for (const forme of species.cosmeticFormes.map(toID)) { if (toID(set.species).includes(forme)) { spriteid += '-' + forme.slice(species.baseSpecies.length); break; // We don't want to end up with deerling-summer-spring } } } if (!spriteid.includes('-') && species.forme) { // for stuff like unown letters spriteid += '-' + toID(species.forme); } const shiny = set.shiny ? '-shiny' : ''; const validFemale = [ 'abomasnow', 'aipom', 'ambipom', 'beautifly', 'bibarel', 'bidoof', 'blaziken', 'buizel', 'cacturne', 'camerupt', 'combee', 'combusken', 'croagunk', 'donphan', 'dustox', 'finneon', 'floatzel', 'frillish', 'gabite', 'garchomp', 'gible', 'girafarig', 'gligar', 'golbat', 'gulpin', 'heracross', 'hippopotas', 'hippowdon', 'houndoom', 'indeedee', 'jellicent', 'kerfluffle', 'kitsunoh', 'kricketot', 'kricketune', 'ledian', 'ledyba', 'ludicolo', 'lumineon', 'luxio', 'luxray', 'magikarp', 'mamoswine', 'medicham', 'meditite', 'meganium', 'meowstic', 'milotic', 'murkrow', 'nidoran', 'numel', 'nuzleaf', 'octillery', 'pachirisu', 'pikachu', 'pikachu-starter', 'piloswine', 'politoed', 'protowatt', 'pyroar', 'quagsire', 'raticate', 'rattata', 'relicanth', 'rhydon', 'rhyperior', 'roselia', 'roserade', 'rotom', 'scizor', 'scyther', 'shiftry', 'shinx', 'sneasel', 'snover', 'staraptor', 'staravia', 'starly', 'steelix', 'sudowoodo', 'swalot', 'tangrowth', 'torchic', 'toxicroak', 'unfezant', 'unown', 'ursaring', 'voodoom', 'weavile', 'wobbuffet', 'wooper', 'xatu', 'zubat', ]; if (set.gender === 'F' && validFemale.includes(species.id)) spriteid += '-f'; return [ species.id, , ]; } static updateStats(pokemonIDs: Set) { for (const mon of pokemonIDs) { if (!wifiData.stats[mon]) wifiData.stats[mon] = []; wifiData.stats[mon].push(Date.now()); } saveData(); } // Wi-Fi uses special IV syntax to show hyper trained IVs static convertIVs(setObj: PokemonSet, ivs: string[]) { let set = Teams.exportSet(setObj); let ivsStr = ''; if (ivs.length) { const convertedIVs = { hp: '31', atk: '31', def: '31', spa: '31', spd: '31', spe: '31' }; for (const [i, iv] of ivs.entries()) { const numStr = iv.trim().split(' ')[0]; const statName = statNames[i]; convertedIVs[toID(statName) as StatID] = numStr; } const array = Object.keys(convertedIVs).map((x, i) => `${convertedIVs[x as StatID]} ${statNames[i]}`); ivsStr = `IVs: ${array.join(' / ')} `; } if (ivsStr) { if (/\nivs:/i.test(set)) { const arr = set.split('\n'); const index = arr.findIndex(x => /^ivs:/i.test(x)); arr[index] = ivsStr; set = arr.join('\n'); } else if (/nature\n/i.test(set)) { const arr = set.split('\n'); const index = arr.findIndex(x => /nature$/i.test(x)); arr.splice(index + 1, 0, ivsStr); set = arr.join('\n'); } else { set += `\n${ivsStr}`; } } return set; } generateWindow(rightSide: Chat.VNode | string): Chat.VNode { const set = Giveaway.convertIVs(this.prize, this.ivs); return

It's {this.game} giveaway time!

Giveaway started by {this.host.name} {!!this.extraInfo?.trim().length && }
Giver: {this.giver.name}
OT: {this.ot}, TID: {this.tid}
{this.sprite}
{set}
{rightSide}
Extra Information
{this.extraInfo.trim().replace(/
/g, '\n')}

Note: You must have a Switch, Pokémon {gameName[this.game]}, {} and NSO to receive the prize. {} Do not join if you are currently unable to trade. Do not enter if you have already won this exact Pokémon, {} unless it is explicitly allowed.

; } } export class QuestionGiveaway extends Giveaway { type: string; question: string; answers: string[]; /** userid: number of guesses */ answered: Utils.Multiset; winner: User | null; constructor( host: User, giver: User, room: Room, ot: string, tid: string, game: Game, ivs: string[], prize: PokemonSet, question: string, answers: string[], ball: string, extraInfo: string ) { super(host, giver, room, ot, tid, ivs, prize, game, ball, extraInfo); this.type = 'question'; this.phase = 'pending'; this.question = question; this.answers = QuestionGiveaway.sanitizeAnswers(answers); this.answered = new Utils.Multiset(); this.winner = null; this.send(this.generateWindow('The question will be displayed in one minute! Use /guess to answer.'), true); this.timer = setTimeout(() => this.start(), 1000 * 60); } static splitTarget( target: string, sep = '|', context: Chat.CommandContext, user: User, type: 'create' | 'store' | 'submit' ) { let [ giver, ot, tid, game, question, answers, ivs, ball, extraInfo, ...prize ] = target.split(sep).map(param => param.trim()); if (!(giver && ot && tid && prize?.length && question && answers?.split(',').length)) { context.parse(`/help giveaway`); throw new Chat.Interruption(); } const targetUser = Users.get(giver); if (!targetUser?.connected) throw new Chat.ErrorMessage(`User '${giver}' is not online.`); Giveaway.checkCanCreate(context, targetUser, type); if (!!ivs && ivs.split('/').length !== 6) { throw new Chat.ErrorMessage(`If you provide IVs, they must be provided for all stats.`); } if (!game) game = 'SV'; game = gameidToGame[toID(game)] || game as Game; if (!game || !['SV', 'BDSP', 'SwSh'].includes(game)) { throw new Chat.ErrorMessage(`The game must be "SV," "BDSP," or "SwSh".`); } if (!ball) ball = 'pokeball'; if (!toID(ball).endsWith('ball')) ball = toID(ball) + 'ball'; if (!Dex.items.get(ball).isPokeball) { throw new Chat.ErrorMessage(`${Dex.items.get(ball).name} is not a Pok\u00e9 Ball.`); } tid = toID(tid); if (isNaN(parseInt(tid)) || tid.length < 5 || tid.length > 6) throw new Chat.ErrorMessage("Invalid TID"); if (!targetUser.autoconfirmed) { throw new Chat.ErrorMessage(`User '${targetUser.name}' needs to be autoconfirmed to give something away.`); } if (Giveaway.checkBanned(context.room!, targetUser)) { throw new Chat.ErrorMessage(`User '${targetUser.name}' is giveaway banned.`); } return { targetUser, ot, tid, game: game as Game, question, answers: answers.split(','), ivs: ivs.split('/'), ball, extraInfo, prize: prize.join('|'), }; } generateQuestion() { return this.generateWindow(<>

Giveaway Question: {this.question}

use /guess to answer.

); } start() { this.changeUhtml(

The giveaway has started! Scroll down to see the question.

); this.phase = 'started'; this.send(this.generateQuestion()); this.timer = setTimeout(() => this.end(false), 1000 * 60 * 5); } choose(user: User, guess: string) { if (this.phase !== 'started') return user.sendTo(this.room, "The giveaway has not started yet."); if (this.checkJoined(user) && ![...this.joined.values()].includes(user.id)) { return user.sendTo(this.room, "You have already joined the giveaway."); } if (Giveaway.checkBanned(this.room, user)) return user.sendTo(this.room, "You are banned from entering giveaways."); if (this.checkExcluded(user)) return user.sendTo(this.room, "You are disallowed from entering the giveaway."); if (this.answered.get(user.id) >= 3) { return user.sendTo( this.room, "You have already guessed three times. You cannot guess anymore in this.giveaway." ); } const sanitized = toID(guess); for (const answer of this.answers.map(toID)) { if (answer === sanitized) { this.winner = user; this.clearTimer(); return this.end(false); } } this.joined.set(user.latestIp, user.id); this.answered.add(user.id); if (this.answered.get(user.id) >= 3) { user.sendTo( this.room, `Your guess '${guess}' is wrong. You have used up all of your guesses. Better luck next time!` ); } else { user.sendTo(this.room, `Your guess '${guess}' is wrong. Try again!`); } } change(value: string, user: User, answer = false) { if (user.id !== this.host.id) return user.sendTo(this.room, "Only the host can edit the giveaway."); if (this.phase !== 'pending') { return user.sendTo(this.room, "You cannot change the question or answer once the giveaway has started."); } if (!answer) { this.question = value; return user.sendTo(this.room, `The question has been changed to ${value}.`); } const ans = QuestionGiveaway.sanitizeAnswers(value.split(',').map(val => val.trim())); if (!ans.length) { return user.sendTo(this.room, "You must specify at least one answer and it must not contain any special characters."); } this.answers = ans; user.sendTo(this.room, `The answer${Chat.plural(ans, "s have", "has")} been changed to ${ans.join(', ')}.`); } end(force: boolean) { const style = { textAlign: 'center', fontSize: '13pt', fontWeight: 'bold' }; if (force) { this.clearTimer(); this.changeUhtml(

The giveaway was forcibly ended.

); this.room.send("The giveaway was forcibly ended."); } else { if (!this.winner) { this.changeUhtml(

The giveaway was forcibly ended.

); this.room.send("The giveaway has been forcibly ended as no one has answered the question."); } else { this.changeUhtml(

The giveaway has ended! Scroll down to see the answer.

); this.phase = 'ended'; this.clearTimer(); this.room.modlog({ action: 'GIVEAWAY WIN', userid: this.winner.id, note: `${this.giver.name}'s giveaway for a "${this.prize.species}" (OT: ${this.ot} TID: ${this.tid} Nature: ${this.prize.nature} Ball: ${this.ball}${this.extraInfo ? ` Other box info: ${this.extraInfo}` : ''})`, }); this.send(this.generateWindow(<>

{this.winner.name} won the giveaway! Congratulations!

{this.question}
Correct answer{Chat.plural(this.answers)}: {this.answers.join(', ')}

)); this.winner.sendTo( this.room, `|raw|You have won the giveaway. PM ${Utils.escapeHTML(this.giver.name)} to claim your prize!` ); if (this.winner.connected) { this.winner.popup(`You have won the giveaway. PM **${this.giver.name}** to claim your prize!`); } if (this.giver.connected) this.giver.popup(`${this.winner.name} has won your question giveaway!`); Giveaway.updateStats(new Set([this.pokemonID])); } } this.destroy(); } static sanitize(str: string) { return str.toLowerCase().replace(/[^a-z0-9 .-]+/ig, "").trim(); } static sanitizeAnswers(answers: string[]) { return answers.map( val => QuestionGiveaway.sanitize(val) ).filter( (val, index, array) => toID(val).length && array.indexOf(val) === index ); } checkExcluded(user: User) { if (user === this.host) return true; if (this.host.ips.includes(user.latestIp) && !Config.noipchecks) return true; if (this.host.previousIDs.includes(toID(user))) return true; return super.checkExcluded(user); } } export class LotteryGiveaway extends Giveaway { type: string; winners: User[]; maxWinners: number; constructor( host: User, giver: User, room: Room, ot: string, tid: string, ivs: string[], game: Game, prize: PokemonSet, winners: number, ball: string, extraInfo: string ) { super(host, giver, room, ot, tid, ivs, prize, game, ball, extraInfo); this.type = 'lottery'; this.phase = 'pending'; this.winners = []; this.maxWinners = winners || 1; this.send(this.generateReminder(false), true); this.timer = setTimeout(() => this.drawLottery(), 1000 * 60 * 2); } static splitTarget( target: string, sep = '|', context: Chat.CommandContext, user: User, type: 'create' | 'store' | 'submit' ) { let [giver, ot, tid, game, winners, ivs, ball, extraInfo, ...prize] = target.split(sep).map(param => param.trim()); if (!(giver && ot && tid && prize?.length)) { context.parse(`/help giveaway`); throw new Chat.Interruption(); } const targetUser = Users.get(giver); if (!targetUser?.connected) throw new Chat.ErrorMessage(`User '${giver}' is not online.`); Giveaway.checkCanCreate(context, user, type); if (!!ivs && ivs.split('/').length !== 6) { throw new Chat.ErrorMessage(`If you provide IVs, they must be provided for all stats.`); } if (!game) game = 'SV'; game = gameidToGame[toID(game)] || game as Game; if (!game || !['SV', 'BDSP', 'SwSh'].includes(game)) { throw new Chat.ErrorMessage(`The game must be "SV," "BDSP," or "SwSh".`); } if (!ball) ball = 'pokeball'; if (!toID(ball).endsWith('ball')) ball = toID(ball) + 'ball'; if (!Dex.items.get(ball).isPokeball) { throw new Chat.ErrorMessage(`${Dex.items.get(ball).name} is not a Pok\u00e9 Ball.`); } tid = toID(tid); if (isNaN(parseInt(tid)) || tid.length < 5 || tid.length > 6) throw new Chat.ErrorMessage("Invalid TID"); if (!targetUser.autoconfirmed) { throw new Chat.ErrorMessage(`User '${targetUser.name}' needs to be autoconfirmed to give something away.`); } if (Giveaway.checkBanned(context.room!, targetUser)) { throw new Chat.ErrorMessage(`User '${targetUser.name}' is giveaway banned.`); } let numWinners = 1; if (winners) { numWinners = parseInt(winners); if (isNaN(numWinners) || numWinners < 1 || numWinners > 5) { throw new Chat.ErrorMessage("The lottery giveaway can have a minimum of 1 and a maximum of 5 winners."); } } return { targetUser, ot, tid, game: game as Game, winners: numWinners, ivs: ivs.split('/'), ball, extraInfo, prize: prize.join('|'), }; } generateReminder(joined = false) { const cmd = (joined ? 'Leave' : 'Join'); return this.generateWindow(<> The lottery drawing will occur in 2 minutes, and with {Chat.count(this.maxWinners, "winners")}!
); } display() { const joined = this.generateReminder(true); const notJoined = this.generateReminder(); for (const i in this.room.users) { const thisUser = this.room.users[i]; if (this.checkJoined(thisUser)) { this.sendToUser(thisUser, joined); } else { this.sendToUser(thisUser, notJoined); } } } addUser(user: User) { if (this.phase !== 'pending') return user.sendTo(this.room, "The join phase of the lottery giveaway has ended."); if (!user.named) return user.sendTo(this.room, "You need to choose a name before joining a lottery giveaway."); if (this.checkJoined(user)) return user.sendTo(this.room, "You have already joined the giveaway."); if (Giveaway.checkBanned(this.room, user)) return user.sendTo(this.room, "You are banned from entering giveaways."); if (this.checkExcluded(user)) return user.sendTo(this.room, "You are disallowed from entering the giveaway."); this.joined.set(user.latestIp, user.id); this.sendToUser(user, this.generateReminder(true)); user.sendTo(this.room, "You have successfully joined the lottery giveaway."); } removeUser(user: User) { if (this.phase !== 'pending') return user.sendTo(this.room, "The join phase of the lottery giveaway has ended."); if (!this.checkJoined(user)) return user.sendTo(this.room, "You have not joined the lottery giveaway."); for (const [ip, id] of this.joined) { if (ip === user.latestIp && !Config.noipchecks || id === user.id) { this.joined.delete(ip); } } this.sendToUser(user, this.generateReminder(false)); user.sendTo(this.room, "You have left the lottery giveaway."); } drawLottery() { this.clearTimer(); const userlist = [...this.joined.values()]; if (userlist.length === 0) { this.changeUhtml(

The giveaway was forcibly ended.

); this.room.send("The giveaway has been forcibly ended as there are no participants."); return this.destroy(); } while (this.winners.length < this.maxWinners && userlist.length > 0) { const winner = Users.get(userlist.splice(Math.floor(Math.random() * userlist.length), 1)[0]); if (!winner) continue; this.winners.push(winner); } this.end(); } end(force = false) { const style = { textAlign: 'center', fontSize: '13pt', fontWeight: 'bold' }; if (force) { this.clearTimer(); this.changeUhtml(

The giveaway was forcibly ended.

); this.room.send("The giveaway was forcibly ended."); } else { this.changeUhtml(

The giveaway has ended! Scroll down to see the winner{Chat.plural(this.winners)}.

); this.phase = 'ended'; const winnerNames = this.winners.map(winner => winner.name).join(', '); this.room.modlog({ action: 'GIVEAWAY WIN', note: `${winnerNames} won ${this.giver.name}'s giveaway for "${this.prize.species}" (OT: ${this.ot} TID: ${this.tid} Nature: ${this.prize.nature} Ball: ${this.ball}${this.extraInfo ? ` Other box info: ${this.extraInfo}` : ''})`, }); this.send(this.generateWindow(<>

Lottery Draw

{Chat.count(this.joined.size, 'users')} joined the giveaway.
Our lucky winner{Chat.plural(this.winners)}: {winnerNames}!
Congratulations!

)); this.room.sendMods(`|c|~|Participants: ${[...this.joined.values()].join(', ')}`); for (const winner of this.winners) { winner.sendTo( this.room, `|raw|You have won the lottery giveaway! PM ${this.giver.name} to claim your prize!` ); if (winner.connected) { winner.popup(`You have won the lottery giveaway! PM **${this.giver.name}** to claim your prize!`); } } if (this.giver.connected) this.giver.popup(`The following users have won your lottery giveaway:\n${winnerNames}`); Giveaway.updateStats(new Set([this.pokemonID])); } this.destroy(); } } export class GTS extends Rooms.SimpleRoomGame { override readonly gameid = 'gts' as ID; gtsNumber: number; room: Room; giver: User; left: number; summary: string; deposit: string; lookfor: string; pokemonID: ID; sprite: Chat.VNode; sent: string[]; noDeposits: boolean; timer: NodeJS.Timeout | null; constructor( room: Room, giver: User, amount: number, summary: string, deposit: string, lookfor: string ) { // Always a sub-game so tours etc can be ran while GTS games are running super(room, true); this.gtsNumber = room.nextGameNumber(); this.room = room; this.giver = giver; this.left = amount; this.summary = summary; this.deposit = GTS.linkify(Utils.escapeHTML(deposit)); this.lookfor = lookfor; // Deprecated, just typed like this to prevent errors, will rewrite when GTS is planned to be used again [this.pokemonID, this.sprite] = Giveaway.getSprite({ species: summary } as PokemonSet); this.sent = []; this.noDeposits = false; this.timer = setInterval(() => this.send(this.generateWindow()), 1000 * 60 * 5); this.send(this.generateWindow()); } send(content: string) { this.room.add(Chat.html`|uhtml|gtsga${this.gtsNumber}|${
{content}
}`); this.room.update(); } changeUhtml(content: string) { this.room.uhtmlchange(`gtsga${this.gtsNumber}`, Chat.html`${
{content}
}`); this.room.update(); } clearTimer() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } } generateWindow() { const sentModifier = this.sent.length ? 5 : 0; const rightSide = this.noDeposits ? More Pokémon have been deposited than there are prizes in this giveaway and new deposits will not be accepted. {} If you have already deposited a Pokémon, please be patient, and do not withdraw your Pokémon. : <> To participate, deposit {this.deposit} into the GTS and look for {this.lookfor} ; return <>

There is a GTS giveaway going on!

Hosted by: {this.giver.name} | Left: {this.left}

{!!sentModifier && }
Last winners:
{this.sent.join(
)}
{this.sprite} {this.summary} {rightSide}
; } updateLeft(num: number) { this.left = num; if (this.left < 1) return this.end(); this.changeUhtml(this.generateWindow()); } updateSent(ign: string) { this.left--; if (this.left < 1) return this.end(); this.sent.push(ign); if (this.sent.length > 5) this.sent.shift(); this.changeUhtml(this.generateWindow()); } stopDeposits() { this.noDeposits = true; this.room.send(Chat.html`|html|${

More Pokémon have been deposited than there are prizes in this giveaway and new deposits will not be accepted. {} If you have already deposited a Pokémon, please be patient, and do not withdraw your Pokémon.

}`); this.changeUhtml(this.generateWindow()); } end(force = false) { if (force) { this.clearTimer(); this.changeUhtml(

The GTS giveaway was forcibly ended.

); this.room.send("The GTS giveaway was forcibly ended."); } else { this.clearTimer(); this.changeUhtml(

The GTS giveaway has finished.

); this.room.modlog({ action: 'GTS FINISHED', userid: this.giver.id, note: `their GTS giveaway for "${this.summary}"`, }); this.send(

The GTS giveaway for a "{this.lookfor}" has finished.

); Giveaway.updateStats(new Set([this.pokemonID])); } this.room.subGame = null; return this.left; } // This currently doesn't match some of the edge cases the other pokemon matching function does account for // (such as Type: Null). However, this should never be used as a fodder mon anyway, // so I don't see a huge need to implement it. static linkify(text: string) { const parsed = toID(text); for (const species of Dex.species.all()) { const id = species.id; const regexp = new RegExp(`\\b${id}\\b`, 'ig'); const res = regexp.exec(parsed); if (res) { const num = String(species.num).padStart(3, '0'); return <> {text.slice(0, res.index)} {text.slice(res.index, res.index + res[0].length)} {text.slice(res.index + res[0].length)} ; } } return text; } } function hasSubmittedGiveaway(user: User) { for (const [key, giveaways] of Object.entries(wifiData.submittedGiveaways)) { for (const [index, giveaway] of giveaways.entries()) { if (user.id === giveaway.targetUserID) { return { index, type: key as 'question' | 'lottery' }; } } } return null; } export const handlers: Chat.Handlers = { onDisconnect(user) { const giveaway = hasSubmittedGiveaway(user); if (giveaway) { wifiData.submittedGiveaways[giveaway.type].splice(giveaway.index, 1); saveData(); } }, onPunishUser(type, user, room) { const game = room?.getGame(LotteryGiveaway) || room?.getGame(QuestionGiveaway); if (game) { game.kickUser(user); } }, }; export const commands: Chat.ChatCommands = { gts: { new: 'start', create: 'start', start(target, room, user) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } if (room.getGame(GTS, true)) { throw new Chat.ErrorMessage(`There is already a GTS Giveaway going on.`); } // GTS is currently deprecated until it's no longer behind a paywall return this.parse(`/help gts`); /* const [giver, amountStr, summary, deposit, lookfor] = target.split(target.includes('|') ? '|' : ',').map( param => param.trim() ); if (!(giver && amountStr && summary && deposit && lookfor)) { return this.errorReply("Invalid arguments specified - /gts start giver | amount | summary | deposit | lookfor"); } const amount = parseInt(amountStr); if (!amount || amount < 20 || amount > 100) { return this.errorReply("Please enter a valid amount. For a GTS giveaway, you need to give away at least 20 mons, and no more than 100."); } const targetUser = Users.get(giver); if (!targetUser?.connected) return this.errorReply(`User '${giver}' is not online.`); this.checkCan('warn', null, room); if (!targetUser.autoconfirmed) { return this.errorReply(`User '${targetUser.name}' needs to be autoconfirmed to host a giveaway.`); } if (Giveaway.checkBanned(room, targetUser)) return this.errorReply(`User '${targetUser.name}' is giveaway banned.`); room.subGame = new GTS(room, targetUser, amount, summary, deposit, lookfor); this.privateModAction(`${user.name} started a GTS giveaway for ${targetUser.name} with ${amount} Pokémon`); this.modlog('GTS GIVEAWAY', null, `for ${targetUser.getLastId()} with ${amount} Pokémon`); */ }, left(target, room, user) { room = this.requireRoom('wifi' as RoomID); const game = this.requireGame(GTS, true); if (!user.can('warn', null, room) && user !== game.giver) { throw new Chat.ErrorMessage("Only the host or a staff member can update GTS giveaways."); } if (!target) { this.runBroadcast(); let output = `The GTS giveaway from ${game.giver} has ${game.left} Pokémon remaining!`; if (game.sent.length) output += `Last winners: ${game.sent.join(', ')}`; return this.sendReply(output); } const newamount = parseInt(target); if (isNaN(newamount)) return this.errorReply("Please enter a valid amount."); if (newamount > game.left) return this.errorReply("The new amount must be lower than the old amount."); if (newamount < game.left - 1) { this.modlog(`GTS GIVEAWAY`, null, `set from ${game.left} to ${newamount} left`); } game.updateLeft(newamount); }, sent(target, room, user) { room = this.requireRoom('wifi' as RoomID); const game = this.requireGame(GTS, true); if (!user.can('warn', null, room) && user !== game.giver) { return this.errorReply("Only the host or a staff member can update GTS giveaways."); } if (!target || target.length > 12) return this.errorReply("Please enter a valid IGN."); game.updateSent(target); }, full(target, room, user) { room = this.requireRoom('wifi' as RoomID); const game = this.requireGame(GTS, true); if (!user.can('warn', null, room) && user !== game.giver) { return this.errorReply("Only the host or a staff member can update GTS giveaways."); } if (game.noDeposits) return this.errorReply("The GTS giveaway was already set to not accept deposits."); game.stopDeposits(); }, end(target, room, user) { room = this.requireRoom('wifi' as RoomID); const game = this.requireGame(GTS, true); this.checkCan('warn', null, room); if (target && target.length > 300) { return this.errorReply("The reason is too long. It cannot exceed 300 characters."); } const amount = game.end(true); if (target) target = `: ${target}`; this.modlog('GTS END', null, `with ${amount} left${target}`); this.privateModAction(`The giveaway was forcibly ended by ${user.name} with ${amount} left${target}`); }, }, gtshelp: [ `GTS giveaways are currently disabled. If you are a Room Owner and would like them to be re-enabled, contact dhelmise.`, ], ga: 'giveaway', giveaway: { help: '', ''() { this.runBroadcast(); this.run('giveawayhelp'); }, view: { ''(target, room, user) { this.room = room = Rooms.search('wifi') || null; if (!room) throw new Chat.ErrorMessage(`The Wi-Fi room doesn't exist on this server.`); this.checkCan('warn', null, room); this.parse(`/j view-giveaways-default`); }, stored(target, room, user) { this.room = room = Rooms.search('wifi') || null; if (!room) throw new Chat.ErrorMessage(`The Wi-Fi room doesn't exist on this server.`); this.checkCan('warn', null, room); this.parse(`/j view-giveaways-stored`); }, submitted(target, room, user) { this.room = room = Rooms.search('wifi') || null; if (!room) throw new Chat.ErrorMessage(`The Wi-Fi room doesn't exist on this server.`); this.checkCan('warn', null, room); this.parse(`/j view-giveaways-submitted`); }, }, rm: 'remind', remind(target, room, user) { room = this.requireRoom('wifi' as RoomID); this.runBroadcast(); if (room.getGame(QuestionGiveaway)) { const game = room.getGame(QuestionGiveaway)!; if (game.phase !== 'started') { throw new Chat.ErrorMessage(`The giveaway has not started yet.`); } game.send(game.generateQuestion()); } else if (room.getGame(LotteryGiveaway)) { room.getGame(LotteryGiveaway)!.display(); } else { throw new Chat.ErrorMessage(`There is no giveaway going on right now.`); } }, leavelotto: 'join', leavelottery: 'join', leave: 'join', joinlotto: 'join', joinlottery: 'join', join(target, room, user, conn, cmd) { room = this.requireRoom('wifi' as RoomID); this.checkChat(); if (user.semilocked) return; const giveaway = this.requireGame(LotteryGiveaway); if (cmd.includes('join')) { giveaway.addUser(user); } else { giveaway.removeUser(user); } }, monthban: 'ban', permaban: 'ban', ban(target, room, user, connection, cmd) { if (!target) return false; room = this.requireRoom('wifi' as RoomID); this.checkCan('warn', null, room); const { targetUser, rest: reason } = this.requireUser(target, { allowOffline: true }); if (reason.length > 300) { return this.errorReply("The reason is too long. It cannot exceed 300 characters."); } if (Punishments.hasRoomPunishType(room, targetUser.name, 'GIVEAWAYBAN')) { return this.errorReply(`User '${targetUser.name}' is already giveawaybanned.`); } const duration = cmd === 'monthban' ? 30 * DAY : cmd === 'permaban' ? 3650 * DAY : 7 * DAY; Giveaway.ban(room, targetUser, reason, duration); (room.getGame(LotteryGiveaway) || room.getGame(QuestionGiveaway))?.kickUser(targetUser); const action = cmd === 'monthban' ? 'MONTHGIVEAWAYBAN' : cmd === 'permaban' ? 'PERMAGIVEAWAYBAN' : 'GIVEAWAYBAN'; this.modlog(action, targetUser, reason); const reasonMessage = reason ? ` (${reason})` : ``; const durationMsg = cmd === 'monthban' ? ' for a month' : cmd === 'permaban' ? ' permanently' : ''; this.privateModAction(`${targetUser.name} was banned from entering giveaways${durationMsg} by ${user.name}.${reasonMessage}`); }, unban(target, room, user) { if (!target) return false; room = this.requireRoom('wifi' as RoomID); this.checkCan('warn', null, room); const { targetUser } = this.requireUser(target, { allowOffline: true }); if (!Giveaway.checkBanned(room, targetUser)) { return this.errorReply(`User '${targetUser.name}' isn't banned from entering giveaways.`); } Giveaway.unban(room, targetUser); this.privateModAction(`${targetUser.name} was unbanned from entering giveaways by ${user.name}.`); this.modlog('GIVEAWAYUNBAN', targetUser, null, { noip: 1, noalts: 1 }); }, new: 'create', start: 'create', create: { ''(target, room, user) { room = this.requireRoom('wifi' as RoomID); if (!user.can('show', null, room)) this.checkCan('warn', null, room); this.parse('/j view-giveaways-create'); }, question(target, room, user) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } if (room.game) { throw new Chat.ErrorMessage(`There is already a room game (${room.game.constructor.name}) going on.`); } // Syntax: giver|ot|tid|game|question|answer1,answer2,etc|ivs/format/like/this|pokeball|packed set const { targetUser, ot, tid, game, question, answers, ivs, ball, extraInfo, prize, } = QuestionGiveaway.splitTarget(target, '|', this, user, 'create'); const set = Teams.import(prize)?.[0]; if (!set) throw new Chat.ErrorMessage(`Please submit the prize in the form of a PS set importable.`); room.game = new QuestionGiveaway(user, targetUser, room, ot, tid, game, ivs, set, question, answers, ball, extraInfo); this.privateModAction(`${user.name} started a question giveaway for ${targetUser.name}.`); this.modlog('QUESTION GIVEAWAY', null, `for ${targetUser.getLastId()} (OT: ${ot} TID: ${tid} Nature: ${(room.game as LotteryGiveaway).prize.nature} Ball: ${ball}${extraInfo ? ` Other box info: ${extraInfo}` : ''})`); }, lottery(target, room, user) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } if (room.game) throw new Chat.ErrorMessage(`There is already a room game (${room.game.constructor.name}) going on.`); // Syntax: giver|ot|tid|game|# of winners|ivs/like/this|pokeball|info|packed set const { targetUser, ot, tid, game, winners, ivs, ball, prize, extraInfo, } = LotteryGiveaway.splitTarget(target, '|', this, user, 'create'); const set = Teams.import(prize)?.[0]; if (!set) throw new Chat.ErrorMessage(`Please submit the prize in the form of a PS set importable.`); room.game = new LotteryGiveaway(user, targetUser, room, ot, tid, ivs, game, set, winners, ball, extraInfo); this.privateModAction(`${user.name} started a lottery giveaway for ${targetUser.name}.`); this.modlog('LOTTERY GIVEAWAY', null, `for ${targetUser.getLastId()} (OT: ${ot} TID: ${tid} Nature: ${(room.game as LotteryGiveaway).prize.nature} Ball: ${ball}${extraInfo ? ` Other box info: ${extraInfo}` : ''})`); }, }, stop: 'end', end(target, room, user) { room = this.requireRoom('wifi' as RoomID); if (!room.game?.constructor.name.includes('Giveaway')) { throw new Chat.ErrorMessage(`There is no giveaway going on at the moment.`); } const game = room.game as LotteryGiveaway | QuestionGiveaway; if (user.id !== game.host.id) this.checkCan('warn', null, room); if (target && target.length > 300) { return this.errorReply("The reason is too long. It cannot exceed 300 characters."); } game.end(true); this.modlog('GIVEAWAY END', null, target); if (target) target = `: ${target}`; this.privateModAction(`The giveaway was forcibly ended by ${user.name}${target}`); }, guess(target, room, user) { this.parse(`/guess ${target}`); }, changeanswer: 'changequestion', changequestion(target, room, user, connection, cmd) { room = this.requireRoom('wifi' as RoomID); const giveaway = this.requireGame(QuestionGiveaway); target = target.trim(); if (!target) throw new Chat.ErrorMessage("You must include a question or an answer."); giveaway.change(target, user, cmd.includes('answer')); }, showanswer: 'viewanswer', viewanswer(target, room, user) { room = this.requireRoom('wifi' as RoomID); const giveaway = this.requireGame(QuestionGiveaway); if (user.id !== giveaway.host.id && user.id !== giveaway.giver.id) return; this.sendReply(`The giveaway question is ${giveaway.question}.\nThe answer${Chat.plural(giveaway.answers, 's are', ' is')} ${giveaway.answers.join(', ')}.`); }, save: 'store', store: { ''(target, room, user) { room = this.requireRoom('wifi' as RoomID); this.checkCan('warn', null, room); this.parse('/j view-giveaways-stored-add'); }, question(target, room, user) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } const { targetUser, ot, tid, game, prize, question, answers, ball, extraInfo, ivs, } = QuestionGiveaway.splitTarget(target, '|', this, user, 'store'); const set = Teams.import(prize)?.[0]; if (!set) throw new Chat.ErrorMessage(`Please submit the prize in the form of a PS set importable.`); if (!wifiData.storedGiveaways.question) wifiData.storedGiveaways.question = []; const data = { targetUserID: targetUser.id, ot, tid, game, prize: set, question, answers, ivs, ball, extraInfo }; wifiData.storedGiveaways.question.push(data); saveData(); this.privateModAction(`${user.name} saved a question giveaway for ${targetUser.name}.`); this.modlog('QUESTION GIVEAWAY SAVE'); }, lottery(target, room, user) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } const { targetUser, ot, tid, game, prize, winners, ball, extraInfo, ivs, } = LotteryGiveaway.splitTarget(target, '|', this, user, 'store'); const set = Teams.import(prize)?.[0]; if (!set) throw new Chat.ErrorMessage(`Please submit the prize in the form of a PS set importable.`); if (!wifiData.storedGiveaways.lottery) wifiData.storedGiveaways.lottery = []; const data = { targetUserID: targetUser.id, ot, tid, game, prize: set, winners, ball, extraInfo, ivs }; wifiData.storedGiveaways.lottery.push(data); saveData(); this.privateModAction(`${user.name} saved a lottery giveaway for ${targetUser.name}.`); this.modlog('LOTTERY GIVEAWAY SAVE'); }, }, submit: { ''(target, room, user) { room = this.requireRoom('wifi' as RoomID); this.checkChat(); this.parse('/j view-giveaways-submitted-add'); }, question(target, room, user) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } const { targetUser, ot, tid, game, prize, question, answers, ball, extraInfo, ivs, } = QuestionGiveaway.splitTarget(target, '|', this, user, 'submit'); const set = Teams.import(prize)?.[0]; if (!set) throw new Chat.ErrorMessage(`Please submit the prize in the form of a PS set importable.`); if (!wifiData.submittedGiveaways.question) wifiData.submittedGiveaways.question = []; const data = { targetUserID: targetUser.id, ot, tid, game, prize: set, question, answers, ball, extraInfo, ivs }; wifiData.submittedGiveaways.question.push(data); saveData(); this.sendReply(`You have submitted a question giveaway for ${set.species}. If you log out or go offline, the giveaway won't go through.`); const message = `|tempnotify|pendingapprovals|Pending question giveaway request!` + `|${user.name} has requested to start a question giveaway for ${set.species}.|new question giveaway request`; room.sendRankedUsers(message, '%'); room.sendMods( Chat.html`|uhtml|giveaway-request-${user.id}|${
{user.name} wants to start a question giveaway for {set.species}
}` ); }, lottery(target, room, user) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } const { targetUser, ot, tid, game, prize, winners, ball, extraInfo, ivs, } = LotteryGiveaway.splitTarget(target, '|', this, user, 'submit'); const set = Teams.import(prize)?.[0]; if (!set) throw new Chat.ErrorMessage(`Please submit the prize in the form of a PS set importable.`); if (!wifiData.submittedGiveaways.lottery) wifiData.submittedGiveaways.lottery = []; const data = { targetUserID: targetUser.id, ot, tid, game, prize: set, winners, ball, extraInfo, ivs }; wifiData.submittedGiveaways.lottery.push(data); saveData(); this.sendReply(`You have submitted a lottery giveaway for ${set.species}. If you log out or go offline, the giveaway won't go through.`); const message = `|tempnotify|pendingapprovals|Pending lottery giveaway request!` + `|${user.name} has requested to start a lottery giveaway for ${set.species}.|new lottery giveaway request`; room.sendRankedUsers(message, '%'); room.sendMods(Chat.html`|uhtml|giveaway-request-${user.id}|${
{user.name} wants to start a lottery giveaway for {set.species}
}`); }, }, approve(target, room, user) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } const targetUser = Users.get(target); if (!targetUser?.connected) { this.refreshPage('giveaways-submitted'); throw new Chat.ErrorMessage(`${targetUser?.name || toID(target)} is offline, so their giveaway can't be run.`); } const hasGiveaway = hasSubmittedGiveaway(targetUser); if (!hasGiveaway) { this.refreshPage('giveaways-submitted'); throw new Chat.ErrorMessage(`${targetUser?.name || toID(target)} doesn't have any submitted giveaways.`); } const giveaway = wifiData.submittedGiveaways[hasGiveaway.type][hasGiveaway.index]; if (hasGiveaway.type === 'question') { const data = giveaway as QuestionGiveawayData; this.parse(`/giveaway create question ${data.targetUserID}|${data.ot}|${data.tid}|${data.game}|${data.question}|${data.answers.join(',')}|${data.ivs.join('/')}|${data.ball}|${data.extraInfo}|${Teams.pack([data.prize])}`); } else { const data = giveaway as LotteryGiveawayData; this.parse(`/giveaway create lottery ${data.targetUserID}|${data.ot}|${data.tid}|${data.game}|${data.winners}|${data.ivs.join('/')}|${data.ball}|${data.extraInfo}|${Teams.pack([data.prize])}`); } wifiData.submittedGiveaways[hasGiveaway.type].splice(hasGiveaway.index, 1); saveData(); this.refreshPage(`giveaways-submitted`); targetUser.send(`${user.name} has approved your ${hasGiveaway.type} giveaway!`); this.privateModAction(`${user.name} approved a ${hasGiveaway.type} giveaway by ${targetUser.name}.`); this.modlog(`GIVEAWAY APPROVE ${hasGiveaway.type.toUpperCase()}`, targetUser, null, { noalts: true, noip: true }); }, deny: 'delete', delete(target, room, user, connection, cmd) { room = this.room = Rooms.search('wifi') || null; if (!room) { throw new Chat.ErrorMessage(`This command must be used in the Wi-Fi room.`); } if (!target) return this.parse('/help giveaway'); const del = cmd === 'delete'; if (del) { const [type, indexStr] = target.split(','); const index = parseInt(indexStr) - 1; if (!type || !indexStr || index <= -1 || !['question', 'lottery'].includes(toID(type)) || isNaN(index)) { return this.parse(`/help giveaway`); } const typedType = toID(type) as 'question' | 'lottery'; const giveaway = wifiData.storedGiveaways[typedType][index]; if (!giveaway) { throw new Chat.ErrorMessage( `There is no giveaway at index ${index}. Indices must be integers between 0 and ${wifiData.storedGiveaways[typedType].length - 1}.` ); } wifiData.storedGiveaways[typedType].splice(index, 1); saveData(); this.privateModAction(`${user.name} deleted a ${typedType} giveaway by ${giveaway.targetUserID}.`); this.modlog(`GIVEAWAY DELETE ${typedType.toUpperCase()}`); } else { const { targetUser, rest: reason } = this.splitUser(target); if (!targetUser?.connected) { throw new Chat.ErrorMessage(`${targetUser?.name || toID(target)} is offline, so their giveaway can't be run.`); } const hasGiveaway = hasSubmittedGiveaway(targetUser); if (!hasGiveaway) { this.refreshPage('giveaways-submitted'); throw new Chat.ErrorMessage(`${targetUser?.name || toID(target)} doesn't have any submitted giveaways.`); } wifiData.submittedGiveaways[hasGiveaway.type].splice(hasGiveaway.index, 1); saveData(); targetUser?.send(`Staff have rejected your giveaway${reason ? `: ${reason}` : '.'}`); this.privateModAction(`${user.name} denied a ${hasGiveaway.type} giveaway by ${targetUser.name}.`); this.modlog(`GIVEAWAY DENY ${hasGiveaway.type.toUpperCase()}`, targetUser, reason || null, { noalts: true, noip: true }); } this.refreshPage(del ? `giveaways-stored` : 'giveaways-submitted'); }, unwhitelist: 'whitelist', whitelist(target, room, user, connection, cmd) { room = this.requireRoom('wifi' as RoomID); this.checkCan('warn', null, room); const targetId = toID(target); if (!targetId) return this.parse(`/help giveaway whitelist`); if (cmd.includes('un')) { const idx = wifiData.whitelist.indexOf(targetId); if (idx < 0) { return this.errorReply(`'${targetId}' is not whitelisted.`); } wifiData.whitelist.splice(idx, 1); this.privateModAction(`${user.name} removed '${targetId}' from the giveaway whitelist.`); this.modlog(`GIVEAWAY UNWHITELIST`, targetId); saveData(); } else { if (wifiData.whitelist.includes(targetId)) { return this.errorReply(`'${targetId}' is already whitelisted.`); } wifiData.whitelist.push(targetId); this.privateModAction(`${user.name} added ${targetId} to the giveaway whitelist.`); this.modlog(`GIVEAWAY WHITELIST`, targetId); saveData(); } }, whitelisthelp: [ `/giveaway whitelist [user] - Allow the given [user] to make giveaways without staff help. Requires: % @ # ~`, `/giveaway unwhitelist [user] - Remove the given user from the giveaway whitelist. Requires: % @ # ~`, ], whitelisted(target, room, user) { room = this.requireRoom('wifi' as RoomID); this.checkCan('warn', null, room); const buf = [Currently whitelisted users,
]; if (!wifiData.whitelist.length) { buf.push(
None.
); } else { buf.push(wifiData.whitelist.map(n => {n})); } this.sendReplyBox(<>{buf}); }, claim(target, room, user) { room = this.requireRoom('wifi' as RoomID); this.checkCan('mute', null, room); const { targetUser } = this.requireUser(target); const hasGiveaway = hasSubmittedGiveaway(targetUser); if (!hasGiveaway) { this.refreshPage('giveaways-submitted'); throw new Chat.ErrorMessage(`${targetUser?.name || toID(target)} doesn't have any submitted giveaways.`); } // we ensure it exists above const giveaway = wifiData.submittedGiveaways[hasGiveaway.type][hasGiveaway.index]; if (giveaway.claimed) throw new Chat.ErrorMessage(`That giveaway is already claimed by ${giveaway.claimed}.`); giveaway.claimed = user.id; Chat.refreshPageFor('giveaways-submitted', room); this.privateModAction(`${user.name} claimed ${targetUser.name}'s giveaway`); saveData(); }, unclaim(target, room, user) { room = this.requireRoom('wifi' as RoomID); this.checkCan('mute', null, room); const { targetUser } = this.requireUser(target); const hasGiveaway = hasSubmittedGiveaway(targetUser); if (!hasGiveaway) { this.refreshPage('giveaways-submitted'); throw new Chat.ErrorMessage(`${targetUser?.name || toID(target)} doesn't have any submitted giveaways.`); } // we ensure it exists above const giveaway = wifiData.submittedGiveaways[hasGiveaway.type][hasGiveaway.index]; if (!giveaway.claimed) throw new Chat.ErrorMessage(`That giveaway is not claimed.`); delete giveaway.claimed; Chat.refreshPageFor('giveaways-submitted', room); saveData(); }, count(target, room, user) { room = this.requireRoom('wifi' as RoomID); if (!Dex.species.get(target).exists) { throw new Chat.ErrorMessage(`No Pok\u00e9mon entered. Proper syntax: /giveaway count pokemon`); } target = Dex.species.get(target).id; this.runBroadcast(); const count = wifiData.stats[target]; if (!count) return this.sendReplyBox("This Pokémon has never been given away."); const recent = count.filter(val => val + RECENT_THRESHOLD > Date.now()).length; this.sendReplyBox(`This Pokémon has been given away ${Chat.count(count, "times")}, a total of ${Chat.count(recent, "times")} in the past month.`); }, }, giveawayhelp(target, room, user) { room = this.requireRoom('wifi' as RoomID); this.runBroadcast(); const buf = []; if (user.can('show', null, room)) { buf.push(
Staff commands /giveaway create - Pulls up a page to create a giveaway. Requires: + % @ # ~
/giveaway create question Giver | OT | TID | Game | Question | Answer 1, Answer 2, Answer 3 | IV/IV/IV/IV/IV/IV | Poké Ball | Extra Info | Prize - Start a new question giveaway (voices can only host their own). Requires: + % @ # ~
/giveaway create lottery Giver | OT | TID | Game | # of Winners | IV/IV/IV/IV/IV/IV | Poké Ball | Extra Info | Prize - Start a new lottery giveaway (voices can only host their own). Requires: + % @ # ~
/giveaway changequestion/changeanswer - Changes the question/answer of a question giveaway. Requires: Being giveaway host
/giveaway viewanswer - Shows the answer of a question giveaway. Requires: Being giveaway host/giver
/giveaway ban [user], [reason] - Temporarily bans [user] from entering giveaways. Requires: % @ # ~
/giveaway end - Forcibly ends the current giveaway. Requires: % @ # ~
/giveaway count [pokemon] - Shows how frequently a certain Pokémon has been given away.
/giveaway whitelist [user] - Allow the given [user] to make giveaways. Requires: % @ # ~
/giveaway unwhitelist [user] - Remove the given user from the giveaway whitelist. Requires: % @ # ~
); } // Giveaway stuff buf.push(
Giveaway participation commands /guess [target] - Guesses an answer for a question giveaway.
/giveaway submit - Allows users to submit giveaways. They must remain online after submitting for it to go through.
/giveaway viewanswer - Guesses an answer for a question giveaway. Requires: Giveaway host/giver
/giveaway remind - Shows the details of the current giveaway.
/giveaway join/leave - Joins/leaves a lottery giveaway.
); this.sendReplyBox(<>{buf}); }, }; function makePageHeader(user: User, pageid?: string) { const titles: { [k: string]: string } = { create: `Create`, stored: `View Stored`, 'stored-add': 'Store', submitted: `View Submitted`, 'submitted-add': `Submit`, }; const icons: Record = { create: , stored: , 'stored-add': , submitted: , 'submitted-add': , }; const buf = []; buf.push(); buf.push(

Wi-Fi Giveaways

); const urls = []; const room = Rooms.get('wifi')!; // we validate before using that wifi exists for (const i in titles) { if (urls.length) urls.push(' / '); if (!user.can('mute', null, room) && i !== 'submitted-add') { continue; } const title = titles[i]; const icon = icons[i]; if (pageid === i) { urls.push(<>{icon} {title}); } else { urls.push(<>{icon} {title}); } } buf.push(<>{[urls]},
); return
{buf}
; } function formatFakeButton(url: string, text: Chat.VNode): Chat.VNode { return {text}; } function generatePokeballDropdown() { const pokeballs = Dex.items.all().filter(item => item.isPokeball).sort((a, b) => a.num - b.num); const pokeballsObj = []; for (const pokeball of pokeballs) { pokeballsObj.push(); } return <>; } export const pages: Chat.PageTable = { giveaways: { ''() { this.title = `[Giveaways]`; if (!Rooms.search('wifi')) return

There is no Wi-Fi room on this server.

; this.checkCan('warn', null, Rooms.search('wifi')!); return
{makePageHeader(this.user)}
; }, create(args, user) { this.title = `[Create Giveaways]`; const wifi = Rooms.search('wifi'); if (!wifi) return

There is no Wi-Fi room on this server.

; if (!(user.can('show', null, wifi) || wifiData.whitelist.includes(user.id))) { this.checkCan('warn', null, wifi); } const [type] = args; return
{makePageHeader(this.user, 'create')}{(() => { if (!type || !['lottery', 'question'].includes(type)) { return

Pick a Giveaway type

{ formatFakeButton(`/view-giveaways-create-lottery`, <> Lottery) } | { formatFakeButton(`/view-giveaways-create-question`, <> Question) }
; } switch (type) { case 'lottery': return <>

Make a Lottery Giveaway







Game:



{generatePokeballDropdown()}









; case 'question': return <>

Make a Question Giveaway







Game:





{generatePokeballDropdown()}








; } })()}
; }, stored(args, user) { this.title = `[Stored Giveaways]`; if (!Rooms.search('wifi')) return

There is no Wi-Fi room on this server.

; this.checkCan('warn', null, Rooms.search('wifi')!); const [add, type] = args; const giveaways = [ ...(wifiData.storedGiveaways?.lottery || []), ...(wifiData.storedGiveaways?.question || []), ]; const adding = add === 'add'; if (!giveaways.length && !adding) { return
{makePageHeader(this.user, adding ? 'stored-add' : 'stored')}

There are no giveaways stored

; } return
{makePageHeader(this.user, adding ? 'stored-add' : 'stored')} {(() => { if (!adding) { const buf = []; for (let giveaway of giveaways) { if (wifiData.storedGiveaways.lottery.includes(giveaway as LotteryGiveawayData)) { giveaway = giveaway as LotteryGiveawayData; const targetUser = Users.get(giveaway.targetUserID); buf.push(

Lottery


Game: {gameName[giveaway.game]}
Giver: {giveaway.targetUserID}, {} OT: {giveaway.ot}, TID: {giveaway.tid}
# of winners: {giveaway.winners}
Poké Ball:
Prize {Giveaway.convertIVs(giveaway.prize, giveaway.ivs)}
{!!giveaway.extraInfo?.trim() && <>
Extra Info {giveaway.extraInfo.trim()}
}
{!targetUser?.connected ? ( ) : ( )}
); } else { giveaway = giveaway as QuestionGiveawayData; const targetUser = Users.get(giveaway.targetUserID); buf.push(

Lottery


Game: {gameName[giveaway.game]}
Giver: {giveaway.targetUserID}, {} OT: {giveaway.ot}, TID: {giveaway.tid}
Question: {giveaway.question}
Answer{Chat.plural(giveaway.answers.length, "s")}: {giveaway.answers.join(', ')}
Poké Ball:
Prize {Giveaway.convertIVs(giveaway.prize, giveaway.ivs)}
{!!giveaway.extraInfo?.trim() && <>
Extra Info {giveaway.extraInfo.trim()}
}
{!targetUser?.connected ? ( ) : ( )}
); } } return <>

Stored Giveaways

{buf}; } else { return <>

Store a Giveaway

{(() => { if (!type || !['question', 'lottery'].includes(type)) { return

Pick a giveaway type

{ formatFakeButton(`/view-giveaways-stored-add-lottery`, <> Lottery) } | { formatFakeButton(`/view-giveaways-stored-add-question`, <> Question) }
; } switch (type) { case 'lottery': return






Game:



{generatePokeballDropdown()}









; case 'question': return






Game:





{generatePokeballDropdown()}








; } })()} ; } })()}
; }, submitted(args, user) { this.title = `[Submitted Giveaways]`; if (!Rooms.search('wifi')) return

There is no Wi-Fi room on this server.

; const [add, type] = args; const adding = add === 'add'; if (!adding) this.checkCan('warn', null, Rooms.get('wifi')!); const giveaways = [ ...(wifiData.submittedGiveaways?.lottery || []), ...(wifiData.submittedGiveaways?.question || []), ]; if (!giveaways.length && !adding) { return
{makePageHeader(this.user, args[0] === 'add' ? 'submitted-add' : 'submitted')}

There are no submitted giveaways.

; } return
{makePageHeader(this.user, args[0] === 'add' ? 'submitted-add' : 'submitted')} {(() => { if (!adding) { const buf = []; for (let giveaway of giveaways) { const claimCmd = giveaway.claimed === user.id ? `/giveaway unclaim ${giveaway.targetUserID}` : `/giveaway claim ${giveaway.targetUserID}`; const claimedTitle = giveaway.claimed === user.id ? "Unclaim" : giveaway.claimed ? `Claimed by ${giveaway.claimed}` : `Claim`; const disabled = giveaway.claimed && giveaway.claimed !== user.id ? " disabled" : ""; buf.push(
{(() => { if (wifiData.submittedGiveaways.lottery.includes(giveaway as LotteryGiveawayData)) { giveaway = giveaway as LotteryGiveawayData; return <>

Lottery


Game: {gameName[giveaway.game]}, Giver: {giveaway.targetUserID}, {} OT: {giveaway.ot}, TID: {giveaway.tid}, {} # of winners: {giveaway.winners} {!!giveaway.claimed && <>
Claimed: {giveaway.claimed}}
Poké Ball:
Prize {Giveaway.convertIVs(giveaway.prize, giveaway.ivs)}
{!!giveaway.extraInfo?.trim() && <>
Extra Info {giveaway.extraInfo.trim()}
} ; } else { giveaway = giveaway as QuestionGiveawayData; return <>

Question


Game: {gameName[giveaway.game]}, Giver: {giveaway.targetUserID}, {} OT: {giveaway.ot}, TID: {giveaway.tid} {!!giveaway.claimed && <>
Claimed: {giveaway.claimed}}
Question: {giveaway.question}
Answer{Chat.plural(giveaway.answers.length, "s")}: {giveaway.answers.join(', ')}
Poké Ball:
Prize {Giveaway.convertIVs(giveaway.prize, giveaway.ivs)}
{!!giveaway.extraInfo?.trim() && <>
Extra Info {giveaway.extraInfo.trim()}
} ; } })()}
{!Users.get(giveaway.targetUserID)?.connected ? <> : <> }
); } return <>

Submitted Giveaways

{buf}; } else { return <>

Submit a Giveaway

{(() => { if (!type || !['question', 'lottery'].includes(type)) { return

Pick a giveaway type

{ formatFakeButton(`/view-giveaways-submitted-add-lottery`, <> Lottery) } | { formatFakeButton(`/view-giveaways-submitted-add-question`, <> Question) }
; } switch (type) { case 'lottery': return






Game:



{generatePokeballDropdown()}









; case 'question': return






Game:





{generatePokeballDropdown()}








; } })()} ; } })()}
; }, }, }; Chat.multiLinePattern.register(`/giveaway (create|new|start|store|submit|save) (question|lottery) `);