"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}&nbsp;${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>&nbsp;<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.gameid = "scavengerhunt";
    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.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(player);
    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;
    this.removePlayer(player);
    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 || "&nbsp;"}</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 || "&nbsp;"}</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 &lt;Hunt Text>: Adds a hunt to the database of recycled hunts.",
      "- removehunt&lt;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 &lt;Hunt Number, Question Number, Hint Text>: Adds a hint to the specified question in the specified hunt.",
      "- removehint &lt;Hunt Number, Question Number, Hint Number>: Removes the specified hint from the specified question in the specified hunt.",
      "- autostart &lt;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.",
      "&nbsp;&nbsp;&nbsp;&nbsp;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
