"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var scavengers_exports = {}; __export(scavengers_exports, { ScavengerHunt: () => ScavengerHunt, ScavengerHuntPlayer: () => ScavengerHuntPlayer, commands: () => commands, pages: () => pages }); module.exports = __toCommonJS(scavengers_exports); var import_lib = require("../../lib"); var import_scavenger_games = require("./scavenger-games"); /** * Scavengers Plugin * Pokemon Showdown - http://pokemonshowdown.com/ * * This is a game plugin to host scavenger games specifically in the Scavengers room, * where the players will race answer several hints. * * @license MIT license */ const RATED_TYPES = ["official", "regular", "mini"]; const DEFAULT_POINTS = { official: [20, 15, 10, 5, 1] }; const DEFAULT_BLITZ_POINTS = { official: 10 }; const DEFAULT_HOST_POINTS = 4; const DEFAULT_TIMER_DURATION = 120; const DATA_FILE = "config/chat-plugins/ScavMods.json"; const HOST_DATA_FILE = "config/chat-plugins/scavhostdata.json"; const PLAYER_DATA_FILE = "config/chat-plugins/scavplayerdata.json"; const DATABASE_FILE = "config/chat-plugins/scavhunts.json"; const ACCIDENTAL_LEAKS = /^((?:\s)?(?:\/{2,}|[^\w/]+)|\s\/)?(?:\s)?(?:s\W?cavenge|s\W?cav(?:engers)? guess|d\W?t|d\W?ata|d\W?etails|g\W?(?:uess)?|v)\b/i; const FILTER_LENIENCY = 7; const HISTORY_PERIOD = 6; const databaseContentsJSON = (0, import_lib.FS)(DATABASE_FILE).readIfExistsSync(); const scavengersData = databaseContentsJSON ? JSON.parse(databaseContentsJSON) : { recycledHunts: [] }; const SCAVENGER_ROOMID = "scavengers"; function getScavsRoom(room) { if (!room) return Rooms.get(SCAVENGER_ROOMID); if (room.roomid === SCAVENGER_ROOMID) return room; if (room.parent?.roomid === SCAVENGER_ROOMID) return room.parent; return null; } class Ladder { constructor(file) { this.file = file; this.data = {}; this.load(); } load() { const json = (0, import_lib.FS)(this.file).readIfExistsSync(); if (json) this.data = JSON.parse(json); } addPoints(name, aspect, points, noUpdate) { const userid = toID(name); if (!userid || userid === "constructor" || !points) return this; if (!this.data[userid]) this.data[userid] = { name }; if (!this.data[userid][aspect]) this.data[userid][aspect] = 0; this.data[userid][aspect] += points; if (!noUpdate) this.data[userid].name = name; return this; } reset() { this.data = {}; return this; } write() { (0, import_lib.FS)(this.file).writeUpdate(() => JSON.stringify(this.data)); } visualize(sortBy, userid) { return new Promise((resolve, reject) => { let lowestScore = Infinity; let lastPlacement = 1; const ladder = import_lib.Utils.sortBy( Object.entries(this.data).filter(([u, bit]) => sortBy in bit), ([u, bit]) => -bit[sortBy] ).map(([u, chunk], i) => { if (chunk[sortBy] !== lowestScore) { lowestScore = chunk[sortBy]; lastPlacement = i + 1; } return { rank: lastPlacement, ...chunk }; }); if (userid) { const rank = ladder.find((entry) => toID(entry.name) === userid); resolve(rank); } else { resolve(ladder); } }); } } class PlayerLadder extends Ladder { constructor(file) { super(file); } addPoints(name, aspect, points, noUpdate) { if (!aspect.startsWith("cumulative-")) { this.addPoints(name, `cumulative-${aspect}`, points, noUpdate); } const userid = toID(name); if (!userid || userid === "constructor" || !points) return this; if (!this.data[userid]) this.data[userid] = { name }; if (!this.data[userid][aspect]) this.data[userid][aspect] = 0; this.data[userid][aspect] += points; if (!noUpdate) this.data[userid].name = name; return this; } // add the different keys to the history - async for larger leaderboards // FIXME: this is not what "async" means softReset() { return new Promise((resolve, reject) => { for (const u in this.data) { const userData = this.data[u]; for (const a in userData) { if (/^(?:cumulative|history)-/i.test(a) || a === "name") continue; const historyKey = "history-" + a; if (!userData[historyKey]) userData[historyKey] = []; userData[historyKey].unshift(userData[a]); userData[historyKey] = userData[historyKey].slice(0, HISTORY_PERIOD); userData[a] = 0; if (!userData[historyKey].some((p) => !!p)) { delete userData[a]; delete userData[historyKey]; } } } resolve(); }); } hardReset() { this.data = {}; return this; } } const LeaderboardRoom = getScavsRoom(); const Leaderboard = LeaderboardRoom?.scavLeaderboard?.scavsLeaderboard || new Ladder(DATA_FILE); const HostLeaderboard = LeaderboardRoom?.scavLeaderboard?.scavsHostLeaderboard || new PlayerLadder(HOST_DATA_FILE); const PlayerLeaderboard = LeaderboardRoom?.scavLeaderboard?.scavsPlayerLeaderboard || new PlayerLadder(PLAYER_DATA_FILE); if (LeaderboardRoom) { if (!LeaderboardRoom.scavLeaderboard) LeaderboardRoom.scavLeaderboard = {}; LeaderboardRoom.scavLeaderboard.scavsLeaderboard = Leaderboard; LeaderboardRoom.scavLeaderboard.scavsHostLeaderboard = HostLeaderboard; LeaderboardRoom.scavLeaderboard.scavsPlayerLeaderboard = PlayerLeaderboard; } function formatQueue(queue, viewer, room, broadcasting) { const showStaff = viewer.can("mute", null, room) && !broadcasting; const queueDisabled = room.settings.scavSettings?.scavQueueDisabled; const timerDuration = room.settings.scavSettings?.defaultScavTimer || DEFAULT_TIMER_DURATION; let buffer; if (queue?.length) { buffer = queue.map((item, index) => { const removeButton = `<button name="send" value="/scav dequeue ${index}" style="color: red; background-color: transparent; border: none; padding: 1px;">[x]</button>`; const startButton = `<button name="send" value="/scav next ${index}" style="color: green; background-color: transparent; border: none; padding: 1px;">[start]</button>`; const unratedText = item.gameType === "unrated" ? '<span style="color: blue; font-style: italic">[Unrated]</span> ' : ""; const hosts = import_lib.Utils.escapeHTML(Chat.toListString(item.hosts.map((h) => h.name))); const queuedBy = item.hosts.every((h) => h.id !== item.staffHostId) ? ` / ${item.staffHostId}` : ""; let questions; if (!broadcasting && (item.hosts.some((h) => h.id === viewer.id) || viewer.id === item.staffHostId)) { questions = item.questions.map( (q, i) => { if (i % 2) { q = q; return import_lib.Utils.html`<span style="color: green"><em>[${q.join(" / ")}]</em></span><br />`; } else { q = q; return import_lib.Utils.escapeHTML(q); } } ).join(" "); } else { questions = `[${item.questions.length / 2} hidden questions]`; } return `<tr><td>${removeButton}${startButton} ${unratedText}${hosts}${queuedBy}</td><td>${questions}</td></tr>`; }).join(""); } else { buffer = `<tr><td colspan=3>The scavenger queue is currently empty.</td></tr>`; } let template = `<div class="ladder"><table style="width: 100%"><tr><th>By</th><th>Questions</th></tr>${showStaff ? buffer : buffer.replace(/<button.*?>.+?<\/button>/gi, "")}</table></div>`; if (showStaff) { template += `<table style="width: 100%"><tr><td style="text-align: left;">Auto Timer Duration: ${timerDuration} minutes</td><td>Auto Dequeue: <button class="button${!queueDisabled ? '" name="send" value="/scav disablequeue"' : ' disabled" style="font-weight:bold; color:#575757; font-weight:bold; background-color:#d3d3d3;"'}>OFF</button> <button class="button${queueDisabled ? '" name="send" value="/scav enablequeue"' : ' disabled" style="font-weight:bold; color:#575757; font-weight:bold; background-color:#d3d3d3;"'}>ON</button></td><td style="text-align: right;"><button class="button" name="send" value="/scav next 0">Start the next hunt</button></td></tr></table>`; } return template; } class ScavengerHuntDatabase { static getRecycledHuntFromDatabase() { return scavengersData.recycledHunts[Math.floor(Math.random() * scavengersData.recycledHunts.length)]; } static addRecycledHuntToDatabase(hosts, params) { const huntSchema = { hosts, questions: [] }; let questionSchema = { text: "", answers: [], hints: [] }; for (let i = 0; i < params.length; ++i) { if (i % 2 === 0) { const questionText = params[i]; questionSchema.text = questionText; } else { const answerText = params[i]; questionSchema.answers = answerText; huntSchema.questions.push(questionSchema); questionSchema = { text: "", answers: [], hints: [] }; } } scavengersData.recycledHunts.push(huntSchema); this.updateDatabaseOnDisk(); } static removeRecycledHuntFromDatabase(index) { scavengersData.recycledHunts.splice(index - 1, 1); this.updateDatabaseOnDisk(); } static addHintToRecycledHunt(huntNumber, questionNumber, hint) { scavengersData.recycledHunts[huntNumber - 1].questions[questionNumber - 1].hints.push(hint); this.updateDatabaseOnDisk(); } static removeHintToRecycledHunt(huntNumber, questionNumber, hintNumber) { scavengersData.recycledHunts[huntNumber - 1].questions[questionNumber - 1].hints.splice(hintNumber - 1); this.updateDatabaseOnDisk(); } static updateDatabaseOnDisk() { (0, import_lib.FS)(DATABASE_FILE).writeUpdate(() => JSON.stringify(scavengersData)); } static isEmpty() { return scavengersData.recycledHunts.length === 0; } static hasHunt(hunt_number) { return !isNaN(hunt_number) && hunt_number > 0 && hunt_number <= scavengersData.recycledHunts.length; } static getFullTextOfHunt(hunt) { return `${hunt.hosts.map((host) => host.name).join(",")} | ${hunt.questions.map((question) => `${question.text} | ${question.answers.join(";")}`).join(" | ")}`; } } class ScavengerHunt extends Rooms.RoomGame { // for purposes of adding new temporary properties for the purpose of twists. constructor(room, staffHost, hosts, gameType, questions, mod) { super(room); this.checkChat = true; this.allowRenames = true; this.gameType = gameType; this.playerCap = Infinity; this.joinedIps = []; this.startTime = Date.now(); this.questions = []; this.completed = []; this.leftHunt = {}; this.hosts = hosts; this.modsList = []; this.mods = {}; this.timer = null; this.timerEnd = null; this.staffHostId = staffHost.id; this.staffHostName = staffHost.name; this.cacheUserIps(staffHost); this.gameid = "scavengerhunt"; this.title = "Scavenger Hunt"; this.scavGame = true; if (this.room.scavgame) { this.loadMods(this.room.scavgame.mod); } if (mod) { this.loadMods(mod); } else if (this.gameType === "official" && this.room.settings.scavSettings?.officialtwist) { this.loadMod(this.room.settings.scavSettings?.officialtwist); } this.runEvent("Load"); this.onLoad(questions); this.runEvent("AfterLoad"); } loadMods(modInformation) { if (Array.isArray(modInformation)) { for (const mod of modInformation) { this.loadMod(mod); } } else { this.loadMod(modInformation); } } loadMod(modData) { let twist; if (typeof modData === "string") { const modId = toID(modData); if (!import_scavenger_games.ScavMods.twists[modId]) return this.announce(`Invalid mod. Starting the hunt without the mod ${modId}.`); twist = import_scavenger_games.ScavMods.twists[modId]; } else { twist = modData; } this.modsList.push(twist.id); for (const key in twist) { if (!key.startsWith("on")) continue; const priority = twist[key + "Priority"] || 0; if (!this.mods[key]) this.mods[key] = []; this.mods[key].push({ exec: twist[key], priority }); } if (twist.isGameMode) { this.announce(`This hunt is part of an ongoing ${twist.name}.`); } else { this.announce(`This hunt uses the twist ${twist.name}.`); } } // alert new users that are joining the room about the current hunt. onConnect(user, connection) { connection.sendTo(this.room, this.getCreationMessage()); this.runEvent("Connect", user, connection); } getCreationMessage(newHunt) { const message = this.runEvent("CreateCallback"); if (message) return message; const hosts = import_lib.Utils.escapeHTML(Chat.toListString(this.hosts.map((h) => h.name))); const staffHost = this.hosts.some((h) => h.id === this.staffHostId) ? `` : import_lib.Utils.html` by <em>${this.staffHostName}</em>`; const article = ["official", "unrated"].includes(this.gameType) && !newHunt ? "An" : "A"; const huntType = `${article} ${newHunt ? "new " : ""}${this.gameType}`; return `|raw|<div class="broadcast-blue"><strong>${huntType} scavenger hunt by <em>${hosts}</em> has been started${staffHost}.</strong><div style="border:1px solid #CCC;padding:4px 6px;margin:4px 1px"><strong><em>Hint #1:</em> ${Chat.formatText(this.questions[0].hint)}</strong></div>(To answer, use <kbd>/scavenge <em>ANSWER</em></kbd>)</div>`; } joinGame(user) { if (this.hosts.some((h) => h.id === user.id) || user.id === this.staffHostId) { return user.sendTo( this.room, "You cannot join your own hunt! If you wish to view your questions, use /viewhunt instead!" ); } if (!Config.noipchecks && user.ips.some((ip) => this.joinedIps.includes(ip))) { return user.sendTo(this.room, "You already have one alt in the hunt."); } if (this.runEvent("Join", user)) return false; if (this.addPlayer(user)) { this.cacheUserIps(user); delete this.leftHunt[user.id]; user.sendTo(this.room, "You joined the scavenger hunt! Use the command /scavenge to answer."); this.onSendQuestion(user); return true; } user.sendTo(this.room, "You have already joined the hunt."); return false; } cacheUserIps(user) { if (!("ips" in user)) return; for (const ip of user.ips) { this.joinedIps.push(ip); } } leaveGame(user) { const player = this.playerTable[user.id]; if (!player) return user.sendTo(this.room, "You have not joined the scavenger hunt."); if (player.completed) return user.sendTo(this.room, "You have already completed this scavenger hunt."); this.runEvent("Leave", player); this.joinedIps = this.joinedIps.filter((ip) => !player.joinIps.includes(ip)); this.removePlayer(user); this.leftHunt[user.id] = 1; user.sendTo(this.room, "You have left the scavenger hunt."); } // overwrite the default makePlayer so it makes a ScavengerHuntPlayer instead. makePlayer(user) { return new ScavengerHuntPlayer(user, this); } onLoad(q) { for (let i = 0; i < q.length; i += 2) { const hint = q[i]; const answer = q[i + 1]; this.questions.push({ hint, answer, spoilers: [] }); } const message = this.getCreationMessage(true); this.room.add(message).update(); } // returns whether or not the next action should be stopped runEvent(event_id, ...args) { const events = this.mods["on" + event_id]; if (!events) return; import_lib.Utils.sortBy(events, (event) => -event.priority); let result = void 0; for (const event of events) { const subResult = event.exec.call(this, ...args); if (subResult === true) return true; result = subResult; } return result === false ? true : result; } onEditQuestion(questionNumber, question_answer, value) { if (question_answer === "question") question_answer = "hint"; if (!["hint", "answer"].includes(question_answer)) return false; let answer = []; if (question_answer === "answer") { answer = value.split(";").map((p) => p.trim()); } if (!questionNumber || questionNumber < 1 || questionNumber > this.questions.length || !answer && !value) { return false; } questionNumber--; if (question_answer === "answer") { this.questions[questionNumber].answer = answer; } else { this.questions[questionNumber].hint = value; } this.announce(`The ${question_answer} for question ${questionNumber + 1} has been edited.`); if (question_answer === "hint") { for (const p in this.playerTable) { this.playerTable[p].onNotifyChange(questionNumber); } } return true; } setTimer(minutes) { if (this.timer) { clearTimeout(this.timer); this.timer = null; this.timerEnd = null; } if (minutes === 0) { return "off"; } if (minutes > 24 * 60) { throw new Chat.ErrorMessage(`Time limit must be under 24 hours (you asked for ${Chat.toDurationString(minutes * 6e4)}).`); } if (minutes && minutes > 0) { this.timer = setTimeout(() => this.onEnd(), minutes * 6e4); this.timerEnd = Date.now() + minutes * 6e4; } return minutes; } choose(user, originalValue) { if (!(user.id in this.playerTable)) { if (!this.joinGame(user)) return false; } const value = toID(originalValue); const player = this.playerTable[user.id]; if (this.runEvent("AnySubmit", player, value, originalValue)) return; if (player.completed) return false; this.validatePlayer(player); player.lastGuess = Date.now(); if (this.runEvent("Submit", player, value, originalValue)) return false; if (player.verifyAnswer(value)) { if (this.runEvent("CorrectAnswer", player, value)) return; player.sendRoom("Congratulations! You have gotten the correct answer."); player.currentQuestion++; if (player.currentQuestion === this.questions.length) { this.onComplete(player); } else { this.onSendQuestion(user); } } else { if (this.runEvent("IncorrectAnswer", player, value)) return; throw new Chat.ErrorMessage("That is not the answer - try again!"); } } getQuestion(question, showHints) { const current = { question: this.questions[question - 1], number: question }; const finalHint = current.number === this.questions.length ? "Final " : ""; return `|raw|<div class="ladder"><table><tr><td><strong style="white-space: nowrap">${finalHint}Hint #${current.number}:</strong></td><td>${Chat.formatText(current.question.hint) + (showHints && current.question.spoilers.length ? `<details><summary>Extra Hints:</summary>${current.question.spoilers.map((p) => `- ${p}`).join("<br />")}</details>` : ``)}</td></tr></table></div>`; } onSendQuestion(user, showHints) { if (!(user.id in this.playerTable) || this.hosts.some((h) => h.id === user.id)) return false; const player = this.playerTable[user.id]; if (player.completed) return false; if (this.runEvent("SendQuestion", player, showHints)) return; const questionDisplay = this.getQuestion(player.getCurrentQuestion().number, showHints); player.sendRoom(questionDisplay); return true; } onViewHunt(user) { if (this.runEvent("ViewHunt", user)) return; let qLimit = 1; if (this.hosts.some((h) => h.id === user.id) || user.id === this.staffHostId) { qLimit = this.questions.length + 1; } else if (user.id in this.playerTable) { const player = this.playerTable[user.id]; qLimit = player.currentQuestion + 1; } user.sendTo( this.room, `|raw|<div class="ladder"><table style="width: 100%"><tr><th style="width: 10%;">#</th><th>Hint</th><th>Answer</th></tr>` + this.questions.slice(0, qLimit).map((q, i) => `<tr><td>${i + 1}</td><td>${Chat.formatText(q.hint) + (q.spoilers.length ? `<details><summary>Extra Hints:</summary>${q.spoilers.map((s) => `- ${s}`).join("<br />")}</details>` : ``)}</td><td>${i + 1 >= qLimit ? `` : import_lib.Utils.escapeHTMLForceWrap(q.answer.join(" ; "))}</td></tr>`).join("") + `</table><div>` ); } onComplete(player) { if (player.completed) return false; const now = Date.now(); const time = Chat.toDurationString(now - this.startTime, { hhmmss: true }); const canBlitz = this.completed.length < 3; const blitz = now - this.startTime <= 6e4 && canBlitz && (this.room.settings.scavSettings?.blitzPoints?.[this.gameType] || DEFAULT_BLITZ_POINTS[this.gameType]); player.completed = true; let result = this.runEvent("Complete", player, time, blitz); if (result === true) return; result = result || { name: player.name, time, blitz }; this.completed.push(result); const place = import_lib.Utils.formatOrder(this.completed.length); const completionMessage = this.runEvent("ConfirmCompletion", player, time, blitz, place, result); this.announce( completionMessage || import_lib.Utils.html`<em>${result.name}</em> has finished the hunt in ${place} place! (${time}${blitz ? " - BLITZ" : ""})` ); player.destroy(); } onShowEndBoard(endedBy) { const sliceIndex = this.gameType === "official" ? 5 : 3; const hosts = Chat.toListString(this.hosts.map((h) => `<em>${import_lib.Utils.escapeHTML(h.name)}</em>`)); this.announce( `The ${this.gameType ? `${this.gameType} ` : ""}scavenger hunt by ${hosts} was ended ${endedBy ? "by " + import_lib.Utils.escapeHTML(endedBy.name) : "automatically"}.<br />${this.completed.slice(0, sliceIndex).map((p, i) => `${import_lib.Utils.formatOrder(i + 1)} place: <em>${import_lib.Utils.escapeHTML(p.name)}</em> <span style="color: lightgreen;">[${p.time}]</span>.<br />`).join("")}${this.completed.length > sliceIndex ? `Consolation Prize: ${this.completed.slice(sliceIndex).map((e) => `<em>${import_lib.Utils.escapeHTML(e.name)}</em> <span style="color: lightgreen;">[${e.time}]</span>`).join(", ")}<br />` : ""}<br /><details style="cursor: pointer;"><summary>Solution: </summary><br />${this.questions.map((q, i) => `${i + 1}) ${Chat.formatText(q.hint)} <span style="color: lightgreen">[<em>${import_lib.Utils.escapeHTML(q.answer.join(" / "))}</em>]</span>`).join("<br />")}</details>` ); } onEnd(reset, endedBy) { if (!endedBy && (this.preCompleted ? this.preCompleted.length : this.completed.length) === 0) { reset = true; } this.runEvent("End", reset); if (!ScavengerHuntDatabase.isEmpty() && this.room.settings.scavSettings?.addRecycledHuntsToQueueAutomatically) { if (!this.room.settings.scavQueue) this.room.settings.scavQueue = []; const next = ScavengerHuntDatabase.getRecycledHuntFromDatabase(); const correctlyFormattedQuestions = next.questions.flatMap((question) => [question.text, question.answers]); this.room.settings.scavQueue.push({ hosts: next.hosts, questions: correctlyFormattedQuestions, staffHostId: "scavengermanager", staffHostName: "Scavenger Manager", gameType: "unrated" }); } if (!reset) { if (!this.runEvent("ShowEndBoard", endedBy)) this.onShowEndBoard(endedBy); if (!this.runEvent("GivePoints")) { const winPoints = this.room.settings.scavSettings?.winPoints?.[this.gameType] || DEFAULT_POINTS[this.gameType]; const blitzPoints = this.room.settings.scavSettings?.blitzPoints?.[this.gameType] || DEFAULT_BLITZ_POINTS[this.gameType]; let hostPoints; if (this.gameType === "regular") { hostPoints = this.room.settings.scavSettings?.hostPoints ? this.room.settings.scavSettings?.hostPoints : DEFAULT_HOST_POINTS; } let didSomething = false; if (winPoints || blitzPoints) { for (const [i, completed] of this.completed.entries()) { if (!completed.blitz && i >= winPoints.length) break; const name = completed.name; if (winPoints[i]) Leaderboard.addPoints(name, "points", winPoints[i]); if (blitzPoints && completed.blitz) Leaderboard.addPoints(name, "points", blitzPoints); } didSomething = true; } if (hostPoints) { if (this.hosts.length === 1) { Leaderboard.addPoints(this.hosts[0].name, "points", hostPoints, this.hosts[0].noUpdate); didSomething = true; } else { this.room.sendMods("|notify|A scavenger hunt with multiple hosts needs points!"); this.room.sendMods("(A scavenger hunt with multiple hosts has ended.)"); } } if (didSomething) Leaderboard.write(); } this.onTallyLeaderboard(); this.tryRunQueue(this.room.roomid); } else if (endedBy) { this.announce(`The scavenger hunt has been reset by ${endedBy.name}.`); } else { this.announce("The hunt has been reset automatically, due to the lack of finishers."); this.tryRunQueue(this.room.roomid); } this.runEvent("AfterEnd", reset); this.destroy(); } onTallyLeaderboard() { for (const p in this.playerTable) { const player = this.playerTable[p]; PlayerLeaderboard.addPoints(player.name, "join", 1); if (player.completed) PlayerLeaderboard.addPoints(player.name, "finish", 1); } for (const id in this.leftHunt) { if (id in this.playerTable) continue; PlayerLeaderboard.addPoints(id, "join", 1, true); } if (this.gameType !== "practice") { for (const host of this.hosts) { HostLeaderboard.addPoints(host.name, "points", 1, host.noUpdate).write(); } } PlayerLeaderboard.write(); } tryRunQueue(roomid) { if (this.room.scavgame || this.room.settings.scavSettings?.scavQueueDisabled) { return; } if (this.room.settings.scavQueue && this.room.settings.scavQueue.length) { setTimeout(() => { const room = Rooms.get(roomid); if (!room || room.game || !room.settings.scavQueue?.length || room.settings.scavSettings?.scavQueueDisabled) return; const next = room.settings.scavQueue.shift(); const duration = room.settings.scavSettings?.defaultScavTimer || DEFAULT_TIMER_DURATION; room.game = new ScavengerHunt( room, { id: next.staffHostId, name: next.staffHostName }, next.hosts, next.gameType, next.questions ); const game = room.getGame(ScavengerHunt); if (game) { game.setTimer(duration); room.add(`|c|~|[ScavengerManager] A scavenger hunt by ${Chat.toListString(next.hosts.map((h) => h.name))} has been automatically started. It will automatically end in ${duration} minutes.`).update(); } room.saveSettings(); }, 2 * 6e4); } } // modify destroy to get rid of any timers in the current roomgame. destroy() { if (this.timer) { clearTimeout(this.timer); } for (const i in this.playerTable) { this.playerTable[i].destroy(); } this.room.game = null; } announce(msg) { this.room.add(`|raw|<div class="broadcast-blue"><strong>${msg}</strong></div>`).update(); } validatePlayer(player) { if (player.infracted) return false; if (this.hosts.some((h) => h.id === player.id) || player.id === this.staffHostId) { player.sendRoom("You have been caught for doing your own hunt; staff has been notified."); const staffMsg = `(${player.name} has been caught trying to do their own hunt.)`; this.room.sendMods(staffMsg); this.room.roomlog(staffMsg); this.room.modlog({ action: "SCAV CHEATER", userid: player.id, note: "caught trying to do their own hunt" }); PlayerLeaderboard.addPoints(player.name, "infraction", 1); player.infracted = true; } const uniqueConnections = this.getUniqueConnections(player.id); if (uniqueConnections > 1 && this.room.settings.scavSettings?.scavmod?.ipcheck) { player.sendRoom("You have been caught for attempting a hunt with multiple connections on your account. Staff has been notified."); const staffMsg = `(${player.name} has been caught attempting a hunt with ${uniqueConnections} connections on the account. The user has also been given 1 infraction point on the player leaderboard.)`; this.room.sendMods(staffMsg); this.room.roomlog(staffMsg); this.room.modlog({ action: "SCAV CHEATER", userid: player.id, note: `caught attempting a hunt with ${uniqueConnections} connections on the account; has also been given 1 infraction point on the player leaderboard` }); PlayerLeaderboard.addPoints(player.name, "infraction", 1); player.infracted = true; } } eliminate(userid) { if (!(userid in this.playerTable)) return false; const player = this.playerTable[userid]; if (player.completed) return true; player.destroy(); delete this.playerTable[userid]; return true; } onUpdateConnection() { } onChatMessage(msg) { let msgId = toID(msg); const commandMatch = ACCIDENTAL_LEAKS.exec(msg); if (commandMatch) msgId = msgId.slice(toID(commandMatch[0]).length); const filtered = this.questions.some((q) => q.answer.some((a) => { a = toID(a); const md = Math.ceil((a.length - 5) / FILTER_LENIENCY); if (import_lib.Utils.levenshtein(msgId, a, md) <= md) return true; return false; })); if (filtered) return "Please do not leak the answer. Use /scavenge [guess] to submit your guess instead."; return; } hasFinished(user) { return this.playerTable[user.id] && this.playerTable[user.id].completed; } getUniqueConnections(userid) { const user = Users.get(userid); if (!user) return 1; const ips = user.connections.map((c) => c.ip); return ips.filter((ip, index) => ips.indexOf(ip) === index).length; } static parseHosts(hostArray, room, allowOffline) { const hosts = []; for (const u of hostArray) { const id = toID(u); const user = Users.getExact(id); if (!allowOffline && (!user?.connected || !(user.id in room.users))) continue; if (!user) { hosts.push({ name: id, id, noUpdate: true }); continue; } hosts.push({ id: "" + user.id, name: "" + user.name }); } return hosts; } static parseQuestions(questionArray) { if (questionArray.length % 2 === 1) return { err: "Your final question is missing an answer" }; if (questionArray.length < 6) return { err: "You must have at least 3 hints and answers" }; const formattedQuestions = []; for (let [i, question] of questionArray.entries()) { if (i % 2) { const answers = question.split(";").map((p) => p.trim()); formattedQuestions[i] = answers; if (!answers.length || answers.some((a) => !toID(a))) { return { err: "Empty answer - only alphanumeric characters will count in answers." }; } } else { question = question.trim(); formattedQuestions[i] = question; if (!question) return { err: "Empty question." }; } } return { result: formattedQuestions }; } } class ScavengerHuntPlayer extends Rooms.RoomGamePlayer { // for purposes of adding new temporary properties for the purpose of twists. constructor(user, game) { super(user, game); this.joinIps = user.ips.slice(); this.currentQuestion = 0; this.completed = false; this.lastGuess = 0; } getCurrentQuestion() { return { question: this.game.questions[this.currentQuestion], number: this.currentQuestion + 1 }; } verifyAnswer(value) { const answer = this.getCurrentQuestion().question.answer; value = toID(value); return answer.some((a) => toID(a) === value); } onNotifyChange(num) { this.game.runEvent("NotifyChange", this, num); if (num === this.currentQuestion) { this.sendRoom(`|raw|<strong>The hint has been changed to:</strong> ${Chat.formatText(this.game.questions[num].hint)}`); } } destroy() { const user = Users.getExact(this.id); if (user) { user.games.delete(this.game.roomid); user.updateSearch(); } } } const ScavengerCommands = { /** * Player commands */ ""() { return this.parse("/join scavengers"); }, guess(target, room, user) { return this.parse(`/choose ${target}`); }, join(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply("There is no scavenger hunt currently running."); this.checkChat(); game.joinGame(user); }, leave(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply("There is no scavenger hunt currently running."); game.leaveGame(user); }, /** * Scavenger Games * -------------- * Individual game commands for each Scavenger Game */ game: "games", games: { /** * General game commands */ create: "start", new: "start", start(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); if (room.scavgame) return this.errorReply("There is already a scavenger game running."); if (room.getGame(ScavengerHunt)) { return this.errorReply("You cannot start a scavenger game where there is already a scavenger hunt in the room."); } target = toID(target); const game = import_scavenger_games.ScavMods.LoadGame(room, target); if (!game) return this.errorReply("Invalid game mode."); room.scavgame = game; this.privateModAction(`A ${game.name} has been created by ${user.name}.`); this.modlog("SCAVENGER", null, "ended the scavenger game"); game.announce(`A game of ${game.name} has been started!`); }, end(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); if (!room.scavgame) return this.errorReply(`There is no scavenger game currently running.`); this.privateModAction(`The ${room.scavgame.name} has been forcibly ended by ${user.name}.`); this.modlog("SCAVENGER", null, "ended the scavenger game"); room.scavgame.announce(`The ${room.scavgame.name} has been forcibly ended.`); room.scavgame.destroy(true); }, kick(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); if (!room.scavgame) return this.errorReply(`There is no scavenger game currently running.`); const targetId = toID(target); if (targetId === "constructor" || !targetId) return this.errorReply("Invalid player."); const success = room.scavgame.eliminate(targetId); if (success) { this.addModAction(`User '${targetId}' has been kicked from the ${room.scavgame.name}.`); this.modlog("SCAVENGERS", target, `kicked from the ${room.scavgame.name}`); const game = room.getGame(ScavengerHunt); if (game) { game.eliminate(targetId); } } else { this.errorReply(`Unable to kick user '${targetId}'.`); } }, points: "leaderboard", score: "leaderboard", scoreboard: "leaderboard", async leaderboard(target, room, user) { room = this.requireRoom(); if (!room.scavgame) return this.errorReply(`There is no scavenger game currently running.`); if (!room.scavgame.leaderboard) return this.errorReply("This scavenger game does not have a leaderboard."); if (!this.runBroadcast()) return false; const html = await room.scavgame.leaderboard.htmlLadder(); this.sendReply(`|raw|${html}`); }, async rank(target, room, user) { room = this.requireRoom(); if (!room.scavgame) return this.errorReply(`There is no scavenger game currently running.`); if (!room.scavgame.leaderboard) return this.errorReply("This scavenger game does not have a leaderboard."); if (!this.runBroadcast()) return false; const targetId = toID(target) || user.id; const rank = await room.scavgame.leaderboard.visualize("points", targetId); if (!rank) { this.sendReplyBox(`User '${targetId}' does not have any points on the scavenger games leaderboard.`); } else { this.sendReplyBox(import_lib.Utils.html`User '${rank.name}' is #${rank.rank} on the scavenger games leaderboard with ${rank.points} points.`); } } }, teamscavs: { addteam: "createteam", createteam(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); const game = room.scavgame; if (!game || game.id !== "teamscavs") return this.errorReply("There is currently no game of Team Scavs going on."); let [teamName, leader] = target.split(","); teamName = teamName.trim(); if (game.teams[teamName]) return this.errorReply(`The team ${teamName} already exists.`); const leaderUser = Users.get(leader); if (!leaderUser) return this.errorReply("The user you specified is currently not online"); if (game.getPlayerTeam(leaderUser)) return this.errorReply("The user is already a member of another team."); game.teams[teamName] = { name: teamName, answers: [], players: [leaderUser.id], question: 1, completed: false }; game.announce(import_lib.Utils.html`A new team "${teamName}" has been created with ${leaderUser.name} as the leader.`); }, deleteteam: "removeteam", removeteam(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); const game = room.scavgame; if (!game || game.id !== "teamscavs") return this.errorReply("There is currently no game of Team Scavs going on."); if (!game.teams[target]) return this.errorReply(`The team ${target} does not exist.`); delete game.teams[target]; game.announce(import_lib.Utils.html`The team "${target}" has been removed.`); }, addplayer(target, room, user) { room = this.requireRoom(); const game = room.scavgame; if (!game || game.id !== "teamscavs") return this.errorReply("There is currently no game of Team Scavs going on."); let userTeam; for (const teamID in game.teams) { const team = game.teams[teamID]; if (team.players[0] === user.id) { userTeam = team; break; } } if (!userTeam) return this.errorReply("You must be the leader of a team to add people into the team."); const targetUsers = target.split(",").map((id) => Users.getExact(id)).filter((u) => u?.connected); if (!targetUsers.length) return this.errorReply("Please select a user that is currently online."); const errors = []; for (const targetUser of targetUsers) { if (game.getPlayerTeam(targetUser)) errors.push(`${targetUser.name} is already in a team.`); } if (errors.length) return this.sendReplyBox(errors.join("<br />")); const playerIDs = targetUsers.map((u) => u.id); userTeam.players.push(...playerIDs); for (const targetUser of targetUsers) { targetUser.sendTo(room, `You have joined ${userTeam.name}.`); } game.announce(import_lib.Utils.html`${Chat.toListString(targetUsers.map((u) => u.name))} ${targetUsers.length > 1 ? "have" : "has"} been added into ${userTeam.name}.`); }, editplayers(target, room, user) { room = this.requireRoom(); const game = room.scavgame; if (!game || game.id !== "teamscavs") return this.errorReply("There is currently no game of Team Scavs going on."); this.checkCan("mute", null, room); const parts = target.split(","); const teamName = parts[0].trim(); const playerchanges = parts.slice(1); const team = game.teams[teamName]; if (!team) return this.errorReply("Invalid team."); for (const entry of playerchanges) { const userid = toID(entry); if (entry.trim().startsWith("-")) { if (!team.players.includes(userid)) { this.errorReply(`User "${userid}" is not in team "${team.name}."`); continue; } else if (team.players[0] === userid) { this.errorReply(`You cannot remove "${userid}", who is the leader of "${team.name}".`); continue; } team.players = team.players.filter((u) => u !== userid); game.announce(`${userid} was removed from "${team.name}."`); } else { const targetUser = Users.getExact(userid); if (!targetUser?.connected) { this.errorReply(`User "${userid}" is not currently online.`); continue; } const targetUserTeam = game.getPlayerTeam(targetUser); if (team.players.includes(userid)) { this.errorReply(`User "${userid}" is already part of "${team.name}."`); continue; } else if (targetUserTeam) { this.errorReply(`User "${userid}" is already part of another team - "${targetUserTeam.name}".`); continue; } team.players.push(userid); game.announce(`${targetUser.name} was added to "${team.name}."`); } } }, teams(target, room, user) { if (!this.runBroadcast()) return false; room = this.requireRoom(); const game = room.scavgame; if (!game || game.id !== "teamscavs") return this.errorReply("There is currently no game of Team Scavs going on."); const display = []; for (const teamID in game.teams) { const team = game.teams[teamID]; display.push(import_lib.Utils.html`<strong>${team.name}</strong> - <strong>${team.players[0]}</strong>${team.players.length > 1 ? ", " + team.players.slice(1).join(", ") : ""}`); } this.sendReplyBox(display.join("<br />")); }, guesses(target, room, user) { room = this.requireRoom(); const game = room.scavgame; if (!game || game.id !== "teamscavs") return this.errorReply("There is currently no game of Team Scavs going on."); const team = game.getPlayerTeam(user); if (!team) return this.errorReply("You are not currently part of this Team Scavs game."); this.sendReplyBox(import_lib.Utils.html`<strong>Question #${team.question} guesses:</strong> ${team.answers.sort().join(", ")}`); }, chat: "note", note(target, room, user) { room = this.requireRoom(); const game = room.scavgame; if (!game || game.id !== "teamscavs") return this.errorReply("There is currently no game of Team Scavs going on."); const team = game.getPlayerTeam(user); if (!team) return this.errorReply("You are not currently part of this Team Scavs game."); if (!target) return this.errorReply("Please include a message as the note."); game.teamAnnounce(user, import_lib.Utils.html`<strong> Note from ${user.name}:</strong> ${target}`); } }, teamscavshelp: [ "/tscav createteam [team name], [leader name] - creates a new team for the current Team Scavs game. (Requires: % @ * # &)", "/tscav deleteteam [team name] - deletes an existing team for the current Team Scavs game. (Requires: % @ * # &)", "/tscav addplayer [user] - allows a team leader to add a player onto their team.", "/tscav editplayers [team name], [added user | -removed user], [...] (use - preceding a user's name to remove a user) - Edits the players within an existing team. (Requires: % @ * # &)", "/tscav teams - views the list of teams and the players on each team.", "/tscav guesses - views the list of guesses already submitted by your team for the current question.", "/tscav chat [message] - adds a message that can be seen by all of your teammates in the Team Scavs game." ], /** * Creation / Moderation commands */ createtwist: "create", createtwistofficial: "create", createtwistmini: "create", createtwistpractice: "create", createtwistunrated: "create", createpractice: "create", createofficial: "create", createunrated: "create", createmini: "create", forcecreate: "create", forcecreateunrated: "create", createrecycled: "create", create(target, room, user, connection, cmd) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("Scavenger hunts can only be created in the scavengers room."); } this.checkCan("mute", null, room); if (room.game) return this.errorReply(`There is already a game in this room - ${room.game.title}.`); let gameType = "regular"; if (cmd.includes("practice")) { gameType = "practice"; } else if (cmd.includes("official")) { gameType = "official"; } else if (cmd.includes("mini")) { gameType = "mini"; } else if (cmd.includes("unrated")) { gameType = "unrated"; } else if (cmd.includes("recycled")) { gameType = "recycled"; } let mod; let questions = target; if (cmd.includes("twist")) { const twistparts = target.split("|"); questions = twistparts.slice(1).join("|"); mod = twistparts[0].split(","); } if (!cmd.includes("force") && ["regular", "unrated", "recycled"].includes(gameType) && !mod && room.settings.scavQueue && room.settings.scavQueue.length && !room.scavgame) { return this.errorReply(`There are currently hunts in the queue! If you would like to start the hunt anyways, use /forcestart${gameType === "regular" ? "hunt" : gameType}.`); } if (gameType === "recycled") { if (ScavengerHuntDatabase.isEmpty()) { return this.errorReply("There are no hunts in the database."); } let hunt; if (questions) { const huntNumber = parseInt(questions); if (!ScavengerHuntDatabase.hasHunt(huntNumber)) return this.errorReply("You specified an invalid hunt number."); hunt = scavengersData.recycledHunts[huntNumber - 1]; } else { hunt = ScavengerHuntDatabase.getRecycledHuntFromDatabase(); } questions = ScavengerHuntDatabase.getFullTextOfHunt(hunt); } let [hostsArray, ...params] = questions.split("|"); if (gameType === "recycled") { hostsArray += `,${user.name}`; } const hosts = ScavengerHunt.parseHosts( hostsArray.split(/[,;]/), room, gameType === "official" || gameType === "recycled" ); if (!hosts.length) { return this.errorReply("The user(s) you specified as the host is not online, or is not in the room."); } const res = ScavengerHunt.parseQuestions(params); if (res.err) return this.errorReply(res.err); room.game = new ScavengerHunt(room, user, hosts, gameType, res.result, mod); this.privateModAction(`A new scavenger hunt was created by ${user.name}.`); this.modlog("SCAV NEW", null, `${gameType.toUpperCase()}: creators - ${hosts.map((h) => h.id)}`); }, status(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); const elapsedMsg = Chat.toDurationString(Date.now() - game.startTime, { hhmmss: true }); const gameTypeMsg = game.gameType ? `<em>${game.gameType}</em> ` : ""; const hostersMsg = import_lib.Utils.escapeHTML(Chat.toListString(game.hosts.map((h) => h.name))); const hostMsg = game.hosts.some((h) => h.id === game.staffHostId) ? "" : import_lib.Utils.html` (started by - ${game.staffHostName})`; const finishers = import_lib.Utils.html`${game.completed.map((u) => u.name).join(", ")}`; let buffer = `<div class="infobox" style="margin-top: 0px;">The current ${gameTypeMsg}scavenger hunt by <em>${hostersMsg}${hostMsg}</em> has been up for: ${elapsedMsg}<br />${!game.timerEnd ? "The timer is currently off." : `The hunt ends in: ${Chat.toDurationString(game.timerEnd - Date.now(), { hhmmss: true })}`}<br />Completed (${game.completed.length}): ${finishers}</div>`; if (game.modsList.includes("timetrial")) { const finisher = game.completed.find((player) => player.id === user.id); const timeTrialMsg = finisher ? `You finished the hunt in: ${finisher.time}.` : game.startTimes?.[user.id] ? `You joined the hunt ${Chat.toDurationString(Date.now() - game.startTimes[user.id], { hhmmss: true })} ago.` : "You have not joined the hunt."; buffer = `<div class="infobox" style="margin-top: 0px;">The current ${gameTypeMsg}scavenger hunt by <em>${hostersMsg}${hostMsg}</em> has been up for: ${elapsedMsg}<br />${timeTrialMsg}<br />${!game.timerEnd ? "The timer is currently off." : `The hunt ends in: ${Chat.toDurationString(game.timerEnd - Date.now(), { hhmmss: true })}`}<br />Completed (${game.completed.length}): ${finishers}</div>`; } if (game.hosts.some((h) => h.id === user.id) || game.staffHostId === user.id) { let str = `<div class="ladder" style="overflow-y: scroll; max-height: 300px;"><table style="width: 100%"><th><b>Question</b></th><th><b>Users on this Question</b></th>`; for (let i = 0; i < game.questions.length; i++) { const questionNum = i + 1; const players = Object.values(game.playerTable).filter((player) => player.currentQuestion === i && !player.completed); if (!players.length) { str += `<tr><td>${questionNum}</td><td>None</td>`; } else { str += `<tr><td>${questionNum}</td><td>`; str += players.map( (pl) => pl.lastGuess > Date.now() - 1e3 * 300 ? import_lib.Utils.html`<strong>${pl.name}</strong>` : import_lib.Utils.escapeHTML(pl.name) ).join(", "); } } const completed = game.preCompleted ? game.preCompleted : game.completed; str += import_lib.Utils.html`<tr><td>Completed</td><td>${completed.length ? completed.map((pl) => pl.name).join(", ") : "None"}`; return this.sendReply(`|raw|${str}</table></div>${buffer}`); } this.sendReply(`|raw|${buffer}`); }, hint(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); if (!game.onSendQuestion(user, true)) this.errorReply("You are not currently participating in the hunt."); }, timer(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); const minutes = toID(target) === "off" ? 0 : parseFloat(target); if (isNaN(minutes) || minutes < 0 || minutes * 60 * 1e3 > Chat.MAX_TIMEOUT_DURATION) { throw new Chat.ErrorMessage(`You must specify a timer length that is a postive number.`); } const result = game.setTimer(minutes); const message = `The scavenger timer has been ${result === "off" ? "turned off" : `set to ${result} minutes`}`; room.add(message + "."); this.privateModAction(`${message} by ${user.name}.`); this.modlog("SCAV TIMER", null, result === "off" ? "OFF" : `${result} minutes`); }, inherit(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); if (game.staffHostId === user.id) return this.errorReply("You already have staff permissions for this hunt."); game.staffHostId = "" + user.id; game.staffHostName = "" + user.name; game.eliminate(user.id); game.cacheUserIps(user); this.privateModAction(`${user.name} has inherited staff permissions for the current hunt.`); this.modlog("SCAV INHERIT"); }, reset(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); game.onEnd(true, user); this.privateModAction(`${user.name} has reset the scavenger hunt.`); this.modlog("SCAV RESET"); }, resettoqueue(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); const hunt = { hosts: game.hosts, questions: [], staffHostId: game.staffHostId, staffHostName: game.StaffHostName, gameType: game.gameType }; for (const entry of game.questions) { hunt.questions.push(...[entry.hint, entry.answer]); } if (!room.settings.scavQueue) room.settings.scavQueue = []; room.settings.scavQueue.push(hunt); game.onEnd(true, user); this.privateModAction(`${user.name} has reset the scavenger hunt, and placed it in the queue.`); this.modlog("SCAV RESETTOQUEUE"); }, forceend: "end", end(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); if (!room.game && room.scavgame) return this.parse("/scav games end"); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); const completed = game.preCompleted ? game.preCompleted : game.completed; if (!this.cmd.includes("force")) { if (!completed.length) { return this.errorReply("No one has finished the hunt yet. Use /forceendhunt if you want to end the hunt and reveal the answers."); } } else if (completed.length) { return this.errorReply(`This hunt has ${Chat.count(completed, "finishers")}; use /endhunt`); } game.onEnd(false, user); this.privateModAction(`${user.name} has ended the scavenger hunt.`); this.modlog("SCAV END"); }, viewhunt(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); if (!("onViewHunt" in game)) return this.errorReply("There is currently no hunt to be viewed."); game.onViewHunt(user); }, edithunt(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); if ((!game.hosts.some((h) => h.id === user.id) || !user.can("show", null, room)) && game.staffHostId !== user.id) { return this.errorReply("You cannot edit the hints and answers if you are not the host."); } const [question, type, ...value] = target.split(","); if (!game.onEditQuestion(parseInt(question), toID(type), value.join(",").trim())) { return this.sendReply("/scavengers edithunt [question number], [hint | answer], [value] - edits the current scavenger hunt."); } }, addhint: "spoiler", spoiler(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); if ((!game.hosts.some((h) => h.id === user.id) || !user.can("show", null, room)) && game.staffHostId !== user.id) { return this.errorReply("You cannot add more hints if you are not the host."); } const parts = target.split(","); const question = parseInt(parts[0]) - 1; const hint = parts.slice(1).join(","); if (!game.questions[question]) return this.errorReply(`Invalid question number.`); if (!hint) return this.errorReply("The hint cannot be left empty."); game.questions[question].spoilers.push(hint); room.addByUser(user, `Question #${question + 1} hint - spoiler: ${hint}`); const playersOnQ = game.players.filter((player) => player.currentQuestion === question && !player.completed); const notif = `|notify|Scavenger hint for Q${question + 1}`; for (const player of playersOnQ) { const playerObj = Users.get(player.id); if (!playerObj?.connected) continue; room.sendUser(playerObj, notif); } }, deletehint: "removehint", removehint(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); if ((!game.hosts.some((h) => h.id === user.id) || !user.can("show", null, room)) && game.staffHostId !== user.id) { return this.errorReply("You cannot remove hints if you are not the host."); } const parts = target.split(","); const question = parseInt(parts[0]) - 1; const hint = parseInt(parts[1]) - 1; if (!game.questions[question]) return this.errorReply(`Invalid question number.`); if (!game.questions[question].spoilers[hint]) return this.errorReply("Invalid hint number."); game.questions[question].spoilers.splice(hint, 1); return this.sendReply("Hint has been removed."); }, modifyhint: "edithint", edithint(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); if ((!game.hosts.some((h) => h.id === user.id) || !user.can("show", null, room)) && game.staffHostId !== user.id) { return this.errorReply("You cannot edit hints if you are not the host."); } const parts = target.split(","); const question = parseInt(parts[0]) - 1; const hint = parseInt(parts[1]) - 1; const value = parts.slice(2).join(","); if (!game.questions[question]) return this.errorReply(`Invalid question number.`); if (!game.questions[question].spoilers[hint]) return this.errorReply("Invalid hint number."); if (!value) return this.errorReply("The hint cannot be left empty."); game.questions[question].spoilers[hint] = value; room.addByUser(user, `Question #${question + 1} hint - spoiler: ${value}`); const playersOnQ = game.players.filter((player) => player.currentQuestion === question && !player.completed); const notif = `|notify|Scavenger hint for Q${question + 1}`; for (const player of playersOnQ) { const playerObj = Users.get(player.id); if (!playerObj?.connected) continue; room.sendUser(playerObj, notif); } return this.sendReply("Hint has been modified."); }, kick(target, room, user) { room = this.requireRoom(); const game = room.getGame(ScavengerHunt); if (!game) return this.errorReply(`There is no scavenger hunt currently running.`); const targetId = toID(target); if (targetId === "constructor" || !targetId) return this.errorReply("Invalid player."); const success = game.eliminate(targetId); if (success) { this.modlog("SCAV KICK", targetId); return this.privateModAction(`${user.name} has kicked '${targetId}' from the scavenger hunt.`); } this.errorReply(`Unable to kick '${targetId}' from the scavenger hunt.`); }, /** * Hunt queuing */ queueunrated: "queue", queuerated: "queue", queuerecycled: "queue", queue(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } if (!target && this.cmd !== "queuerecycled") { if (this.cmd === "queue") { this.runBroadcast(); const commandHandler = ScavengerCommands.viewqueue; commandHandler.call(this, target, room, user, this.connection, this.cmd, this.message); return; } return this.parse("/scavhelp staff"); } this.checkCan("mute", null, room); if (this.cmd === "queuerecycled") { if (ScavengerHuntDatabase.isEmpty()) { return this.errorReply(`There are no hunts in the database.`); } if (!room.settings.scavQueue) { room.settings.scavQueue = []; } let next; if (target) { const huntNumber = parseInt(target); if (!ScavengerHuntDatabase.hasHunt(huntNumber)) return this.errorReply("You specified an invalid hunt number."); next = scavengersData.recycledHunts[huntNumber - 1]; } else { next = ScavengerHuntDatabase.getRecycledHuntFromDatabase(); } const correctlyFormattedQuestions = next.questions.flatMap((question) => [question.text, question.answers]); room.settings.scavQueue.push({ hosts: next.hosts, questions: correctlyFormattedQuestions, staffHostId: "scavengermanager", staffHostName: "Scavenger Manager", gameType: "unrated" }); } else { const [hostsArray, ...params] = target.split("|"); const hosts = ScavengerHunt.parseHosts(hostsArray.split(/[,;]/), room); if (!hosts.length) { return this.errorReply("The user(s) you specified as the host is not online, or is not in the room."); } const results = ScavengerHunt.parseQuestions(params); if (results.err) return this.errorReply(results.err); if (!room.settings.scavQueue) room.settings.scavQueue = []; room.settings.scavQueue.push({ hosts, questions: results.result, staffHostId: user.id, staffHostName: user.name, gameType: this.cmd.includes("unrated") ? "unrated" : "regular" }); } this.privateModAction(`${user.name} has added a scavenger hunt to the queue.`); room.saveSettings(); }, dequeue(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } this.checkCan("mute", null, room); const id = parseInt(target); if (!room.settings.scavQueue || isNaN(id) || id < 0 || id >= room.settings.scavQueue.length) return false; const removed = room.settings.scavQueue.splice(id, 1)[0]; this.privateModAction(`${user.name} has removed a scavenger hunt created by [${removed.hosts.map((u) => u.id).join(", ")}] from the queue.`); this.sendReply(`|uhtmlchange|scav-queue|${formatQueue(room.settings.scavQueue, user, room)}`); room.saveSettings(); }, viewqueue(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } if (!this.runBroadcast()) return false; this.sendReply(`|uhtml|scav-queue|${formatQueue(room.settings.scavQueue, user, room, this.broadcasting)}`); }, next(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } this.checkCan("mute", null, room); if (!room.settings.scavQueue?.length) { return this.errorReply("The scavenger hunt queue is currently empty."); } if (room.game) return this.errorReply(`There is already a game in this room - ${room.game.title}.`); const huntId = parseInt(target) || 0; if (!room.settings.scavQueue[huntId]) return false; const next = room.settings.scavQueue.splice(huntId, 1)[0]; room.game = new ScavengerHunt( room, { id: next.staffHostId, name: next.staffHostName }, next.hosts, next.gameType, next.questions ); if (huntId) this.sendReply(`|uhtmlchange|scav-queue|${formatQueue(room.settings.scavQueue, user, room)}`); this.modlog("SCAV NEW", null, `from queue: creators - ${next.hosts.map((h) => h.id)}`); room.saveSettings(); }, enablequeue: "disablequeue", disablequeue(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } this.checkCan("mute", null, room); if (!room.settings.scavSettings) room.settings.scavSettings = {}; const state = this.cmd === "disablequeue"; if ((room.settings.scavSettings.scavQueueDisabled || false) === state) { return this.errorReply(`The queue is already ${state ? "disabled" : "enabled"}.`); } room.settings.scavSettings.scavQueueDisabled = state; room.saveSettings(); this.sendReply(`|uhtmlchange|scav-queue|${formatQueue(room.settings.scavQueue, user, room)}`); this.privateModAction(`The queue has been ${state ? "disabled" : "enabled"} by ${user.name}.`); this.modlog("SCAV QUEUE", null, state ? "disabled" : "enabled"); }, defaulttimer(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } this.checkCan("declare", null, room); if (!room.settings.scavSettings) room.settings.scavSettings = {}; if (!target) { const duration_string = room.settings.scavSettings.defaultScavTimer || DEFAULT_TIMER_DURATION; return this.sendReply(`The default scavenger timer is currently set at: ${duration_string} minutes.`); } const duration = parseInt(target); if (!duration || duration < 0) { return this.errorReply("The default timer must be an integer greater than zero, in minutes."); } room.settings.scavSettings.defaultScavTimer = duration; room.saveSettings(); this.privateModAction(`The default scavenger timer has been set to ${duration} minutes by ${user.name}.`); this.modlog("SCAV DEFAULT TIMER", null, `${duration} minutes`); }, /** * Leaderboard Commands */ addpoints(target, room, user) { room = this.requireRoom("scavengers"); this.checkCan("mute", null, room); const parts = target.split(","); const targetId = toID(parts[0]); const points = parseInt(parts[1]); if (!targetId || targetId === "constructor" || targetId.length > 18) return this.errorReply("Invalid username."); if (!points || points < 0 || points > 1e3) return this.errorReply("Points must be an integer between 1 and 1000."); Leaderboard.addPoints(targetId, "points", points, true).write(); this.privateModAction(`${targetId} was given ${points} points on the current scavengers ladder by ${user.name}.`); this.modlog("SCAV ADDPOINTS", targetId, "" + points); }, removepoints(target, room, user) { room = this.requireRoom("scavengers"); this.checkCan("mute", null, room); const parts = target.split(","); const targetId = toID(parts[0]); const points = parseInt(parts[1]); if (!targetId || targetId === "constructor" || targetId.length > 18) return this.errorReply("Invalid username."); if (!points || points < 0 || points > 1e3) return this.errorReply("Points must be an integer between 1 and 1000."); Leaderboard.addPoints(targetId, "points", -points, true).write(); this.privateModAction(`${user.name} has taken ${points} points from ${targetId} on the current scavengers ladder.`); this.modlog("SCAV REMOVEPOINTS", targetId, "" + points); }, resetladder(target, room, user) { room = this.requireRoom("scavengers"); this.checkCan("declare", null, room); Leaderboard.reset().write(); this.privateModAction(`${user.name} has reset the current scavengers ladder.`); this.modlog("SCAV RESETLADDER"); }, top: "ladder", async ladder(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } if (!this.runBroadcast()) return false; const isChange = !this.broadcasting && target; const hideStaff = !this.broadcasting && this.meansNo(target); const ladder = await Leaderboard.visualize("points"); this.sendReply( `|uhtml${isChange ? "change" : ""}|scavladder|<div class="ladder" style="overflow-y: scroll; max-height: 300px;"><table style="width: 100%"><tr><th>Rank</th><th>Name</th><th>Points</th></tr>${ladder.map((entry) => { const roomRank = room.auth.getDirect(toID(entry.name)); const isStaff = Users.Auth.atLeast(roomRank, "+"); if (isStaff && hideStaff) return ""; return `<tr><td>${entry.rank}</td><td>${isStaff ? `<em>${import_lib.Utils.escapeHTML(entry.name)}</em>` : entry.rank <= 5 ? `<strong>${import_lib.Utils.escapeHTML(entry.name)}</strong>` : import_lib.Utils.escapeHTML(entry.name)}</td><td>${entry.points}</td></tr>`; }).join("")}</table></div><div style="text-align: center"><button class="button" name="send" value="/scav top ${hideStaff ? "yes" : "no"}">${hideStaff ? "Show" : "Hide"} Auth</button></div>` ); }, async rank(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } if (!this.runBroadcast()) return false; const targetId = toID(target) || user.id; const rank = await Leaderboard.visualize("points", targetId); if (!rank) { this.sendReplyBox(`User '${targetId}' does not have any points on the scavengers leaderboard.`); } else { this.sendReplyBox(import_lib.Utils.html`User '${rank.name}' is #${rank.rank} on the scavengers leaderboard with ${rank.points} points.`); } }, /** * Leaderboard Point Distribution Editing */ setblitz(target, room, user) { room = this.requireRoom(); const scavsRoom = getScavsRoom(room); if (!scavsRoom) { return this.errorReply("This command can only be used in the scavengers room."); } this.checkCan("mute", null, room); if (!room.settings.scavSettings) room.settings.scavSettings = {}; if (!target) { const points = []; const source = Object.entries(Object.assign(DEFAULT_BLITZ_POINTS, room.settings.scavSettings.blitzPoints || {})); for (const entry of source) { points.push(`${entry[0]}: ${entry[1]}`); } return this.sendReplyBox(`The points rewarded for winning hunts within a minute is:<br />${points.join("<br />")}`); } this.checkCan("declare", null, room); const parts = target.split(","); const blitzPoints = parseInt(parts[1]); const gameType = toID(parts[0]); if (!RATED_TYPES.includes(gameType)) return this.errorReply(`You cannot set blitz points for ${gameType} hunts.`); if (isNaN(blitzPoints) || blitzPoints < 0 || blitzPoints > 1e3) { return this.errorReply("The points value awarded for blitz must be an integer bewteen 0 and 1000."); } if (!room.settings.scavSettings.blitzPoints) room.settings.scavSettings.blitzPoints = {}; room.settings.scavSettings.blitzPoints[gameType] = blitzPoints; room.saveSettings(); this.privateModAction(`${user.name} has set the points awarded for blitz for ${gameType} hunts to ${blitzPoints}.`); this.modlog("SCAV BLITZ", null, `${gameType}: ${blitzPoints}`); if (room.parent && !room.persist && scavsRoom) { scavsRoom.modlog({ action: "SCAV BLITZ", loggedBy: user.id, note: `${gameType}: ${blitzPoints}` }); scavsRoom.sendMods(`(${user.name} has set the points awarded for blitz for ${gameType} hunts to ${blitzPoints} in <<${room.roomid}>>.)`); scavsRoom.roomlog(`(${user.name} has set the points awarded for blitz for ${gameType} hunts to ${blitzPoints} in <<${room.roomid}>>.)`); } }, sethostpoints(target, room, user) { room = this.requireRoom(); const scavsRoom = getScavsRoom(room); if (!scavsRoom) { return this.errorReply("This command can only be used in the scavengers room."); } this.checkCan("mute", null, room); if (!room.settings.scavSettings) room.settings.scavSettings = {}; if (!target) { const pointSetting = Object.hasOwnProperty.call(room.settings.scavSettings, "hostPoints") ? room.settings.scavSettings.hostPoints : DEFAULT_HOST_POINTS; return this.sendReply(`The points rewarded for hosting a regular hunt is ${pointSetting}.`); } this.checkCan("declare", null, room); const points = parseInt(target); if (isNaN(points)) return this.errorReply(`${target} is not a valid number of points.`); room.settings.scavSettings.hostPoints = points; room.saveSettings(); this.privateModAction(`${user.name} has set the points awarded for hosting regular scavenger hunts to ${points}`); this.modlog("SCAV SETHOSTPOINTS", null, `${points}`); if (room.parent && !room.persist) { scavsRoom.modlog({ action: "SCAV SETHOSTPOINTS", loggedBy: user.id, note: `${points} [room: ${room.roomid}]` }); scavsRoom.sendMods(`(${user.name} has set the points awarded for hosting regular scavenger hunts to - ${points} in <<${room.roomid}>>)`); scavsRoom.roomlog(`(${user.name} has set the points awarded for hosting regular scavenger hunts to - ${points} in <<${room.roomid}>>)`); } }, setpoints(target, room, user) { room = this.requireRoom(); const scavsRoom = getScavsRoom(room); if (!scavsRoom) { return this.errorReply("This command can only be used in the scavengers room."); } this.checkCan("mute", null, room); if (!room.settings.scavSettings) room.settings.scavSettings = {}; if (!target) { const points = []; const source = Object.entries({ ...DEFAULT_POINTS, ...room.settings.scavSettings.winPoints || {} }); for (const entry of source) { points.push(`${entry[0]}: ${entry[1].map((p, i) => `(${i + 1}) ${p}`).join(", ")}`); } return this.sendReplyBox(`The points rewarded for winning hunts is:<br />${points.join("<br />")}`); } this.checkCan("declare", null, room); let [type, ...pointsSet] = target.split(","); type = toID(type); if (!RATED_TYPES.includes(type)) return this.errorReply(`You cannot set win points for ${type} hunts.`); const winPoints = pointsSet.map((p) => parseInt(p)); if (winPoints.some((p) => isNaN(p) || p < 0 || p > 1e3) || !winPoints.length) { return this.errorReply("The points value awarded for winning a scavenger hunt must be an integer between 0 and 1000."); } if (!room.settings.scavSettings.winPoints) room.settings.scavSettings.winPoints = {}; room.settings.scavSettings.winPoints[type] = winPoints; room.saveSettings(); const pointsDisplay = winPoints.map((p, i) => `(${i + 1}) ${p}`).join(", "); this.privateModAction(`${user.name} has set the points awarded for winning ${type} scavenger hunts to - ${pointsDisplay}`); this.modlog("SCAV SETPOINTS", null, `${type}: ${pointsDisplay}`); if (room.parent && !room.persist) { scavsRoom.modlog({ action: "SCAV SETPOINTS", loggedBy: user.id, note: `${pointsDisplay} [room: ${room.roomid}]` }); scavsRoom.sendMods(`(${user.name} has set the points awarded for winning ${type} scavenger hunts to - ${pointsDisplay} in <<${room.roomid}>>)`); scavsRoom.roomlog(`(${user.name} has set the points awarded for winning ${type} scavenger hunts to - ${pointsDisplay} in <<${room.roomid}>>)`); } }, resettwist: "settwist", settwist(target, room, user) { room = this.requireRoom(); const scavsRoom = getScavsRoom(room); if (!scavsRoom) { return this.errorReply("This command can only be used in the scavengers room."); } if (this.cmd.includes("reset")) target = "RESET"; if (!room.settings.scavSettings) room.settings.scavSettings = {}; if (!target) { const twist = room.settings.scavSettings.officialtwist || "none"; return this.sendReplyBox(`The current official twist is: ${twist}`); } this.checkCan("declare", null, room); if (target === "RESET") { room.settings.scavSettings.officialtwist = null; } else { const twist = toID(target); if (!import_scavenger_games.ScavMods.twists[twist] || twist === "constructor") return this.errorReply("Invalid twist."); room.settings.scavSettings.officialtwist = twist; room.saveSettings(); } if (room.settings.scavSettings.officialtwist) { this.privateModAction(`${user.name} has set the official twist to ${room.settings.scavSettings.officialtwist}`); } else { this.privateModAction(`${user.name} has removed the official twist.`); } this.modlog("SCAV TWIST", null, room.settings.scavSettings.officialtwist); if (room.parent && !room.persist) { if (room.settings.scavSettings.officialtwist) { scavsRoom.modlog({ action: "SCAV TWIST", loggedBy: user.id, note: `${room.settings.scavSettings.officialtwist} [room: ${room.roomid}]` }); scavsRoom.sendMods(`(${user.name} has set the official twist to - ${room.settings.scavSettings.officialtwist} in <<${room.roomid}>>)`); scavsRoom.roomlog(`(${user.name} has set the official twist to - ${room.settings.scavSettings.officialtwist} in <<${room.roomid}>>)`); } else { scavsRoom.sendMods(`(${user.name} has reset the official twist in <<${room.roomid}>>)`); scavsRoom.roomlog(`(${user.name} has reset the official twist in <<${room.roomid}>>)`); } } }, twists(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } this.checkCan("mute", null, room); if (!this.runBroadcast()) return false; let buffer = `<table><tr><th>Twist</th><th>Description</th></tr>`; buffer += Object.values(import_scavenger_games.ScavMods.twists).map((twist) => import_lib.Utils.html`<tr><td style="padding: 5px;">${twist.name}</td><td style="padding: 5px;">${twist.desc}</td></tr>`).join(""); buffer += `</table>`; this.sendReply(`|raw|<div class="ladder infobox-limited">${buffer}</div>`); }, /** * Scavenger statistic tracking */ huntcount: "huntlogs", async huntlogs(target, room, user) { room = this.requireRoom("scavengers"); this.checkCan("mute", null, room); if (target === "RESET") { this.checkCan("declare", null, room); await HostLeaderboard.softReset(); HostLeaderboard.write(); this.privateModAction(`${user.name} has reset the host log leaderboard into the next month.`); this.modlog("SCAV HUNTLOGS", null, "RESET"); return; } else if (target === "HARD RESET") { this.checkCan("declare", null, room); HostLeaderboard.hardReset().write(); this.privateModAction(`${user.name} has hard reset the host log leaderboard.`); this.modlog("SCAV HUNTLOGS", null, "HARD RESET"); return; } let [sortMethod, isUhtmlChange] = target.split(","); const sortingFields = ["points", "cumulative-points"]; if (!sortingFields.includes(sortMethod)) sortMethod = "points"; const data = await HostLeaderboard.visualize(sortMethod); this.sendReply( `|${isUhtmlChange ? "uhtmlchange" : "uhtml"}|scav-huntlogs|<div class="ladder" style="overflow-y: scroll; max-height: 300px;"><table style="width: 100%"><tr><th>Rank</th><th>Name</th><th>Hunts Created</th><th>Total Hunts Created</th><th>History</th></tr>${data.map((entry) => { const auth = room.auth.get(toID(entry.name)).trim(); const color = auth ? "inherit" : "gray"; return `<tr><td>${entry.rank}</td><td><span style="color: ${color}">${auth || " "}</span>${import_lib.Utils.escapeHTML(entry.name)}</td><td style="text-align: right;">${entry.points || 0}</td><td style="text-align: right;">${entry["cumulative-points"] || 0}</td><td style="text-align: left;">${entry["history-points"] ? `<span style="color: gray">{ ${entry["history-points"].join(", ")} }</span>` : ""}</td></tr>`; }).join("")}</table></div><div style="text-align: center">${sortingFields.map( (f) => `<button class="button${f === sortMethod ? " disabled" : ""}" name="send" value="/scav huntlogs ${f}, 1">${f}</button>` ).join(" ")}</div>` ); }, async playlogs(target, room, user) { room = this.requireRoom("scavengers"); this.checkCan("mute", null, room); if (target === "RESET") { this.checkCan("declare", null, room); await PlayerLeaderboard.softReset(); PlayerLeaderboard.write(); this.privateModAction(`${user.name} has reset the player log leaderboard into the next month.`); this.modlog("SCAV PLAYLOGS", null, "RESET"); return; } else if (target === "HARD RESET") { this.checkCan("declare", null, room); PlayerLeaderboard.hardReset().write(); this.privateModAction(`${user.name} has hard reset the player log leaderboard.`); this.modlog("SCAV PLAYLOGS", null, "HARD RESET"); return; } let [sortMethod, isUhtmlChange] = target.split(","); const sortingFields = ["join", "cumulative-join", "finish", "cumulative-finish", "infraction", "cumulative-infraction"]; if (!sortingFields.includes(sortMethod)) sortMethod = "finish"; const data = await PlayerLeaderboard.visualize(sortMethod); const formattedData = data.map((d) => { d.ratio = ((d.finish || 0) / (d.join || 1) * 100).toFixed(2); d["cumulative-ratio"] = ((d["cumulative-finish"] || 0) / (d["cumulative-join"] || 1) * 100).toFixed(2); return d; }); this.sendReply( `|${isUhtmlChange ? "uhtmlchange" : "uhtml"}|scav-playlogs|<div class="ladder" style="overflow-y: scroll; max-height: 300px;"><table style="width: 100%"><tr><th>Rank</th><th>Name</th><th>Finished Hunts</th><th>Joined Hunts</th><th>Ratio</th><th>Infractions</th></tr>${formattedData.map((entry) => { const auth = room.auth.get(toID(entry.name)).trim(); const color = auth ? "inherit" : "gray"; return `<tr><td>${entry.rank}</td><td><span style="color: ${color}">${auth || " "}</span>${import_lib.Utils.escapeHTML(entry.name)}</td><td style="text-align: right;">${entry.finish || 0} <span style="color: blue">(${entry["cumulative-finish"] || 0})</span>${entry["history-finish"] ? `<br /><span style="color: gray">(History: ${entry["history-finish"].join(", ")})</span>` : ""}</td><td style="text-align: right;">${entry.join || 0} <span style="color: blue">(${entry["cumulative-join"] || 0})</span>${entry["history-join"] ? `<br /><span style="color: gray">(History: ${entry["history-join"].join(", ")})</span>` : ""}</td><td style="text-align: right;">${entry.ratio}%<br /><span style="color: blue">(${entry["cumulative-ratio"] || "0.00"}%)</span></td><td style="text-align: right;">${entry.infraction || 0} <span style="color: blue">(${entry["cumulative-infraction"] || 0})</span>${entry["history-infraction"] ? `<br /><span style="color: gray">(History: ${entry["history-infraction"].join(", ")})</span>` : ""}</td></tr>`; }).join("")}</table></div><div style="text-align: center">${sortingFields.map( (f) => `<button class="button${f === sortMethod ? " disabled" : ""}" name="send" value="/scav playlogs ${f}, 1">${f}</button>` ).join(" ")}</div>` ); }, uninfract: "infract", infract(target, room, user) { room = this.requireRoom("scavengers"); this.checkCan("mute", null, room); const targetId = toID(target); if (!targetId) return this.errorReply(`Please include the name of the user to ${this.cmd}.`); const change = this.cmd === "infract" ? 1 : -1; PlayerLeaderboard.addPoints(targetId, "infraction", change, true).write(); this.privateModAction(`${user.name} has ${change > 0 ? "given" : "taken"} one infraction point ${change > 0 ? "to" : "from"} '${targetId}'.`); this.modlog(`SCAV ${this.cmd.toUpperCase()}`, user); }, modsettings: { "": "update", "update"(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) return false; this.checkCan("declare", null, room); const settings = room.settings.scavSettings?.scavmod || {}; this.sendReply(`|uhtml${this.cmd === "update" ? "change" : ""}|scav-modsettings|<div class=infobox><strong>Scavenger Moderation Settings:</strong><br /><br /><button name=send value='/scav modsettings ipcheck toggle'><i class="fa fa-power-off"></i></button> Multiple connection verification: ${settings.ipcheck ? "ON" : "OFF"}</div>`); }, "ipcheck"(target, room, user) { room = this.requireRoom(); if (!getScavsRoom(room)) return false; this.checkCan("declare", null, room); if (!room.settings.scavSettings) room.settings.scavSettings = {}; const settings = room.settings.scavSettings.scavmod || {}; target = toID(target); const setting = { "on": true, "off": false, "toggle": !settings.ipcheck }; if (!(target in setting)) return this.sendReply("Invalid setting - ON, OFF, TOGGLE"); settings.ipcheck = setting[target]; room.settings.scavSettings.scavmod = settings; room.saveSettings(); this.privateModAction(`${user.name} has set multiple connections verification to ${setting[target] ? "ON" : "OFF"}.`); this.modlog("SCAV MODSETTINGS IPCHECK", null, setting[target] ? "ON" : "OFF"); return this.parse("/scav modsettings update"); } }, /** * Database Commands */ recycledhunts(target, room, user) { room = this.requireRoom(); this.checkCan("mute", null, room); if (!getScavsRoom(room)) { return this.errorReply("Scavenger Hunts can only be added to the database in the scavengers room."); } let cmd; [cmd, target] = import_lib.Utils.splitFirst(target, " "); cmd = toID(cmd); if (!["addhunt", "list", "removehunt", "addhint", "removehint", "autostart"].includes(cmd)) { return this.parse(`/recycledhuntshelp`); } if (cmd === "addhunt") { if (!target) return this.errorReply(`Usage: ${cmd} Hunt Text`); const [hostsArray, ...questions] = target.split("|"); const hosts = ScavengerHunt.parseHosts(hostsArray.split(/[,;]/), room, true); if (!hosts.length) { return this.errorReply("You need to specify a host."); } const result = ScavengerHunt.parseQuestions(questions); if (result.err) return this.errorReply(result.err); ScavengerHuntDatabase.addRecycledHuntToDatabase(hosts, result.result); return this.privateModAction(`A recycled hunt has been added to the database.`); } if (ScavengerHuntDatabase.isEmpty()) return this.errorReply("There are no hunts in the database."); if (cmd === "list") { return this.parse(`/join view-recycledHunts-${room}`); } const params = target.split(",").map((param) => param.trim()).filter((param) => param !== ""); const usageMessages = { "removehunt": "Usage: removehunt hunt_number", "addhint": "Usage: addhint hunt number, question number, hint text", "removehint": "Usage: removehint hunt number, question number, hint text", "autostart": "Usage: autostart on/off" }; if (!params) return this.errorReply(usageMessages[cmd]); const numberOfRequiredParameters = { "removehunt": 1, "addhint": 3, "removehint": 3, "autostart": 1 }; if (params.length < numberOfRequiredParameters[cmd]) return this.errorReply(usageMessages[cmd]); const [huntNumber, questionNumber, hintNumber] = params.map((param) => parseInt(param)); const cmdsNeedingHuntNumber = ["removehunt", "removehint", "addhint"]; if (cmdsNeedingHuntNumber.includes(cmd)) { if (!ScavengerHuntDatabase.hasHunt(huntNumber)) return this.errorReply("You specified an invalid hunt number."); } const cmdsNeedingQuestionNumber = ["addhint", "removehint"]; if (cmdsNeedingQuestionNumber.includes(cmd)) { if (isNaN(questionNumber) || questionNumber <= 0 || questionNumber > scavengersData.recycledHunts[huntNumber - 1].questions.length) { return this.errorReply("You specified an invalid question number."); } } const cmdsNeedingHintNumber = ["removehint"]; if (cmdsNeedingHintNumber.includes(cmd)) { const numQuestions = scavengersData.recycledHunts[huntNumber - 1].questions.length; if (isNaN(questionNumber) || questionNumber <= 0 || questionNumber > numQuestions) { return this.errorReply("You specified an invalid hint number."); } } if (cmd === "removehunt") { ScavengerHuntDatabase.removeRecycledHuntFromDatabase(huntNumber); return this.privateModAction(`Recycled hunt #${huntNumber} was removed from the database.`); } else if (cmd === "addhint") { const hintText = params[2]; ScavengerHuntDatabase.addHintToRecycledHunt(huntNumber, questionNumber, hintText); return this.privateModAction(`Hint added to Recycled hunt #${huntNumber} question #${questionNumber}: ${hintText}.`); } else if (cmd === "removehint") { ScavengerHuntDatabase.removeHintToRecycledHunt(huntNumber, questionNumber, hintNumber); return this.privateModAction(`Hint #${hintNumber} was removed from Recycled hunt #${huntNumber} question #${questionNumber}.`); } else if (cmd === "autostart") { if (!room.settings.scavSettings) room.settings.scavSettings = {}; if (params[0] !== "on" && params[0] !== "off") return this.errorReply(usageMessages[cmd]); if (params[0] === "on" === !!room.settings.scavSettings.addRecycledHuntsToQueueAutomatically) { return this.errorReply(`Autostarting recycled hunts is already ${room.settings.scavSettings.addRecycledHuntsToQueueAutomatically ? "on" : "off"}.`); } room.settings.scavSettings.addRecycledHuntsToQueueAutomatically = !room.settings.scavSettings.addRecycledHuntsToQueueAutomatically; this.privateModAction(`Automatically adding recycled hunts to the queue is now ${room.settings.scavSettings.addRecycledHuntsToQueueAutomatically ? "on" : "off"}`); if (params[0] === "on") { return this.parse("/scav queuerecycled"); } } }, recycledhuntshelp() { if (!this.runBroadcast()) return; this.sendReplyBox([ "<b>Help for Recycled Hunts</b>", "- addhunt <Hunt Text>: Adds a hunt to the database of recycled hunts.", "- removehunt<Hunt Number>: Removes a hunt form the database of recycled hunts.", "- list: Shows a list of hunts in the database along with their questions and hints.", "- addhint <Hunt Number, Question Number, Hint Text>: Adds a hint to the specified question in the specified hunt.", "- removehint <Hunt Number, Question Number, Hint Number>: Removes the specified hint from the specified question in the specified hunt.", "- autostart <on/off>: Sets whether or not recycled hunts are automatically added to the queue when a hunt ends." ].join("<br/>")); } }; const pages = { recycledHunts(query, user, connection) { this.title = "Recycled Hunts"; const room = this.requireRoom(); let buf = ""; if (!user.named) return Rooms.RETRY_AFTER_LOGIN; if (!room.persist) return; this.checkCan("mute", null, room); buf += `<div class="pad"><h2>List of recycled Scavenger hunts</h2>`; buf += `<ol style="width: 90%;">`; for (const hunt of scavengersData.recycledHunts) { buf += `<li>`; buf += `<h4>By ${hunt.hosts.map((host) => host.name).join(", ")}</h4>`; for (const question of hunt.questions) { buf += `<details>`; buf += `<summary>${question.text}</summary>`; buf += `<dl>`; buf += `<dt>Answers:</dt>`; for (const answer of question.answers) { buf += `<dd>${answer}</dd>`; } buf += `</dl>`; if (question.hints.length) { buf += `<dl>`; buf += `<dt>Hints:</dt>`; for (const hint of question.hints) { buf += `<dd>${hint}</dd>`; } buf += `</dl>`; } buf += `</details>`; } buf += `</li>`; } buf += `</ol>`; buf += `</div>`; return buf; } }; const commands = { // general scav: "scavengers", scavengers: ScavengerCommands, tscav: "teamscavs", teamscavs: ScavengerCommands.teamscavs, teamscavshelp: ScavengerCommands.teamscavshelp, // old game aliases scavenge: ScavengerCommands.guess, startpracticehunt: "starthunt", startofficialhunt: "starthunt", startminihunt: "starthunt", startunratedhunt: "starthunt", startrecycledhunt: "starthunt", starttwisthunt: "starthunt", starttwistofficial: "starthunt", starttwistpractice: "starthunt", starttwistmini: "starthunt", starttwistunrated: "starthunt", forcestarthunt: "starthunt", forcestartunrated: "starthunt", forcestartpractice: "starthunt", starthunt: ScavengerCommands.create, joinhunt: ScavengerCommands.join, leavehunt: ScavengerCommands.leave, resethunt: ScavengerCommands.reset, resethunttoqueue: ScavengerCommands.resettoqueue, forceendhunt: "endhunt", endhunt: ScavengerCommands.end, edithunt: ScavengerCommands.edithunt, viewhunt: ScavengerCommands.viewhunt, inherithunt: ScavengerCommands.inherit, scavengerstatus: ScavengerCommands.status, scavengerhint: ScavengerCommands.hint, nexthunt: ScavengerCommands.next, // point aliases scavaddpoints: "scavengeraddpoints", scavengersaddpoints: ScavengerCommands.addpoints, scavrmpoints: "scavengersremovepoints", scavengersrmpoints: "scavengersremovepoints", scavremovepoints: "scavengersremovepoints", scavengersremovepoints: ScavengerCommands.addpoints, scavresetlb: "scavengersresetlb", scavengersresetlb: ScavengerCommands.resetladder, recycledhunts: ScavengerCommands.recycledhunts, recycledhuntshelp: ScavengerCommands.recycledhuntshelp, scavrank: ScavengerCommands.rank, scavladder: "scavtop", scavtop: ScavengerCommands.ladder, scavengerhelp: "scavengershelp", scavhelp: "scavengershelp", scavengershelp(target, room, user) { if (!room || !getScavsRoom(room)) { return this.errorReply("This command can only be used in the scavengers room."); } if (!this.runBroadcast()) return false; const userCommands = [ "<strong>Player commands:</strong>", "- /scavengers: Join the scavengers room.", "- /joinhunt: Join the current scavenger hunt.", "- /leavehunt: Leave the current scavenger hunt. Also resets your progress.", "- /viewhunt: Show the ongoing hunt up to where you solved it.", "- /scavenge <em>[guess]</em>: Submit your answer to the current hint.", "- /scavengerstatus (or /scav status): Check your status in the current hunt.", "- /scavengers queue (or /scav queue): Showcase the hunts currently in queue, with the answers hidden for any hunt that is not yours.", "- /scavengerhint (or /scav hint): View your latest hint in the current game.", "- /scavladder (or /scav top): View the current scavengers leaderboard.", "- /scavrank <em>[user]</em>: View the rank of the user on the current scavenger leaderboard. Defaults to the user if no name is provided.", "For a more in-depth overview, use /scavhelp staff." ].join("<br />"); const staffCommands = [ "<strong>Staff and auth commands:</strong>", "As a <strong>room voice (+)</strong>, you can use the following Scavengers commands, on top of the regular commands (see /scavhelp):", "- /scav edithunt <em>[question number]</em>, <em>[hint | answer]</em>, <em>[value]</em>: Edit the ongoing scavenger hunt. Only the host(s) can edit the hunt.", "- /scav addhint <em>[question number]</em>, <em>[value]</em>: Add a hint to a question in the ongoing scavenger hunt. Only the host(s) can add a hint.", "- /scav edithint <em>[question number]</em>, <em>[hint number]</em>, <em>[value]</em>: Edit a hint to a question in the ongoing scavenger hunt. Only the host(s) can edit a hint.", "- /scav removehint <em>[question number]</em>, <em>[hint number]<e/m> (or /scav deletehint): Remove a hint from a question in the current scavenger hunt. Only the host(s) can remove a hint.", "- /teamscavshelp: Explains the team scavs plugin.", "<br />As a <strong>room driver (%)</strong>, you can also use the following Scavengers commands:", "- /scav queue (unrated) <em>[host(s)]</em> | <em>[hint]</em> | <em>[answer]</em> | <em>[hint]</em> | <em>[answer]</em> | <em>[hint]</em> | <em>[answer]</em> | ...: Queue a scavenger hunt to be started after the current hunt is finished.", "- /start(official/practice/mini/unrated)hunt <em>[host]</em> | <em>[hint]</em> | <em>[answer]</em> | <em>[hint]</em> | <em>[answer]</em> | <em>[hint]</em> | </em>[answer]</em> | ...: Create a new (official/practice/mini/unrated) scavenger hunt and start it immediately.", "- /scav viewqueue (or /scav queue): Look at the list of queued scavenger hunts. Now also includes the option to remove hunts from the queue.", "- /resethunt: Reset the current scavenger hunt without revealing the hints and answers, nor giving out points.", "- /resethunttoqueue: Reset the ongoing scavenger hunt without revealing the hints and answers, nor giving out points. Then, add it directly to the queue.", "- /scav timer <em>[minutes]</em>: Set a timer to automatically end the current hunt. Setting [minutes] to 0 turns off the timer.", "- /endhunt: End the current scavenger hunt immediately and announce the winners and the answers.", "- /nexthunt: Start the next hunt in the queue.", "- /viewhunt: View the ongoing scavenger hunt. As a host, you can also view the hunt in its entirety.", "- /inherithunt: Become the staff host, gaining staff permissions to the current hunt.", "- /scav games create <em>[game mode]</em>: start a game of the given mode.", " Game modes include: Jump Start, Point Rally, KO games, Scav games and team scavs.", "- /scav games end: End the game of the given type.", "- /starttwist(hunt / practice / official / mini /unrated) <em>[twist]</em> | <em>[host]</em> | <em>[hint]</em> | <em>[answer]</em> | <em>[hint]</em> | <em>[answer]</em> | <em>[hint]</em> | <em>[answer]</em> | \u2026 : Create and start a new scavenger hunt that uses a specified twist mode. This can be used inside a scavenger game mode.", "- /scav twists: Show a list of all the twists that are available on the server.", "- /scav settwist: View the current default official hunt twist that is in use.", "- /scav setpoints: Show the current point distribution for officials, minis and regular hunts.", "- /scav setblitz: Show the current points awarded for Blitzing an official, mini or regular hunt.", "- /scav defaulttimer: Show the default timer applied to hunts started automatically from the queue.", "- /scav addpoints <em>[user]</em>, <em>[amount]</em>: Give the user the specified amount of points towards the current ladder.", "- /scav removepoints <em>[user]</em>, <em>[amount]</em>: Remove the specified amount of points from the user towards the current ladder.", "- /recycledhunts: Modify the database of recycled hunts and enable/disable autoqueing them.", "- /scav queuerecycled <em>[number]</em>: Queue a recycled hunt from the database. If <em>[number]</em> is left blank, then a random hunt is queued.", "- /recycledhuntshelp: give more info about the recycled hunts.", "<br />As a <strong>room owner (#)</strong>, you can also use the following scavengers commands:", "- /scav resetladder: Reset the current scavenger leaderboard.", "- /scav setpoints <em>[1st place]</em>, <em>[2nd place]</em>, <em>[3rd place]</em>, <em>[4th place]</em>, <em>[5th place]</em>, ...: Set the point values for wins of officials, minis and regular hunts.", "- /scav defaulttimer <em>[value]</em>: Set the default timer applied to automatically started hunts from the queue.", "- /scav setblitz <em>[value]</em> ...: Set the blitz award to the given value.", "- /scav settwist <em>[twist name]</em>: Set the default twist mode for all official hunts.", "- /scav resettwist: Reset the default twist mode for all official hunts to nothing.", "- /scav modsettings: Allow or disallow miscellaneous room settings" ].join("<br />"); const gamesCommands = [ "<strong>Game commands:</strong>", "- /scav game create <em>[kogames | pointrally | scavengergames | jumpstart | teamscavs]</em>: Start a new scripted scavenger game. (Requires: % @ * # &)", "- /scav game end: End the current scavenger game. (Requires: % @ * # &)", "- /scav game kick <em>[user]</em>: Kick the user from the current scavenger game. (Requires: % @ * # &)", "- /scav game score: Show the current scoreboard for any game with a leaderboard.", "- /scav game rank <em>[user]</em>: Show a user's rank in the current scavenger game leaderboard." ].join("<br />"); target = toID(target); const display = target === "all" ? `${userCommands}<br /><br />${staffCommands}<br /><br />${gamesCommands}` : target === "staff" || target === "auth" ? staffCommands : target === "games" || target === "game" ? gamesCommands : userCommands; this.sendReplyBox(display); } }; //# sourceMappingURL=scavengers.js.map