diff --git a/build/scripts/commands.js b/build/scripts/commands.js index 187fb01..a076cd5 100644 --- a/build/scripts/commands.js +++ b/build/scripts/commands.js @@ -805,11 +805,17 @@ function resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback) { break; default: (0, funcs_4.crash)("Unable to resolve arg of type ".concat(argToResolve_1.type)); } - (0, menus_1.menu)("Select a player", "Select a player for the argument \"".concat(argToResolve_1.name, "\""), optionsList_1, sender, function (_a) { - var option = _a.option; - processedArgs[argToResolve_1.name] = players_1.FishPlayer.get(option); + /* + menu(`Select a player`, `Select a player for the argument "${argToResolve.name}"`, [new GUI_Container(optionsList, "auto", player => Strings.stripColors(player.name).length >= 3 ? Strings.stripColors(player.name) : escapeStringColorsClient(player.name)), new GUI_Cancel()], sender, ({data}) => { + processedArgs[argToResolve.name] = FishPlayer.get(data); resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback); - }, true, function (player) { return Strings.stripColors(player.name).length >= 3 ? Strings.stripColors(player.name) : (0, funcs_3.escapeStringColorsClient)(player.name); }); + }, ) + */ + (0, menus_1.listMenu)("Select a player", "Select a player for the argument ".concat(argToResolve_1.name), new menus_1.GUI_Container(optionsList_1, "auto", function (player) { return player.name; }), sender, function (_a) { + var data = _a.data; + processedArgs[argToResolve_1.name] = players_1.FishPlayer.get(data); + resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback); + }); } } function initialize() { diff --git a/build/scripts/menus.js b/build/scripts/menus.js index d96768a..2d57e46 100644 --- a/build/scripts/menus.js +++ b/build/scripts/menus.js @@ -30,15 +30,27 @@ var __read = (this && this.__read) || function (o, n) { } return ar; }; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.listeners = void 0; +exports.listeners = exports.GUI_Confirm = exports.GUI_Page = exports.GUI_Cancel = exports.GUI_Container = void 0; exports.registerListeners = registerListeners; +exports.pageMenu = pageMenu; +exports.listMenu = listMenu; exports.menu = menu; var commands_1 = require("./commands"); var players_1 = require("./players"); var utils_1 = require("./utils"); var funcs_1 = require("./funcs"); var funcs_2 = require("./funcs"); +//#region Draw Menu /** Stores a mapping from name to the numeric id of a listener that has been registered. */ var registeredListeners = {}; exports.listeners = registeredListeners; @@ -79,39 +91,45 @@ function registerListeners() { } } //this is a minor abomination but theres no good way to do overloads in typescript -function menu(title, description, options, target, callback, includeCancel, optionStringifier, //this is dubious -columns) { - if (includeCancel === void 0) { includeCancel = true; } - if (optionStringifier === void 0) { optionStringifier = function (t) { return t; }; } - if (columns === void 0) { columns = 3; } +function menu(title, description, elements, target, callback) { + //target.activeMenu.cancelOptionId = -1; GUI_Cancel handles cancel already + var ArrangedElements = { data: [], stringified: [] }; + elements.forEach(function (element) { + var _a; + (_a = ArrangedElements.data).push.apply(_a, __spreadArray([], __read(element.data()), false)); + }); + elements.forEach(function (element) { + var _a; + return (_a = ArrangedElements.stringified).push.apply(_a, __spreadArray([], __read(element.format()), false)); + }); + //flatten to arrays + var PackedElements = { data: ArrangedElements.data.flat(), stringified: ArrangedElements.stringified.flat() }; + if (PackedElements.data.length == 0) { + ArrangedElements.stringified.push([""]); + ArrangedElements.data.push([null]); // not needed, but nice to keep data and string in sync. + } if (!callback) { //overload 1, just display a menu with no callback - Call.menu(target.con, registeredListeners.none, title, description, options.length == 0 ? [[""]] : (0, funcs_2.to2DArray)(options.map(optionStringifier), columns)); + Call.menu(target.con, registeredListeners.none, title, description, ArrangedElements.stringified); } else { //overload 2, display a menu with callback - //Set up the 2D array of options, and add cancel - //Use "" as a fallback, because Call.menu with an empty array of options causes a client crash - var arrangedOptions = (options.length == 0 && !includeCancel) ? [[""]] : (0, funcs_2.to2DArray)(options.map(optionStringifier), columns); - if (includeCancel) { - arrangedOptions.push(["Cancel"]); - target.activeMenu.cancelOptionId = options.length; - } - else { - target.activeMenu.cancelOptionId = -1; - } //The target fishPlayer has a property called activeMenu, which stores information about the last menu triggered. - target.activeMenu.callback = function (fishSender, option) { + target.activeMenu.callback = function (_fishSender, option) { //Additional permission validation could be done here, but the only way that callback() can be called is if the above statement executed, //and on sensitive menus such as the stop menu, the only way to reach that is if menu() was called by the /stop command, //which already checks permissions. //Additionally, the callback is cleared by the generic menu listener after it is executed. //We do need to validate option though, as it can be any number. - if (!(option in options)) + if (!(option in PackedElements.data)) + return; + if (typeof PackedElements.data[option] === 'string' && PackedElements.data[option] == "cancel") { return; + } // cancel button pressed, no need to callback try { callback({ - option: options[option], + data: PackedElements.data[option], + text: PackedElements.stringified[option], sender: target, outputFail: function (message) { return (0, utils_1.outputFail)(message, target); }, outputSuccess: function (message) { return (0, utils_1.outputSuccess)(message, target); }, @@ -131,6 +149,99 @@ columns) { } } }; - Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions); + Call.menu(target.con, registeredListeners.generic, title, description, ArrangedElements.stringified); + } +} +//#endregion +//#region Draw Page Menus +//draws a page menu with arbitrary pages +function pageMenu(title, description, elements, target, callback) { + var pages = elements.length; + function drawpage(index) { + var e = []; + if (!pages) { + e.push(new GUI_Cancel()); + } + else { + e.push.apply(e, __spreadArray([], __read(elements[index]), false)); + e.push(new GUI_Page(index + 1, pages)); + } + menu(title, description, e, target, function (res) { + // handle control element of the ui + if (typeof res.data === 'string') { + switch (res.data) { + case "left": + drawpage((index == 0) ? (0) : (index - 1)); + break; + case "right": + drawpage((index == pages - 1) ? (pages - 1) : (index + 1)); + break; + case "center": + drawpage(index); + break; + default: + callback(res); + break; + } + } + }); + return; } + drawpage(0); } +//TODO make list a GUI_Element[] instead of a single Container +//TODO use GUI_Element for formatting instead of defaulting to single column +function listMenu(title, description, list, target, callback, pageSize) { + if (pageSize === void 0) { pageSize = 10; } + var pooledData = []; + list.data().flat().forEach(function (data) { pooledData.push(data); }); + var pagedData = pooledData.reduce(function (res, _, index) { if (index % pageSize === 0) { + res.push(pooledData.slice(index, index + pageSize)); + } return res; }, []); + var pagesElements = []; + pagedData.forEach(function (pageData) { return pagesElements.push([new GUI_Container(pageData, 1, list.stringifier)]); }); + pageMenu(title, description, pagesElements, target, function (res) { Log.info("".concat(res.data)); callback(res); }); +} +var GUI_Container = /** @class */ (function () { + function GUI_Container(options, columns, stringifier) { + if (columns === void 0) { columns = 3; } + if (stringifier === void 0) { stringifier = function (option) { return option; }; } + var _this = this; + this.options = options; + this.columns = columns; + this.stringifier = stringifier; + this.format = function () { return ((0, funcs_2.to2DArray)(_this.options.map(_this.stringifier), (_this.columns == 'auto') ? (3) : (_this.columns))); }; + this.data = function () { return (0, funcs_2.to2DArray)(_this.options, (_this.columns == 'auto') ? (3) : (_this.columns)); }; + } + ; + return GUI_Container; +}()); +exports.GUI_Container = GUI_Container; +var GUI_Cancel = /** @class */ (function () { + function GUI_Cancel() { + this.format = function () { return ([["cancel"]]); }; + this.data = function () { return ([["cancel"]]); }; + } + return GUI_Cancel; +}()); +exports.GUI_Cancel = GUI_Cancel; +var GUI_Page = /** @class */ (function () { + function GUI_Page(currentPage, pages) { + var _this = this; + this.currentPage = currentPage; + this.pages = pages; + this.format = function () { return ((0, funcs_2.to2DArray)(["<--", "".concat(_this.currentPage, "/").concat(_this.pages), "-->"], 3)); }; + this.data = function () { return ([["left", "center", "right"]]); }; + } + return GUI_Page; +}()); +exports.GUI_Page = GUI_Page; +var GUI_Confirm = /** @class */ (function () { + function GUI_Confirm() { + this.format = function () { return [["[green]Yes, do it", "[red] No, cancel"]]; }; + this.data = function () { return [[true, false]]; }; + } + return GUI_Confirm; +}()); +exports.GUI_Confirm = GUI_Confirm; +//#endregion diff --git a/build/scripts/playerCommands.js b/build/scripts/playerCommands.js index d6d4213..099d10b 100644 --- a/build/scripts/playerCommands.js +++ b/build/scripts/playerCommands.js @@ -565,11 +565,11 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { if (target.hasPerm("blockTrolling")) (0, commands_1.fail)(f(templateObject_9 || (templateObject_9 = __makeTemplateObject(["Player ", " is insufficiently trollable."], ["Player ", " is insufficiently trollable."])), args.player)); } - (0, menus_1.menu)("Rules for [#0000ff]>|||> FISH [white]servers", config_1.rules.join("\n\n"), ["[green]I agree to abide by these rules[]", "No"], target, function (_a) { - var option = _a.option; - if (option == "No") + (0, menus_1.menu)("Rules for [#0000ff]>|||> FISH [white]servers", config_1.rules.join("\n\n"), [new menus_1.GUI_Container(["[green]I agree to abide by these rules[]", "No"])], target, function (_a) { + var text = _a.text; + if (text == "No") target.kick("You must agree to the rules to play on this server. Rejoin to agree to the rules.", 1); - }, false); + }); if (target !== sender) outputSuccess(f(templateObject_10 || (templateObject_10 = __makeTemplateObject(["Reminded ", " of the rules."], ["Reminded ", " of the rules."])), target)); }, @@ -587,7 +587,7 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { (0, commands_1.fail)("You do not have permission to show popups to other players, please run /void with no arguments to send a chat message to everyone."); if (args.player !== sender && args.player.hasPerm("blockTrolling")) (0, commands_1.fail)("Target player is insufficiently trollable."); - (0, menus_1.menu)("\uf83f [scarlet]WARNING[] \uf83f", "[white]Don't break the Power Void (\uF83F), it's a trap!\nPower voids disable anything they are connected to.\nIf you break it, [scarlet]you will get attacked[] by enemy units.\nPlease stop attacking and [lime]build defenses[] first!", ["I understand"], args.player); + (0, menus_1.menu)("\uf83f [scarlet]WARNING[] \uf83f", "[white]Don't break the Power Void (\uF83F), it's a trap!\nPower voids disable anything they are connected to.\nIf you break it, [scarlet]you will get attacked[] by enemy units.\nPlease stop attacking and [lime]build defenses[] first!", [new menus_1.GUI_Container(["I understand"])], args.player); (0, utils_1.logAction)("showed void warning", sender, args.player); outputSuccess(f(templateObject_11 || (templateObject_11 = __makeTemplateObject(["Warned ", " about power voids with a popup message."], ["Warned ", " about power voids with a popup message."])), args.player)); } @@ -667,20 +667,20 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { handler: function (_a) { var sender = _a.sender, manager = _a.data.manager; if (!manager.session) { - (0, menus_1.menu)("Start a Next Wave Vote", "Select the amount of waves you would like to skip, or click \"Cancel\" to abort.", [1, 5, 10], sender, function (_a) { - var option = _a.option; + (0, menus_1.menu)("Start a Next Wave Vote", "Select the amount of waves you would like to skip, or click \"Cancel\" to abort.", [new menus_1.GUI_Container([1, 5, 10], "auto", function (n) { return "".concat(n, " waves"); }), new menus_1.GUI_Cancel()], sender, function (_a) { + var data = _a.data; if (manager.session) { //Someone else started a vote - if (manager.session.data != option) + if (manager.session.data != data) (0, commands_1.fail)("Someone else started a vote with a different number of waves to skip."); else - manager.vote(sender, sender.voteWeight(), option); + manager.vote(sender, sender.voteWeight(), data); } else { //this is still a race condition technically... shouldn't be that bad right? - manager.start(sender, sender.voteWeight(), option); + manager.start(sender, sender.voteWeight(), data); } - }, true, function (n) { return "".concat(n, " waves"); }); + }); } else { manager.vote(sender, sender.voteWeight(), null); @@ -833,25 +833,36 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { Events.on(EventType.GameOverEvent, resetVotes); Events.on(EventType.ServerLoadEvent, resetVotes); return { - args: ['map:map'], + args: ['map:map?'], description: 'Allows you to vote for the next map. Use /maps to see all available maps.', perm: commands_1.Perm.play, data: { votes: votes, voteEndTime: function () { return voteEndTime; }, resetVotes: resetVotes, endVote: endVote }, requirements: [commands_1.Req.cooldown(10000), commands_1.Req.modeNot("hexed")], handler: function (_a) { var map = _a.args.map, sender = _a.sender; - if (votes.get(sender)) - (0, commands_1.fail)("You have already voted."); - votes.set(sender, map); - if (voteEndTime == -1) { - if ((Date.now() - lastVoteTime) < 60000) - (0, commands_1.fail)("Please wait 1 minute before starting a new map vote."); - startVote(); - Call.sendMessage("[cyan]Next Map Vote: ".concat(sender.name, "[cyan] started a map vote, and voted for [yellow]").concat(map.name(), "[cyan]. Use /nextmap ").concat(map.plainName(), " to add your vote!")); + if (!map) { + (0, menus_1.listMenu)("Please Select a Map", "", new menus_1.GUI_Container(Vars.maps.customMaps().toArray(), 1, function (map) { return "[accent]".concat(map.name()); }), sender, function (_a) { + var data = _a.data; + playervote(data); + }); } else { - Call.sendMessage("[cyan]Next Map Vote: ".concat(sender.name, "[cyan] voted for [yellow]").concat(map.name(), "[cyan]. Time left: [scarlet]").concat((0, utils_1.formatTimeRelative)(voteEndTime, true))); - showVotes(); + playervote(map); + } + function playervote(option) { + if (votes.get(sender)) + (0, commands_1.fail)("You have already voted."); + votes.set(sender, option); + if (voteEndTime == -1) { + if ((Date.now() - lastVoteTime) < 60000) + (0, commands_1.fail)("Please wait 1 minute before starting a new map vote."); + startVote(); + Call.sendMessage("[cyan]Next Map Vote: ".concat(sender.name, "[cyan] started a map vote, and voted for [yellow]").concat(option.name(), "[cyan]. Use /nextmap ").concat(option.plainName(), " to add your vote!")); + } + else { + Call.sendMessage("[cyan]Next Map Vote: ".concat(sender.name, "[cyan] voted for [yellow]").concat(option.name(), "[cyan]. Time left: [scarlet]").concat((0, utils_1.formatTimeRelative)(voteEndTime, true))); + showVotes(); + } } } }; diff --git a/build/scripts/players.js b/build/scripts/players.js index 9196d5a..05a0310 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -352,7 +352,7 @@ var FishPlayer = /** @class */ (function () { }); //I think this is a better spot for this if (fishPlayer.firstJoin()) - (0, menus_1.menu)("Rules for [#0000ff] >|||> FISH [white] servers [white]", config_1.rules.join("\n\n[white]") + "\nYou can view these rules again by running [cyan]/rules[].", ["[green]I understand and agree to these terms"], fishPlayer); + (0, menus_1.menu)("Rules for [#0000ff] >|||> FISH [white] servers [white]", config_1.rules.join("\n\n[white]") + "\nYou can view these rules again by running [cyan]/rules[].", [new menus_1.GUI_Container(["[green]I understand and agree to these terms"])], fishPlayer); } }; /** Must be run on PlayerJoinEvent. */ @@ -676,15 +676,12 @@ var FishPlayer = /** @class */ (function () { api.sendStaffMessage("Autoflagged player ".concat(_this.name, "[cyan] for suspected vpn!"), "AntiVPN"); FishPlayer.messageStaff("[yellow]WARNING:[scarlet] player [cyan]\"".concat(_this.name, "[cyan]\"[yellow] is new (").concat(info.timesJoined - 1, " joins) and using a vpn. They have been automatically stopped and muted. Unless there is an ongoing griefer raid, they are most likely innocent. Free them with /free.")); Log.warn("Player ".concat(_this.name, " (").concat(_this.uuid, ") was autoflagged.")); - (0, menus_1.menu)("[gold]Welcome to Fish Community!", "[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ".concat(config_1.FColor.discord(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Join our Discord"], ["Join our Discord"]))), " to request a staff member come online if none are on."), ["Close", "Discord"], _this, function (_a) { - var option = _a.option, sender = _a.sender; - if (option == "Discord") { + (0, menus_1.menu)("[gold]Welcome to Fish Community!", "[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ".concat(config_1.FColor.discord(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Join our Discord"], ["Join our Discord"]))), " to request a staff member come online if none are on."), [new menus_1.GUI_Container(["Close", "Discord"], 1, function (str) { return ((str == "Discord") ? (config_1.FColor.discord(str)) : (str)); })], _this, function (_a) { + var result = _a.data, sender = _a.sender; + if (result == "Discord") { Call.openURI(sender.con, config_1.text.discordURL); } - }, false, function (str) { return ({ - "Close": "Close", - "Discord": config_1.FColor.discord("Discord") - }[str]); }); + }); _this.sendMessage("[gold]Welcome to Fish Community!\n[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ".concat(config_1.FColor.discord(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Join our Discord"], ["Join our Discord"]))), " to request a staff member come online if none are on.")); } } diff --git a/build/scripts/staffCommands.js b/build/scripts/staffCommands.js index 2b307b7..5b96460 100644 --- a/build/scripts/staffCommands.js +++ b/build/scripts/staffCommands.js @@ -74,7 +74,7 @@ exports.commands = (0, commands_1.commandList)({ if (args.player.hasPerm("blockTrolling")) (0, commands_1.fail)("Player ".concat(args.player, " is insufficiently trollable.")); var message = (_b = args.message) !== null && _b !== void 0 ? _b : "You have been warned. I suggest you stop what you're doing"; - (0, menus_1.menu)('Warning', message, ["[green]Accept"], args.player); + (0, menus_1.menu)('Warning', message, [new menus_1.GUI_Container(["[green]Accept"])], args.player); (0, utils_1.logAction)('warned', sender, args.player, message); outputSuccess(f(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Warned player ", " for \"", "\""], ["Warned player ", " for \"", "\""])), args.player, message)); } @@ -278,22 +278,22 @@ exports.commands = (0, commands_1.commandList)({ else { possiblePlayers = players_1.FishPlayer.recentLeaves.map(function (p) { return p.info(); }); } - (0, menus_1.menu)("Stop", "Choose a player to mark", possiblePlayers, sender, function (_a) { - var optionPlayer = _a.option, sender = _a.sender; + (0, menus_1.listMenu)("Stop", "Choose a player to mark", new menus_1.GUI_Container(possiblePlayers, "auto", function (p) { return p.lastName; }), sender, function (_a) { + var optionPlayer = _a.data; if (args.time == null) { - (0, menus_1.menu)("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, function (_a) { - var optionTime = _a.option, sender = _a.sender; + (0, menus_1.menu)("Stop", "Select stop time", [new menus_1.GUI_Container(["2 days", "7 days", "30 days", "forever"])], sender, function (_a) { + var optionTime = _a.text; var time = optionTime == "2 days" ? 172800000 : optionTime == "7 days" ? 604800000 : optionTime == "30 days" ? 2592000000 : (globals_1.maxTime - Date.now() - 10000); stop(optionPlayer, time); - }, false); + }); } else { stop(optionPlayer, args.time); } - }, true, function (p) { return p.lastName; }); + }); } }, restart: { @@ -419,9 +419,9 @@ exports.commands = (0, commands_1.commandList)({ if ((data_1 = admins.getInfoOptional(uuid_1)) != null && data_1.admin) (0, commands_1.fail)("Cannot ban an admin."); var name = data_1 ? "".concat((0, funcs_2.escapeStringColorsClient)(data_1.lastName), " (").concat(uuid_1, "/").concat(data_1.lastIP, ")") : uuid_1; - (0, menus_1.menu)("Confirm", "Are you sure you want to ban ".concat(name, "?"), ["[red]Yes", "[green]Cancel"], sender, function (_a) { - var confirm = _a.option; - if (confirm != "[red]Yes") + (0, menus_1.menu)("Confirm", "Are you sure you want to ban ".concat(name, "?"), [new menus_1.GUI_Confirm()], sender, function (_a) { + var confirm = _a.data; + if (!confirm) (0, commands_1.fail)("Cancelled."); admins.banPlayerID(uuid_1); if (data_1) { @@ -440,15 +440,15 @@ exports.commands = (0, commands_1.commandList)({ outputSuccess(f(templateObject_28 || (templateObject_28 = __makeTemplateObject(["Banned player ", ". [yellow]Unable to determine IP.[]"], ["Banned player ", ". [yellow]Unable to determine IP.[]"])), uuid_1)); } (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); - }, false); + }); return; } else if (args.uuid_or_ip && globals_2.ipPattern.test(args.uuid_or_ip)) { //Overload 2: ban by uuid var ip_1 = args.uuid_or_ip; - (0, menus_1.menu)("Confirm", "Are you sure you want to ban IP ".concat(ip_1, "?"), ["[red]Yes", "[green]Cancel"], sender, function (_a) { - var confirm = _a.option; - if (confirm != "[red]Yes") + (0, menus_1.menu)("Confirm", "Are you sure you want to ban IP ".concat(ip_1, "?"), [new menus_1.GUI_Confirm()], sender, function (_a) { + var confirm = _a.data; + if (!confirm) (0, commands_1.fail)("Cancelled."); api.ban({ ip: ip_1 }); var info = admins.findByIP(ip_1); @@ -464,26 +464,40 @@ exports.commands = (0, commands_1.commandList)({ outputSuccess(f(templateObject_30 || (templateObject_30 = __makeTemplateObject(["IP ", " has been banned. Ban was synced to other servers."], ["IP ", " has been banned. Ban was synced to other servers."])), ip_1)); } (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); - }, false); + }); return; } //Overload 3: ban by menu - (0, menus_1.menu)("[scarlet]BAN[]", "Choose a player to ban.", (0, funcs_5.setToArray)(Groups.player), sender, function (_a) { - var option = _a.option; - if (option.admin) + /* + menu(`[scarlet]BAN[]`, "Choose a player to ban.", [new GUI_Container(setToArray(Groups.player), "auto", opt => opt.name), new GUI_Cancel()], sender, ({data:target}) => { + if(target.admin) fail(`Cannot ban an admin.`); + menu("Confirm", `Are you sure you want to ban ${target.name}?`, [new GUI_Confirm()], sender, ({data:confirm}) => { + if(!confirm) fail("Cancelled."); + admins.banPlayerIP(target.ip()); //this also bans the UUID + api.ban({ip: target.ip(), uuid: target.uuid()}); + Log.info(`${target.ip()}/${target.uuid()} was banned.`); + logAction("banned", sender, target.getInfo()); + outputSuccess(f`Banned player ${target}.`); + updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); + }); + }); + */ + (0, menus_1.listMenu)("[scarlet]BAN[]", "Choose a player to ban.", new menus_1.GUI_Container((0, funcs_5.setToArray)(Groups.player), "auto", function (opt) { return opt.name; }), sender, function (_a) { + var target = _a.data; + if (target.admin) (0, commands_1.fail)("Cannot ban an admin."); - (0, menus_1.menu)("Confirm", "Are you sure you want to ban ".concat(option.name, "?"), ["[red]Yes", "[green]Cancel"], sender, function (_a) { - var confirm = _a.option; - if (confirm != "[red]Yes") + (0, menus_1.menu)("Confirm", "Are you sure you want to ban ".concat(target.name, "?"), [new menus_1.GUI_Confirm()], sender, function (_a) { + var confirm = _a.data; + if (!confirm) (0, commands_1.fail)("Cancelled."); - admins.banPlayerIP(option.ip()); //this also bans the UUID - api.ban({ ip: option.ip(), uuid: option.uuid() }); - Log.info("".concat(option.ip(), "/").concat(option.uuid(), " was banned.")); - (0, utils_1.logAction)("banned", sender, option.getInfo()); - outputSuccess(f(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Banned player ", "."], ["Banned player ", "."])), option)); + admins.banPlayerIP(target.ip()); //this also bans the UUID + api.ban({ ip: target.ip(), uuid: target.uuid() }); + Log.info("".concat(target.ip(), "/").concat(target.uuid(), " was banned.")); + (0, utils_1.logAction)("banned", sender, target.getInfo()); + outputSuccess(f(templateObject_31 || (templateObject_31 = __makeTemplateObject(["Banned player ", "."], ["Banned player ", "."])), target)); (0, utils_1.updateBans)(function (player) { return "[scarlet]Player [yellow]".concat(player.name, "[scarlet] has been whacked by ").concat(sender.prefixedName, "."); }); - }, false); - }, true, function (opt) { return opt.name; }); + }); + }); } }, kill: { @@ -510,9 +524,9 @@ exports.commands = (0, commands_1.commandList)({ handler: function (_a) { var _b = _a.args, team = _b.team, unit = _b.unit, sender = _a.sender, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, f = _a.f; if (team) { - (0, menus_1.menu)("Confirm", "This will kill [scarlet]every ".concat(unit ? unit.localizedName : "unit", "[] on the team ").concat(team.coloredName(), "."), ["[orange]Kill units[]", "[green]Cancel[]"], sender, function (_a) { - var option = _a.option; - if (option == "[orange]Kill units[]") { + (0, menus_1.menu)("Confirm", "This will kill [scarlet]every ".concat(unit ? unit.localizedName : "unit", "[] on the team ").concat(team.coloredName(), "."), [new menus_1.GUI_Confirm()], sender, function (_a) { + var confirm = _a.data; + if (confirm) { if (unit) { var i_1 = 0; team.data().units.each(function (u) { return u.type == unit; }, function (u) { @@ -529,12 +543,12 @@ exports.commands = (0, commands_1.commandList)({ } else outputFail("Cancelled."); - }, false); + }); } else { - (0, menus_1.menu)("Confirm", "This will kill [scarlet]every single ".concat(unit ? unit.localizedName : "unit", "[]."), ["[orange]Kill all units[]", "[green]Cancel[]"], sender, function (_a) { - var option = _a.option; - if (option == "[orange]Kill all units[]") { + (0, menus_1.menu)("Confirm", "This will kill [scarlet]every single ".concat(unit ? unit.localizedName : "unit", "[]."), [new menus_1.GUI_Confirm], sender, function (_a) { + var option = _a.data; + if (option) { if (unit) { var i_2 = 0; Groups.unit.each(function (u) { return u.type == unit; }, function (u) { @@ -551,7 +565,7 @@ exports.commands = (0, commands_1.commandList)({ } else outputFail("Cancelled."); - }, false); + }); } } }, @@ -562,28 +576,28 @@ exports.commands = (0, commands_1.commandList)({ handler: function (_a) { var team = _a.args.team, sender = _a.sender, outputSuccess = _a.outputSuccess, outputFail = _a.outputFail, f = _a.f; if (team) { - (0, menus_1.menu)("Confirm", "This will kill [scarlet]every building[] on the team ".concat(team.coloredName(), ", except cores."), ["[orange]Kill buildings[]", "[green]Cancel[]"], sender, function (_a) { - var option = _a.option; - if (option == "[orange]Kill buildings[]") { + (0, menus_1.menu)("Confirm", "This will kill [scarlet]every building[] on the team ".concat(team.coloredName(), ", except cores."), [new menus_1.GUI_Confirm()], sender, function (_a) { + var confirm = _a.data; + if (confirm) { var count = team.data().buildings.size; team.data().buildings.each(function (b) { return !(b.block instanceof CoreBlock); }, function (b) { return b.tile.remove(); }); outputSuccess(f(templateObject_38 || (templateObject_38 = __makeTemplateObject(["Killed ", " buildings on ", ""], ["Killed ", " buildings on ", ""])), count, team)); } else outputFail("Cancelled."); - }, false); + }); } else { - (0, menus_1.menu)("Confirm", "This will kill [scarlet]every building[] except cores.", ["[orange]Kill buildings[]", "[green]Cancel[]"], sender, function (_a) { - var option = _a.option; - if (option == "[orange]Kill buildings[]") { + (0, menus_1.menu)("Confirm", "This will kill [scarlet]every building[] except cores.", [new menus_1.GUI_Confirm()], sender, function (_a) { + var confirm = _a.data; + if (confirm) { var count = Groups.build.size(); Groups.build.each(function (b) { return !(b.block instanceof CoreBlock); }, function (b) { return b.tile.remove(); }); outputSuccess(f(templateObject_39 || (templateObject_39 = __makeTemplateObject(["Killed ", " buildings."], ["Killed ", " buildings."])), count)); } else outputFail("Cancelled."); - }, false); + }); } } }, @@ -935,13 +949,16 @@ exports.commands = (0, commands_1.commandList)({ if (matches_1.isEmpty()) (0, commands_1.fail)(f(templateObject_60 || (templateObject_60 = __makeTemplateObject(["No stored data matched name ", ""], ["No stored data matched name ", ""])), input)); output(f(templateObject_61 || (templateObject_61 = __makeTemplateObject(["[accent]Found ", " match", " for search \"", "\"."], ["[accent]Found ", " match", " for search \"", "\"."])), matches_1.size, matches_1.size == 1 ? "" : "es", input)); - var displayMatches = function () { + var displayMatches_1 = function () { matches_1.each(function (info) { return output(f(templateObject_62 || (templateObject_62 = __makeTemplateObject(["[accent]Player with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""], ["[accent]\\\nPlayer with uuid ", "\nLast name used: \"", "\" [gray](", ")[] [[", "]\nIPs used: ", ""])), info.id, info.plainLastName(), (0, funcs_2.escapeStringColorsClient)(info.lastName), info.names.map(funcs_2.escapeStringColorsClient).items.join(", "), info.ips.map(function (i) { return "[blue]".concat(i, "[]"); }).toString(", "))); }); }; if (matches_1.size > 20) - (0, menus_1.menu)("Confirm", "Are you sure you want to view all ".concat(matches_1.size, " matches?"), ["Yes"], sender, displayMatches); - else - displayMatches(); + (0, menus_1.menu)("Confirm", "Are you sure you want to view all ".concat(matches_1.size, " matches?"), [new menus_1.GUI_Confirm()], sender, function (_a) { + var confirm = _a.data; + if (!confirm) + (0, commands_1.fail)("aborted."); + displayMatches_1(); + }); } } }, diff --git a/package.json b/package.json index 35cf62c..99dcf49 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,8 @@ "esbuild": "^0.23.1", "jasmine": "^5.5.0", "typescript": "^5.7.3" + }, + "directories": { + "doc": "docs" } } diff --git a/src/commands.ts b/src/commands.ts index 73b8328..556c3a9 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -6,7 +6,7 @@ This file contains the commands system. import { FColor, Gamemode, GamemodeName, text } from "./config"; import { ipPattern, uuidPattern } from "./globals"; -import { menu } from "./menus"; +import { GUI_Cancel, GUI_Container, listMenu, menu, pageMenu } from "./menus"; import { FishPlayer } from "./players"; import { Rank, RankName, RoleFlag } from "./ranks"; import type { ClientCommandHandler, CommandArg, FishCommandArgType, FishCommandData, FishCommandHandlerData, FishCommandHandlerUtils, FishConsoleCommandData, Formattable, PartialFormatString, SelectEnumClassKeys, ServerCommandHandler } from "./types"; @@ -676,10 +676,16 @@ function resolveArgsRecursive(processedArgs: Record, case "player": Groups.player.each(player => optionsList.push(player)); break; default: crash(`Unable to resolve arg of type ${argToResolve.type}`); } - menu(`Select a player`, `Select a player for the argument "${argToResolve.name}"`, optionsList, sender, ({option}) => { - processedArgs[argToResolve.name] = FishPlayer.get(option); + /* + menu(`Select a player`, `Select a player for the argument "${argToResolve.name}"`, [new GUI_Container(optionsList, "auto", player => Strings.stripColors(player.name).length >= 3 ? Strings.stripColors(player.name) : escapeStringColorsClient(player.name)), new GUI_Cancel()], sender, ({data}) => { + processedArgs[argToResolve.name] = FishPlayer.get(data); resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback); - }, true, player => Strings.stripColors(player.name).length >= 3 ? Strings.stripColors(player.name) : escapeStringColorsClient(player.name)) + }, ) + */ + listMenu(`Select a player`, `Select a player for the argument ${argToResolve.name}`,new GUI_Container(optionsList, "auto", (player:Player) => {return player.name}), sender, ({data}) => { + processedArgs[argToResolve.name] = FishPlayer.get(data); + resolveArgsRecursive(processedArgs, unresolvedArgs, sender, callback); + }) } diff --git a/src/menus.ts b/src/menus.ts index 6fa9a44..4c1e58c 100644 --- a/src/menus.ts +++ b/src/menus.ts @@ -9,109 +9,199 @@ import { outputFail, outputSuccess } from "./utils"; import { parseError } from './funcs'; import { to2DArray } from './funcs'; + +//#region Draw Menu + /** Stores a mapping from name to the numeric id of a listener that has been registered. */ -const registeredListeners:{ - [index:string]: number; +const registeredListeners: { + [index: string]: number; } = {}; /** Stores all listeners in use by fish-commands. */ const listeners = ( - void>>(d:T) => d + void>>(d: T) => d )({ - generic(player, option){ + generic(player, option) { const fishSender = FishPlayer.get(player); - if(option === -1 || option === fishSender.activeMenu.cancelOptionId) return; + if (option === -1 || option === fishSender.activeMenu.cancelOptionId) return; const prevCallback = fishSender.activeMenu.callback; fishSender.activeMenu.callback?.(fishSender, option); //if the callback wasn't modified, then clear it - if(fishSender.activeMenu.callback === prevCallback) + if (fishSender.activeMenu.callback === prevCallback) fishSender.activeMenu.callback = undefined; //otherwise, the menu spawned another menu that needs to be handled }, - none(player, option){ + none(player, option) { //do nothing } }); /** Registers all listeners, should be called on server load. */ -export function registerListeners(){ - for(const [key, listener] of Object.entries(listeners)){ +export function registerListeners() { + for (const [key, listener] of Object.entries(listeners)) { registeredListeners[key] ??= Menus.registerMenu(listener); } } + + /** Displays a menu to a player. */ -function menu(title:string, description:string, options:string[], target:FishPlayer):void; +function menu(title: string, description: string, elements: GUI_Element[], target: FishPlayer): void; /** Displays a menu to a player with callback. */ -function menu( - title:string, description:string, options:T[], target:FishPlayer, +function menu( + title: string, description: string, elements: GUI_Element[], target: FishPlayer, callback: (opts: { - option:T, sender:FishPlayer, outputSuccess:(message:string) => void, outputFail:(message:string) => void; + data: any, text: string, sender: FishPlayer, outputSuccess: (message: string) => void, outputFail: (message: string) => void; }) => void, - includeCancel?:boolean, optionStringifier?:(opt:T) => string, columns?:number -):void; + +): void; //this is a minor abomination but theres no good way to do overloads in typescript -function menu( - title:string, description:string, options:T[], target:FishPlayer, +function menu( + title: string, description: string, elements: GUI_Element[], target: FishPlayer, callback?: (opts: { - option:T, sender:FishPlayer, outputSuccess:(message:string) => void, outputFail:(message:string) => void; + data: any, text: string, sender: FishPlayer, outputSuccess: (message: string) => void, outputFail: (message: string) => void; }) => void, - includeCancel:boolean = true, - optionStringifier:(opt:T) => string = t => t as unknown as string, //this is dubious - columns:number = 3, -){ +) { + //target.activeMenu.cancelOptionId = -1; GUI_Cancel handles cancel already + + let ArrangedElements = { data: [] as any[][], stringified: [] as string[][] } + elements.forEach(element => { + ArrangedElements.data.push(...element.data()); + }); + elements.forEach(element => ArrangedElements.stringified.push(...element.format())); - if(!callback){ + //flatten to arrays + let PackedElements = { data: ArrangedElements.data.flat(), stringified: ArrangedElements.stringified.flat() } + if (PackedElements.data.length == 0) { + ArrangedElements.stringified.push([""]) + ArrangedElements.data.push([null]); // not needed, but nice to keep data and string in sync. + } + + if (!callback) { //overload 1, just display a menu with no callback - Call.menu(target.con, registeredListeners.none, title, description, options.length == 0 ? [[""]] : to2DArray(options.map(optionStringifier), columns)); + Call.menu(target.con, registeredListeners.none, title, description, ArrangedElements.stringified); } else { //overload 2, display a menu with callback - //Set up the 2D array of options, and add cancel - //Use "" as a fallback, because Call.menu with an empty array of options causes a client crash - const arrangedOptions = (options.length == 0 && !includeCancel) ? [[""]] : to2DArray(options.map(optionStringifier), columns); - if(includeCancel){ - arrangedOptions.push(["Cancel"]); - target.activeMenu.cancelOptionId = options.length; - } else { - target.activeMenu.cancelOptionId = -1; - } - //The target fishPlayer has a property called activeMenu, which stores information about the last menu triggered. - target.activeMenu.callback = (fishSender, option) => { + target.activeMenu.callback = (_fishSender, option) => { //Additional permission validation could be done here, but the only way that callback() can be called is if the above statement executed, //and on sensitive menus such as the stop menu, the only way to reach that is if menu() was called by the /stop command, //which already checks permissions. //Additionally, the callback is cleared by the generic menu listener after it is executed. //We do need to validate option though, as it can be any number. - if(!(option in options)) return; + if (!(option in PackedElements.data)) return; + if (typeof PackedElements.data[option] === 'string' && PackedElements.data[option] == "cancel") { return; } // cancel button pressed, no need to callback try { callback({ - option: options[option], + data: PackedElements.data[option], + text: PackedElements.stringified[option], sender: target, outputFail: message => outputFail(message, target), outputSuccess: message => outputSuccess(message, target), }); - } catch(err){ - if(err instanceof CommandError){ + } catch (err) { + if (err instanceof CommandError) { //If the error is a command error, then just outputFail outputFail(err.data, target); } else { target.sendMessage(`[scarlet]\u274C An error occurred while executing the command!`); - if(target.hasPerm("seeErrorMessages")) target.sendMessage(parseError(err)); + if (target.hasPerm("seeErrorMessages")) target.sendMessage(parseError(err)); Log.err(`Unhandled error in menu callback: ${target.cleanedName} submitted menu "${title}" "${description}"`); Log.err(err as Error); } } }; - - Call.menu(target.con, registeredListeners.generic, title, description, arrangedOptions); + + Call.menu(target.con, registeredListeners.generic, title, description, ArrangedElements.stringified); } } +//#endregion +//#region Draw Page Menus +//draws a page menu with arbitrary pages +export function pageMenu(title: string, description: string, elements: GUI_Element[][], target: FishPlayer, callback: (opts: { data: any, text: string, sender: FishPlayer, outputSuccess: (message: string) => void, outputFail: (message: string) => void; }) => void) { + let pages = elements.length + function drawpage(index: number) { + let e: GUI_Element[] = []; + if(!pages){ + e.push(new GUI_Cancel()); + }else{ + e.push(...elements[index]) + e.push(new GUI_Page(index + 1, pages)) + } + menu(title, description, e, target, (res) => { + // handle control element of the ui + if (typeof res.data === 'string') { + switch (res.data) { + case "left": + drawpage((index == 0) ? (0) : (index - 1)); + break; + case "right": + drawpage((index == pages - 1) ? (pages - 1) : (index + 1)); + break; + case "center": + drawpage(index); + break; + default: + callback(res); + break; + } + } + }) + return; + } + drawpage(0); +} +//TODO make list a GUI_Element[] instead of a single Container +//TODO use GUI_Element for formatting instead of defaulting to single column +export function listMenu(title: string, description: string, list: GUI_Container, target: FishPlayer, callback: (opts: { data: any, text: string, sender: FishPlayer, outputSuccess: (message: string) => void, outputFail: (message: string) => void; }) => void, pageSize: number = 10) { + let pooledData: any[] = []; + list.data().flat().forEach((data) => { pooledData.push(data) }); + let pagedData: any[][] = pooledData.reduce((res, _, index) => { if (index % pageSize === 0) { res.push(pooledData.slice(index, index + pageSize)); } return res; }, [] as any[][]); + let pagesElements: GUI_Element[][] = []; + pagedData.forEach(pageData => pagesElements.push([new GUI_Container(pageData, 1, list.stringifier)])); + pageMenu(title, description, pagesElements, target, (res) => {Log.info(`${res.data}`);callback(res)}) +} +//#endregion +//#region GUI Elements + +interface GUI_Element { + format(): string[][], // honestly should have made this a 1d array for simplicity, but 2d lets you define multi-row elements + data(): any[][] +} +export class GUI_Container implements GUI_Element { + constructor( + public options: any[], + public columns: number | "auto" = 3, + public stringifier: ((option: any) => string) = option => option as unknown as string + ) { }; + format = () => { return (to2DArray(this.options.map(this.stringifier), (this.columns == 'auto') ? (3) : (this.columns))) }; + data = () => { return to2DArray(this.options, (this.columns == 'auto') ? (3) : (this.columns)) }; +} +export class GUI_Cancel implements GUI_Element { + format = () => { return ([["cancel"]]) }; + data = () => { return ([["cancel"]]) } +} +export class GUI_Page implements GUI_Element { + constructor( + public currentPage: number, + public pages: number + ) { } + public format = () => { return (to2DArray(["<--", `${this.currentPage}/${this.pages}`, "-->"], 3)) }; + public data = () => { return ([["left", "center", "right"]]) } + +} +export class GUI_Confirm implements GUI_Element { + public format = () => { return [["[green]Yes, do it", "[red] No, cancel"]] } + public data = () => { return [[true, false]] } +} +//#endregion +//#region Exports export { registeredListeners as listeners, menu }; +//#endregion diff --git a/src/playerCommands.ts b/src/playerCommands.ts index a1da1b4..0d64ece 100644 --- a/src/playerCommands.ts +++ b/src/playerCommands.ts @@ -7,7 +7,7 @@ import * as api from './api'; import { command, commandList, fail, formatArg, Perm, Req } from './commands'; import { FishServer, Gamemode, rules, text } from './config'; import { FishEvents, fishPlugin, fishState, ipPortPattern, recentWhispers, tileHistory, uuidPattern } from './globals'; -import { menu } from './menus'; +import { GUI_Cancel, GUI_Container, listMenu, menu } from './menus'; import { FishPlayer } from './players'; import { Rank, RoleFlag } from './ranks'; import type { FishCommandData } from './types'; @@ -47,9 +47,9 @@ export const commands = commandList({ perm: Perm.play, requirements: [Req.modeNot("pvp")], handler({ args, sender }) { - if(!sender.unit()?.spawnedByCore) fail(`Can only teleport while in a core unit.`); - if(sender.team() !== args.player.team()) fail(`Cannot teleport to players on another team.`); - if(sender.unit().hasPayload?.()) fail(`Cannot teleport to players while holding a payload.`); + if (!sender.unit()?.spawnedByCore) fail(`Can only teleport while in a core unit.`); + if (sender.team() !== args.player.team()) fail(`Cannot teleport to players on another team.`); + if (sender.unit().hasPayload?.()) fail(`Cannot teleport to players while holding a payload.`); teleportPlayer(sender.player!, args.player.player!); }, }, @@ -59,13 +59,13 @@ export const commands = commandList({ description: 'Removes all boulders from the map.', perm: Perm.play, requirements: [Req.cooldownGlobal(100_000)], - handler({sender, outputSuccess}){ + handler({ sender, outputSuccess }) { Timer.schedule( () => Call.sound(sender.con, Sounds.rockBreak, 1, 1, 0), 0, 0.05, 10 ); - Vars.world.tiles.eachTile((t:Tile) => { - if(t.breakable() && t.block() instanceof Prop){ + Vars.world.tiles.eachTile((t: Tile) => { + if (t.breakable() && t.block() instanceof Prop) { t.removeNet(); } }); @@ -97,9 +97,9 @@ export const commands = commandList({ args: ['persist:boolean?'], description: 'Checks the history of a tile.', perm: Perm.none, - handler({args, output, outputSuccess, currentTapMode, handleTaps}){ - if(currentTapMode == "off"){ - if(args.persist){ + handler({ args, output, outputSuccess, currentTapMode, handleTaps }) { + if (currentTapMode == "off") { + if (args.persist) { handleTaps("on"); outputSuccess(`Tilelog mode enabled. Click tiles to check their recent history. Run /tilelog again to disable.`); } else { @@ -111,7 +111,7 @@ export const commands = commandList({ outputSuccess(`Tilelog disabled.`); } }, - tapped({tile, x, y, output, sender, admins}){ + tapped({ tile, x, y, output, sender, admins }) { const historyData = tileHistory[`${x},${y}`] ?? fail(`There is no recorded history for the selected tile (${tile.x}, ${tile.y}).`); const history = StringIO.read(historyData, str => str.readArray(d => ({ action: d.readString(2), @@ -121,10 +121,10 @@ export const commands = commandList({ }), 1)); output(`[yellow]Tile history for tile (${tile.x}, ${tile.y}):\n` + history.map(e => uuidPattern.test(e.uuid) - ? (sender.hasPerm("viewUUIDs") - ? `[yellow]${admins.getInfoOptional(e.uuid)?.plainLastName()}[lightgray](${e.uuid})[yellow] ${e.action} a [cyan]${e.type}[] ${formatTimeRelative(e.time)}` - : `[yellow]${admins.getInfoOptional(e.uuid)?.plainLastName()} ${e.action} a [cyan]${e.type}[] ${formatTimeRelative(e.time)}`) - : `[yellow]${e.uuid}[yellow] ${e.action} a [cyan]${e.type}[] ${formatTimeRelative(e.time)}` + ? (sender.hasPerm("viewUUIDs") + ? `[yellow]${admins.getInfoOptional(e.uuid)?.plainLastName()}[lightgray](${e.uuid})[yellow] ${e.action} a [cyan]${e.type}[] ${formatTimeRelative(e.time)}` + : `[yellow]${admins.getInfoOptional(e.uuid)?.plainLastName()} ${e.action} a [cyan]${e.type}[] ${formatTimeRelative(e.time)}`) + : `[yellow]${e.uuid}[yellow] ${e.action} a [cyan]${e.type}[] ${formatTimeRelative(e.time)}` ).join('\n')); } }, @@ -136,7 +136,7 @@ export const commands = commandList({ handler({ sender, outputSuccess }) { sender.manualAfk = !sender.manualAfk; sender.updateName(); - if(sender.manualAfk) outputSuccess(`You are now marked as AFK.`); + if (sender.manualAfk) outputSuccess(`You are now marked as AFK.`); else outputSuccess(`You are no longer marked as AFK.`); }, }, @@ -144,29 +144,29 @@ export const commands = commandList({ args: ['target:player?'], description: `Toggles visibility of your rank and flags.`, perm: Perm.vanish, - handler({ args, sender, outputSuccess }){ - if(sender.stelled()) fail(`Marked players may not hide flags.`); - if(sender.muted) fail(`Muted players may not hide flags.`); + handler({ args, sender, outputSuccess }) { + if (sender.stelled()) fail(`Marked players may not hide flags.`); + if (sender.muted) fail(`Muted players may not hide flags.`); args.target ??= sender; - if(sender != args.target && args.target.hasPerm("blockTrolling")) fail(`Target is insufficentlly trollable.`); - if(sender != args.target && !sender.ranksAtLeast("mod")) fail(`You do not have permission to vanish other players.`); + if (sender != args.target && args.target.hasPerm("blockTrolling")) fail(`Target is insufficentlly trollable.`); + if (sender != args.target && !sender.ranksAtLeast("mod")) fail(`You do not have permission to vanish other players.`); args.target.showRankPrefix = !args.target.showRankPrefix; outputSuccess( -`${args.target == sender ? `Your` : `${args.target.name}'s`} rank prefix is now ${args.target.showRankPrefix ? "visible" : "hidden"}.` + `${args.target == sender ? `Your` : `${args.target.name}'s`} rank prefix is now ${args.target.showRankPrefix ? "visible" : "hidden"}.` ); }, }, - + tileid: { args: [], description: 'Checks id of a tile.', perm: Perm.none, - handler({output, handleTaps}){ + handler({ output, handleTaps }) { handleTaps("once"); output(`Click a tile to see its id...`); }, - tapped({output, f, tile}){ + tapped({ output, f, tile }) { output(f`ID is ${tile.block().id}`); } }, @@ -191,11 +191,11 @@ export const commands = commandList({ args: ["server:string", "target:player?"], description: "Switches to another server.", perm: Perm.play, - handler({args, sender, f}){ - if(args.target != null && args.target != sender && !sender.canModerate(args.target, true, "admin", true)) + handler({ args, sender, f }) { + if (args.target != null && args.target != sender && !sender.canModerate(args.target, true, "admin", true)) fail(f`You do not have permission to switch player ${args.target}.`); const target = args.target ?? sender; - if(ipPortPattern.test(args.server) && sender.hasPerm("admin")){ + if (ipPortPattern.test(args.server) && sender.hasPerm("admin")) { //direct connect Call.connect(target.con, ...args.server.split(":")); } else { @@ -204,10 +204,10 @@ export const commands = commandList({ ?? fail(unknownServerMessage); //Pretend the server doesn't exist - if(server.requiredPerm && !sender.hasPerm(server.requiredPerm)) + if (server.requiredPerm && !sender.hasPerm(server.requiredPerm)) fail(unknownServerMessage); - if(target == sender) + if (target == sender) FishPlayer.messageAllWithPerm(server.requiredPerm, `${sender.name}[magenta] has gone to the ${server.name} server. Use [cyan]/${server.name} [magenta]to join them!`); Call.connect(target.con, server.ip, server.port); @@ -219,17 +219,17 @@ export const commands = commandList({ args: ['message:string'], description: `Sends a message to staff only.`, perm: Perm.chat, - handler({ sender, args, outputSuccess, outputFail, lastUsedSender }){ - if(!sender.hasPerm("mod")){ - if(Date.now() - lastUsedSender < 4000) fail(`This command was used recently and is on cooldown. [orange]Misuse of this command may result in a mute.`); + handler({ sender, args, outputSuccess, outputFail, lastUsedSender }) { + if (!sender.hasPerm("mod")) { + if (Date.now() - lastUsedSender < 4000) fail(`This command was used recently and is on cooldown. [orange]Misuse of this command may result in a mute.`); } api.sendStaffMessage(args.message, sender.name, (sent) => { - if(!sender.hasPerm("mod")){ - if(sent){ + if (!sender.hasPerm("mod")) { + if (sent) { outputSuccess(`Message sent to [orange]all online staff.`); } else { const wasReceived = FishPlayer.messageStaff(sender.prefixedName, args.message); - if(wasReceived) outputSuccess(`Message sent to staff.`); + if (wasReceived) outputSuccess(`Message sent to staff.`); else outputFail(`No staff were online to receive your message.`); } } @@ -250,16 +250,16 @@ export const commands = commandList({ description: `Watch/unwatch a player.`, perm: Perm.none, handler({ args, sender, outputSuccess, outputFail }) { - if(sender.watch){ + if (sender.watch) { outputSuccess(`No longer watching a player.`); sender.watch = false; - } else if(args.player){ + } else if (args.player) { sender.watch = true; const stayX = sender.unit().x; const stayY = sender.unit().y; const target = args.player.player!; const watch = () => { - if(sender.watch){ + if (sender.watch) { // Self.X+(172.5-Self.X)/10 Call.setCameraPosition(sender.con, target.unit().x, target.unit().y); sender.unit().set(stayX, stayY); @@ -279,30 +279,30 @@ export const commands = commandList({ //TODO revise code /** Mapping between player and original team */ const spectators = new Map(); - function spectate(target:FishPlayer){ + function spectate(target: FishPlayer) { spectators.set(target, target.team()); target.forceRespawn(); target.setTeam(Team.derelict); target.forceRespawn(); } - function resume(target:FishPlayer){ - if(spectators.get(target) == null) return; // this state is possible for a person who left not in spectate + function resume(target: FishPlayer) { + if (spectators.get(target) == null) return; // this state is possible for a person who left not in spectate target.setTeam(spectators.get(target)!); spectators.delete(target); target.forceRespawn(); } Events.on(EventType.GameOverEvent, () => spectators.clear()); - Events.on(EventType.PlayerLeave, ({player}:{player:mindustryPlayer}) => resume(FishPlayer.get(player))); + Events.on(EventType.PlayerLeave, ({ player }: { player: mindustryPlayer }) => resume(FishPlayer.get(player))); return { args: ["target:player?"], description: `Toggles spectator mode in PVP games.`, perm: Perm.play, - handler({args, sender, outputSuccess, f}){ + handler({ args, sender, outputSuccess, f }) { args.target ??= sender; - if(!Gamemode.pvp() && !sender.hasPerm("mod")) fail(`You do not have permission to spectate on a non-pvp server.`); - if(args.target !== sender && args.target.hasPerm("blockTrolling")) fail(`Target player is insufficiently trollable.`); - if(args.target !== sender && !sender.ranksAtLeast("admin")) fail(`You do not have permission to force other players to spectate.`); - if(spectators.has(args.target)){ + if (!Gamemode.pvp() && !sender.hasPerm("mod")) fail(`You do not have permission to spectate on a non-pvp server.`); + if (args.target !== sender && args.target.hasPerm("blockTrolling")) fail(`Target player is insufficiently trollable.`); + if (args.target !== sender && !sender.ranksAtLeast("admin")) fail(`You do not have permission to force other players to spectate.`); + if (spectators.has(args.target)) { resume(args.target); outputSuccess(args.target == sender ? f`Rejoining game as team ${args.target.team()}.` @@ -313,7 +313,7 @@ export const commands = commandList({ outputSuccess(args.target == sender ? f`Now spectating. Run /spectate again to resume gameplay.` : f`Forced ${args.target} into spectator mode.`) - ; + ; } } }; @@ -334,7 +334,7 @@ export const commands = commandList({ //name is not a number or a category, therefore it is probably a command name if (args.name in allCommands && (!allCommands[args.name].isHidden || allCommands[args.name].perm.check(sender))) { output( -`Help for command ${args.name}: + `Help for command ${args.name}: ${allCommands[args.name].description} Usage: [sky]/${args.name} [white]${allCommands[args.name].args.map(formatArg).join(' ')} Permission required: ${allCommands[args.name].perm.name}` @@ -396,7 +396,7 @@ export const commands = commandList({ perm: Perm.chat, handler({ args, sender, output, f }) { const recipient = FishPlayer.getById(recentWhispers[sender.uuid] ?? fail(`It doesn't look like someone has messaged you recently. Try whispering to them with [white]"/msg "`)); - if(!(recipient?.connected())) fail(`The person who last messaged you doesn't seem to exist anymore. Try whispering to someone with [white]"/msg "`); + if (!(recipient?.connected())) fail(`The person who last messaged you doesn't seem to exist anymore. Try whispering to someone with [white]"/msg "`); recentWhispers[recentWhispers[sender.uuid]] = sender.uuid; recipient.sendMessage(`${sender.name}[lightgray] whispered:[#BBBBBB] ${args.message}`); output(f`[#BBBBBB]Message sent to ${recipient}[#BBBBBB].`); @@ -409,8 +409,8 @@ export const commands = commandList({ perm: Perm.none, handler({ args, sender, output, outputFail, outputSuccess }) { //overload 1: type not specified - if(!args.type){ - if(sender.trail != null){ + if (!args.type) { + if (sender.trail != null) { sender.trail = null; outputSuccess(`Trail turned off.`); } else { @@ -441,8 +441,8 @@ Available types:[yellow] }; const selectedType = trailTypes[args.type as keyof typeof trailTypes] as string | undefined; - if(!selectedType){ - if(Object.values(trailTypes).includes(args.type)) fail(`Please use the numeric id to refer to a trail type.`); + if (!selectedType) { + if (Object.values(trailTypes).includes(args.type)) fail(`Please use the numeric id to refer to a trail type.`); else fail(`"${args.type}" is not an available type.`); } @@ -454,7 +454,7 @@ Available types:[yellow] }; } else { outputFail( -`[scarlet]Sorry, "${args.color}" is not a valid color. + `[scarlet]Sorry, "${args.color}" is not a valid color. [yellow]Color can be in the following formats: [pink]pink [white]| [gray]#696969 [white]| 255,0,0.` ); @@ -466,11 +466,11 @@ Available types:[yellow] args: [], description: 'Spawns an ohno.', perm: Perm.play, - init(){ + init() { const Ohnos = { enabled: true, ohnos: new Array(), - makeOhno(team:Team, x:number, y:number){ + makeOhno(team: Team, x: number, y: number) { const ohno = UnitTypes.atrax.create(team); ohno.set(x, y); ohno.type = UnitTypes.alpha; @@ -480,14 +480,14 @@ Available types:[yellow] this.ohnos.push(ohno); return ohno; }, - updateLength(){ + updateLength() { this.ohnos = this.ohnos.filter(o => o && o.isAdded() && !o.dead); }, - killAll(){ + killAll() { this.ohnos.forEach(ohno => ohno?.kill?.()); this.ohnos = []; }, - amount(){ + amount() { return this.ohnos.length; }, }; @@ -497,17 +497,17 @@ Available types:[yellow] return Ohnos; }, requirements: [Req.gameRunning, Req.modeNot("pvp")], - handler({sender, data:Ohnos}){ - if(!Ohnos.enabled) fail(`Ohnos have been temporarily disabled.`); - if(!(sender.connected() && sender.unit().added && !sender.unit().dead)) fail(`You cannot spawn ohnos while dead.`); + handler({ sender, data: Ohnos }) { + if (!Ohnos.enabled) fail(`Ohnos have been temporarily disabled.`); + if (!(sender.connected() && sender.unit().added && !sender.unit().dead)) fail(`You cannot spawn ohnos while dead.`); Ohnos.updateLength(); - if( + if ( Ohnos.ohnos.length >= (Groups.player.size() + 1) || sender.team().data().countType(UnitTypes.alpha) >= Units.getCap(sender.team()) ) fail(`Sorry, the max number of ohno units has been reached.`); - if(nearbyEnemyTile(sender.unit(), 6) != null) fail(`Too close to an enemy tile!`); - if(!UnitTypes.alpha.supportsEnv(Vars.state.rules.env)) fail(`Ohnos cannot survive in this map.`); - + if (nearbyEnemyTile(sender.unit(), 6) != null) fail(`Too close to an enemy tile!`); + if (!UnitTypes.alpha.supportsEnv(Vars.state.rules.env)) fail(`Ohnos cannot survive in this map.`); + Ohnos.makeOhno(sender.team(), sender.player!.x, sender.player!.y); }, }), @@ -516,12 +516,12 @@ Available types:[yellow] args: [], description: 'Displays information about all ranks.', perm: Perm.none, - handler({ output }){ + handler({ output }) { output( `List of ranks:\n` + - Object.values(Rank.ranks) - .map((rank) => `${rank.prefix} ${rank.color}${capitalizeText(rank.name)}[]: ${rank.color}${rank.description}[]\n`) - .join("") + + Object.values(Rank.ranks) + .map((rank) => `${rank.prefix} ${rank.color}${capitalizeText(rank.name)}[]: ${rank.color}${rank.description}[]\n`) + .join("") + `List of flags:\n` + Object.values(RoleFlag.flags) .map((flag) => `${flag.prefix} ${flag.color}${capitalizeText(flag.name)}[]: ${flag.color}${flag.description}[]\n`) @@ -534,20 +534,20 @@ Available types:[yellow] args: ['player:player?'], description: 'Displays the server rules.', perm: Perm.none, - handler({args, sender, outputSuccess, f}){ + handler({ args, sender, outputSuccess, f }) { const target = args.player ?? sender; - if(target !== sender){ - if(!sender.hasPerm("warn")) fail(`You do not have permission to show rules to other players.`); - if(target.hasPerm("blockTrolling")) fail(f`Player ${args.player!} is insufficiently trollable.`); + if (target !== sender) { + if (!sender.hasPerm("warn")) fail(`You do not have permission to show rules to other players.`); + if (target.hasPerm("blockTrolling")) fail(f`Player ${args.player!} is insufficiently trollable.`); } menu( "Rules for [#0000ff]>|||> FISH [white]servers", rules.join("\n\n"), - ["[green]I agree to abide by these rules[]", "No"], target, - ({option}) => { - if(option == "No") target.kick("You must agree to the rules to play on this server. Rejoin to agree to the rules.", 1); - }, false + [new GUI_Container(["[green]I agree to abide by these rules[]", "No"])], target, + ({ text }) => { + if (text == "No") target.kick("You must agree to the rules to play on this server. Rejoin to agree to the rules.", 1); + } ); - if(target !== sender) outputSuccess(f`Reminded ${target} of the rules.`); + if (target !== sender) outputSuccess(f`Reminded ${target} of the rules.`); }, }, @@ -556,24 +556,24 @@ Available types:[yellow] description: 'Warns other players about power voids.', perm: Perm.play, requirements: [Req.mode("attack")], - handler({args, sender, lastUsedSuccessfullySender, lastUsedSuccessfully, outputSuccess, f}){ - if(args.player){ - if(Date.now() - lastUsedSuccessfullySender < 20000) fail(`This command was used recently and is on cooldown.`); - if(!sender.hasPerm("trusted")) fail(`You do not have permission to show popups to other players, please run /void with no arguments to send a chat message to everyone.`); - if(args.player !== sender && args.player.hasPerm("blockTrolling")) fail(`Target player is insufficiently trollable.`); + handler({ args, sender, lastUsedSuccessfullySender, lastUsedSuccessfully, outputSuccess, f }) { + if (args.player) { + if (Date.now() - lastUsedSuccessfullySender < 20000) fail(`This command was used recently and is on cooldown.`); + if (!sender.hasPerm("trusted")) fail(`You do not have permission to show popups to other players, please run /void with no arguments to send a chat message to everyone.`); + if (args.player !== sender && args.player.hasPerm("blockTrolling")) fail(`Target player is insufficiently trollable.`); menu("\uf83f [scarlet]WARNING[] \uf83f", -`[white]Don't break the Power Void (\uf83f), it's a trap! + `[white]Don't break the Power Void (\uf83f), it's a trap! Power voids disable anything they are connected to. If you break it, [scarlet]you will get attacked[] by enemy units. Please stop attacking and [lime]build defenses[] first!`, - ["I understand"], args.player + [new GUI_Container(["I understand"])], args.player ); logAction("showed void warning", sender, args.player); outputSuccess(f`Warned ${args.player} about power voids with a popup message.`); } else { - if(Date.now() - lastUsedSuccessfully < 10000) fail(`This command was used recently and is on cooldown.`); + if (Date.now() - lastUsedSuccessfully < 10000) fail(`This command was used recently and is on cooldown.`); Call.sendMessage( -`[white]Don't break the Power Void (\uf83f), it's a trap! + `[white]Don't break the Power Void (\uf83f), it's a trap! Power voids disable anything they are connected to. If you break it, [scarlet]you will get attacked[] by enemy units. Please stop attacking and [lime]build defenses[] first!` ); @@ -585,18 +585,18 @@ Please stop attacking and [lime]build defenses[] first!` args: ['team:team', 'target:player?'], description: 'Changes the team of a player.', perm: Perm.changeTeam, - handler({args, sender, outputSuccess, f}){ + handler({ args, sender, outputSuccess, f }) { args.target ??= sender; - if(!sender.canModerate(args.target, true, "mod", true)) fail(f`You do not have permission to change the team of ${args.target}`); - if(Gamemode.sandbox() && fishState.peacefulMode && !sender.hasPerm("admin")) fail(`You do not have permission to change teams because peaceful mode is on.`); - if(!sender.hasPerm("changeTeamExternal")){ - if(args.team.data().cores.size <= 0) fail(`You do not have permission to change to a team with no cores.`); - if(!sender.player!.dead() && !sender.unit()?.spawnedByCore) + if (!sender.canModerate(args.target, true, "mod", true)) fail(f`You do not have permission to change the team of ${args.target}`); + if (Gamemode.sandbox() && fishState.peacefulMode && !sender.hasPerm("admin")) fail(`You do not have permission to change teams because peaceful mode is on.`); + if (!sender.hasPerm("changeTeamExternal")) { + if (args.team.data().cores.size <= 0) fail(`You do not have permission to change to a team with no cores.`); + if (!sender.player!.dead() && !sender.unit()?.spawnedByCore) args.target.forceRespawn(); } - if(!sender.hasPerm("mod")) args.target.changedTeam = true; + if (!sender.hasPerm("mod")) args.target.changedTeam = true; args.target.setTeam(args.team); - if(args.target === sender) outputSuccess(f`Changed your team to ${args.team}.`); + if (args.target === sender) outputSuccess(f`Changed your team to ${args.team}.`); else outputSuccess(f`Changed team of player ${args.target} to ${args.team}.`); }, }, @@ -605,23 +605,23 @@ Please stop attacking and [lime]build defenses[] first!` args: ['player:player'], description: 'Displays the rank of a player.', perm: Perm.none, - handler({args, output, f}) { + handler({ args, output, f }) { output(f`Player ${args.player}'s rank is ${args.player.rank}.`); }, }, - + forcevnw: { args: ["force:boolean?"], description: 'Force skip to the next wave.', perm: Perm.admin, - handler({allCommands, sender, args:{force}}){ + handler({ allCommands, sender, args: { force } }) { force ??= true; - if(allCommands.vnw.data.manager.session == null){ - if(force == false) fail(`Cannot clear votes for VNW because no vote is currently ongoing.`); + if (allCommands.vnw.data.manager.session == null) { + if (force == false) fail(`Cannot clear votes for VNW because no vote is currently ongoing.`); skipWaves(1, false); } else { - if(force) Call.sendMessage(`VNW: [green]Vote was forced by admin [yellow]${sender.name}[green], skipping wave.`); + if (force) Call.sendMessage(`VNW: [green]Vote was forced by admin [yellow]${sender.name}[green], skipping wave.`); else Call.sendMessage(`VNW: [red]Votes cleared by admin [yellow]${sender.name}[red].`); allCommands.vnw.data.manager.forceVote(force); } @@ -641,44 +641,42 @@ Please stop attacking and [lime]build defenses[] first!` .on("player vote removed", (t, player) => Call.sendMessage(`VNW: ${player.name} [white] has left. [green]${t.currentVotes()}[white] votes, [green]${t.requiredVotes()}[white] required.`)) }), requirements: [Req.cooldown(3000), Req.mode("survival"), Req.gameRunning], - handler({sender, data:{manager}}){ + handler({ sender, data: { manager } }) { - if(!manager.session){ + if (!manager.session) { menu( "Start a Next Wave Vote", "Select the amount of waves you would like to skip, or click \"Cancel\" to abort.", - [1, 5, 10], + [new GUI_Container([1, 5, 10], "auto", n => `${n} waves`), new GUI_Cancel()], sender, - ({option}) => { - if(manager.session){ + ({ data }) => { + if (manager.session) { //Someone else started a vote - if(manager.session.data != option) fail(`Someone else started a vote with a different number of waves to skip.`); - else manager.vote(sender, sender.voteWeight(), option); + if (manager.session.data != data) fail(`Someone else started a vote with a different number of waves to skip.`); + else manager.vote(sender, sender.voteWeight(), data); } else { //this is still a race condition technically... shouldn't be that bad right? - manager.start(sender, sender.voteWeight(), option); + manager.start(sender, sender.voteWeight(), data); } }, - true, - n => `${n} waves` ); } else { manager.vote(sender, sender.voteWeight(), null); } - } + } }), forcertv: { args: ["force:boolean?"], description: 'Force skip to the next map.', perm: Perm.admin, - handler({args:{force}, sender, allCommands}){ + handler({ args: { force }, sender, allCommands }) { force ??= true; - if(allCommands.rtv.data.manager.session == null){ - if(force == false) fail(`Cannot clear votes for RTV because no vote is currently ongoing.`); + if (allCommands.rtv.data.manager.session == null) { + if (force == false) fail(`Cannot clear votes for RTV because no vote is currently ongoing.`); allCommands.rtv.data.manager.forceVote(true); } else { - if(force) Call.sendMessage(`RTV: [green]Vote was forced by admin [yellow]${sender.name}[green].`); + if (force) Call.sendMessage(`RTV: [green]Vote was forced by admin [yellow]${sender.name}[green].`); else Call.sendMessage(`RTV: [red]Votes cleared by admin [yellow]${sender.name}[red].`); allCommands.rtv.data.manager.forceVote(force); } @@ -698,7 +696,7 @@ Please stop attacking and [lime]build defenses[] first!` .on("player vote removed", (t, player) => Call.sendMessage(`RTV: ${player.name}[white] has left the game. [green]${t.currentVotes()}[white] votes, [green]${t.requiredVotes()}[white] required.`)) }), requirements: [Req.cooldown(3000), Req.gameRunning], - handler({sender, data:{manager}}){ + handler({ sender, data: { manager } }) { manager.vote(sender, 1, 0); //No weighting for RTV except for removing AFK players } }), @@ -730,9 +728,9 @@ Please stop attacking and [lime]build defenses[] first!` description: 'Override the next map in queue.', perm: Perm.admin, requirements: [Req.modeNot("hexed")], - handler({allCommands, args, sender, outputSuccess, f}){ + handler({ allCommands, args, sender, outputSuccess, f }) { Vars.maps.setNextMapOverride(args.map); - if(allCommands.nextmap.data.voteEndTime() > -1){ + if (allCommands.nextmap.data.voteEndTime() > -1) { //Cancel /nextmap vote if it's ongoing allCommands.nextmap.data.resetVotes(); Call.sendMessage(`[red]Admin ${sender.name}[red] has cancelled the vote. The next map will be [yellow]${args.map.name()}.`); @@ -747,15 +745,15 @@ Please stop attacking and [lime]build defenses[] first!` args: [], description: 'Lists the available maps.', perm: Perm.none, - handler({output}){ + handler({ output }) { output(`\ [yellow]Use [white]/nextmap [lightgray] [yellow]to vote on a map. [blue]Available maps: _________________________ ${Vars.maps.customMaps().toArray().map((map, i) => -`[yellow]${map.name()}` -).join("\n")}` + `[yellow]${map.name()}` + ).join("\n")}` ); } }, @@ -768,7 +766,7 @@ ${Vars.maps.customMaps().toArray().map((map, i) => const voteDuration = 1.5 * 60000; // 1.5 mins let task: TimerTask | null = null; - function resetVotes(){ + function resetVotes() { votes.clear(); voteEndTime = -1; task?.cancel(); @@ -776,32 +774,32 @@ ${Vars.maps.customMaps().toArray().map((map, i) => lastVoteTime = 1; } - function getMapData():Seq> { + function getMapData(): Seq> { return [...votes.values()].reduce( (acc, map) => (acc.increment(map), acc), new ObjectIntMap() ).entries().toArray(); } - function showVotes(){ + function showVotes() { Call.sendMessage(`\ [green]Current votes: ------------------------------ -${getMapData().map(({key:map, value:votes}) => -`[cyan]${map.name()}[yellow]: ${votes}` -).toString("\n")}` +${getMapData().map(({ key: map, value: votes }) => + `[cyan]${map.name()}[yellow]: ${votes}` + ).toString("\n")}` ); } - function startVote(){ + function startVote() { voteEndTime = Date.now() + voteDuration; task = Timer.schedule(endVote, voteDuration / 1000); } - function endVote(){ - if(voteEndTime == -1) return; //aborted somehow - if(votes.size == 0) return; //no votes? + function endVote() { + if (voteEndTime == -1) return; //aborted somehow + if (votes.size == 0) return; //no votes? - if((votes.size / Groups.player.size()) + 0.2 < lastVoteTurnout){ + if ((votes.size / Groups.player.size()) + 0.2 < lastVoteTurnout) { Call.sendMessage("[cyan]Next Map Vote: [scarlet]Vote aborted because a previous vote had significantly higher turnout"); resetVotes(); return; @@ -813,15 +811,15 @@ ${getMapData().map(({key:map, value:votes}) => const mapData = getMapData(); const highestVoteCount = mapData.max(floatf(e => e.value)).value; const highestVotedMaps = mapData.select(e => e.value == highestVoteCount); - let winner:MMap; + let winner: MMap; - if(highestVotedMaps.size > 1){ + if (highestVotedMaps.size > 1) { winner = highestVotedMaps.random()!.key; Call.sendMessage( -`[green]There was a tie between the following maps: -${highestVotedMaps.map(({key:map, value:votes}) => -`[cyan]${map.name()}[yellow]: ${votes}` -).toString("\n")} + `[green]There was a tie between the following maps: +${highestVotedMaps.map(({ key: map, value: votes }) => + `[cyan]${map.name()}[yellow]: ${votes}` + ).toString("\n")} [green]Picking random winner: [yellow]${winner.name()}` ); } else { @@ -836,22 +834,32 @@ ${highestVotedMaps.map(({key:map, value:votes}) => Events.on(EventType.ServerLoadEvent, resetVotes); return { - args: ['map:map'], + args: ['map:map?'], description: 'Allows you to vote for the next map. Use /maps to see all available maps.', perm: Perm.play, - data: {votes, voteEndTime: () => voteEndTime, resetVotes, endVote}, + data: { votes, voteEndTime: () => voteEndTime, resetVotes, endVote }, requirements: [Req.cooldown(10000), Req.modeNot("hexed")], - handler({args:{map}, sender}){ - if(votes.get(sender)) fail(`You have already voted.`); - - votes.set(sender, map); - if(voteEndTime == -1){ - if((Date.now() - lastVoteTime) < 60_000) fail(`Please wait 1 minute before starting a new map vote.`); - startVote(); - Call.sendMessage(`[cyan]Next Map Vote: ${sender.name}[cyan] started a map vote, and voted for [yellow]${map.name()}[cyan]. Use /nextmap ${map.plainName()} to add your vote!`); + handler({ args: { map }, sender }) { + if (!map) { + listMenu("Please Select a Map", "", new GUI_Container(Vars.maps.customMaps().toArray(), 1, (map: MMap) => { return `[accent]${map.name()}` }), sender, ({ data }) => { + playervote(data); + }) } else { - Call.sendMessage(`[cyan]Next Map Vote: ${sender.name}[cyan] voted for [yellow]${map.name()}[cyan]. Time left: [scarlet]${formatTimeRelative(voteEndTime, true)}`); - showVotes(); + playervote(map); + } + + + function playervote(option: MMap) { + if (votes.get(sender)) fail(`You have already voted.`); + votes.set(sender, option); + if (voteEndTime == -1) { + if ((Date.now() - lastVoteTime) < 60_000) fail(`Please wait 1 minute before starting a new map vote.`); + startVote(); + Call.sendMessage(`[cyan]Next Map Vote: ${sender.name}[cyan] started a map vote, and voted for [yellow]${option.name()}[cyan]. Use /nextmap ${option.plainName()} to add your vote!`); + } else { + Call.sendMessage(`[cyan]Next Map Vote: ${sender.name}[cyan] voted for [yellow]${option.name()}[cyan]. Time left: [scarlet]${formatTimeRelative(voteEndTime, true)}`); + showVotes(); + } } } }; @@ -859,7 +867,7 @@ ${highestVotedMaps.map(({key:map, value:votes}) => surrender: command(() => { const prefix = "[orange]Surrender[white]: "; const managers = Team.all.map(team => - new VoteManager(1.5 * 60_000, Gamemode.hexed() ? 1 : 3/4, p => p.team() == team && !p.afk()) + new VoteManager(1.5 * 60_000, Gamemode.hexed() ? 1 : 3 / 4, p => p.team() == team && !p.afk()) .on("success", () => team.cores().copy().each(c => c.kill())) .on("vote passed", () => Call.sendMessage( prefix + `Team ${team.coloredName()} has voted to forfeit this match.` @@ -885,7 +893,7 @@ ${highestVotedMaps.map(({key:map, value:votes}) => perm: Perm.play, requirements: [Req.cooldown(30_000), Req.mode("pvp"), Req.teamAlive], data: { managers }, - handler({ sender }){ + handler({ sender }) { managers[sender.team().id].vote(sender, 1, 0); }, }; @@ -894,7 +902,7 @@ ${highestVotedMaps.map(({key:map, value:votes}) => args: ["target:player"], perm: Perm.none, description: "Views a player's stats.", - handler({args:{target}, output, f}){ + handler({ args: { target }, output, f }) { output(f`[accent]\ Statistics for player ${target}: (note: we started recording statistics on 22 Jan 2024) diff --git a/src/players.ts b/src/players.ts index e1bceaa..c79330c 100644 --- a/src/players.ts +++ b/src/players.ts @@ -8,7 +8,7 @@ import { Perm, PermType } from "./commands"; import * as globals from "./globals"; import { FColor, Gamemode, heuristics, Mode, prefixes, rules, stopAntiEvadeTime, text, tips } from "./config"; import { uuidPattern } from "./globals"; -import { menu } from "./menus"; +import { GUI_Container, menu } from "./menus"; import { Rank, RankName, RoleFlag, RoleFlagName } from "./ranks"; import type { FishCommandArgType, FishPlayerData, PlayerHistoryEntry } from "./types"; import { cleanText, formatTime, formatTimeRelative, isImpersonator, logAction, logHTrip, matchFilter } from "./utils"; @@ -320,7 +320,7 @@ export class FishPlayer { fishPlayer.updateName(); }); //I think this is a better spot for this - if(fishPlayer.firstJoin()) menu("Rules for [#0000ff] >|||> FISH [white] servers [white]", rules.join("\n\n[white]") + "\nYou can view these rules again by running [cyan]/rules[].",["[green]I understand and agree to these terms"],fishPlayer); + if(fishPlayer.firstJoin()) menu("Rules for [#0000ff] >|||> FISH [white] servers [white]", rules.join("\n\n[white]") + "\nYou can view these rules again by running [cyan]/rules[].",[new GUI_Container(["[green]I understand and agree to these terms"])],fishPlayer); } } @@ -611,18 +611,13 @@ Previously used UUID \`${uuid}\`(${Vars.netServer.admins.getInfoOptional(uuid)?. menu( "[gold]Welcome to Fish Community!", `[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ${FColor.discord`Join our Discord`} to request a staff member come online if none are on.`, - ["Close", "Discord"], + [new GUI_Container(["Close", "Discord"], 1, str => ((str == "Discord")?(FColor.discord(str)):(str)))], this, - ({option, sender}) => { - if(option == "Discord"){ + ({data:result, sender}) => { + if(result == "Discord"){ Call.openURI(sender.con, text.discordURL); } }, - false, - str => ({ - "Close": "Close", - "Discord": FColor.discord("Discord") - }[str]) ); this.sendMessage(`[gold]Welcome to Fish Community!\n[gold]Hi there! You have been automatically [scarlet]stopped and muted[] because we've found something to be [pink]a bit sus[]. You can still talk to staff and request to be freed. ${FColor.discord`Join our Discord`} to request a staff member come online if none are on.`); } diff --git a/src/staffCommands.ts b/src/staffCommands.ts index 7f304f3..176c4e4 100644 --- a/src/staffCommands.ts +++ b/src/staffCommands.ts @@ -10,7 +10,7 @@ import { maxTime } from "./globals"; import { updateMaps } from "./files"; import * as fjsContext from "./fjsContext"; import { fishState, ipPattern, uuidPattern } from "./globals"; -import { menu } from './menus'; +import { GUI_Cancel, GUI_Confirm, GUI_Container, listMenu, menu } from './menus'; import { FishPlayer } from "./players"; import { Rank } from "./ranks"; import { addToTileHistory, colorBadBoolean, formatTime, formatTimeRelative, getAntiBotInfo, logAction, match, serverRestartLoop, untilForever, updateBans } from "./utils"; @@ -32,7 +32,7 @@ export const commands = commandList({ if(args.player.hasPerm("blockTrolling")) fail(`Player ${args.player} is insufficiently trollable.`); const message = args.message ?? "You have been warned. I suggest you stop what you're doing"; - menu('Warning', message, ["[green]Accept"], args.player); + menu('Warning', message, [new GUI_Container(["[green]Accept"])], args.player); logAction('warned', sender, args.player, message); outputSuccess(f`Warned player ${args.player} for "${message}"`); } @@ -223,20 +223,20 @@ export const commands = commandList({ } - menu("Stop", "Choose a player to mark", possiblePlayers, sender, ({option: optionPlayer, sender}) => { + listMenu("Stop", "Choose a player to mark", new GUI_Container(possiblePlayers, "auto", (p:PlayerInfo) => {return p.lastName}), sender, ({data: optionPlayer}) => { if(args.time == null){ - menu("Stop", "Select stop time", ["2 days", "7 days", "30 days", "forever"], sender, ({option: optionTime, sender}) => { + menu("Stop", "Select stop time", [new GUI_Container(["2 days", "7 days", "30 days", "forever"])], sender, ({text: optionTime}) => { const time = optionTime == "2 days" ? 172800000 : optionTime == "7 days" ? 604800000 : optionTime == "30 days" ? 2592000000 : (maxTime - Date.now() - 10000); stop(optionPlayer, time); - }, false); + }); } else { stop(optionPlayer, args.time); } - }, true, p => p.lastName); + },); } }, @@ -364,8 +364,8 @@ export const commands = commandList({ let data:PlayerInfo | null; if((data = admins.getInfoOptional(uuid)) != null && data.admin) fail(`Cannot ban an admin.`); const name = data ? `${escapeStringColorsClient(data.lastName)} (${uuid}/${data.lastIP})` : uuid; - menu("Confirm", `Are you sure you want to ban ${name}?`, ["[red]Yes", "[green]Cancel"], sender, ({option:confirm}) => { - if(confirm != "[red]Yes") fail("Cancelled."); + menu("Confirm", `Are you sure you want to ban ${name}?`, [new GUI_Confirm()], sender, ({data:confirm}) => { + if(!confirm) fail("Cancelled."); admins.banPlayerID(uuid); if(data){ const ip = data.lastIP; @@ -382,14 +382,13 @@ export const commands = commandList({ outputSuccess(f`Banned player ${uuid}. [yellow]Unable to determine IP.[]`); } updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); - }, false); + }); return; } else if(args.uuid_or_ip && ipPattern.test(args.uuid_or_ip)){ //Overload 2: ban by uuid const ip = args.uuid_or_ip; - menu("Confirm", `Are you sure you want to ban IP ${ip}?`, ["[red]Yes", "[green]Cancel"], sender, ({option:confirm}) => { - if(confirm != "[red]Yes") fail("Cancelled."); - + menu("Confirm", `Are you sure you want to ban IP ${ip}?`, [new GUI_Confirm()], sender, ({data:confirm}) => { + if(!confirm) fail("Cancelled."); api.ban({ip}); const info = admins.findByIP(ip); if(info) logAction("banned", sender, info); @@ -403,22 +402,36 @@ export const commands = commandList({ } updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); - }, false); + }); return; } //Overload 3: ban by menu - menu(`[scarlet]BAN[]`, "Choose a player to ban.", setToArray(Groups.player), sender, ({option}) => { - if(option.admin) fail(`Cannot ban an admin.`); - menu("Confirm", `Are you sure you want to ban ${option.name}?`, ["[red]Yes", "[green]Cancel"], sender, ({option:confirm}) => { - if(confirm != "[red]Yes") fail("Cancelled."); - admins.banPlayerIP(option.ip()); //this also bans the UUID - api.ban({ip: option.ip(), uuid: option.uuid()}); - Log.info(`${option.ip()}/${option.uuid()} was banned.`); - logAction("banned", sender, option.getInfo()); - outputSuccess(f`Banned player ${option}.`); + /* + menu(`[scarlet]BAN[]`, "Choose a player to ban.", [new GUI_Container(setToArray(Groups.player), "auto", opt => opt.name), new GUI_Cancel()], sender, ({data:target}) => { + if(target.admin) fail(`Cannot ban an admin.`); + menu("Confirm", `Are you sure you want to ban ${target.name}?`, [new GUI_Confirm()], sender, ({data:confirm}) => { + if(!confirm) fail("Cancelled."); + admins.banPlayerIP(target.ip()); //this also bans the UUID + api.ban({ip: target.ip(), uuid: target.uuid()}); + Log.info(`${target.ip()}/${target.uuid()} was banned.`); + logAction("banned", sender, target.getInfo()); + outputSuccess(f`Banned player ${target}.`); + updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); + }); + }); + */ + listMenu(`[scarlet]BAN[]`, "Choose a player to ban.", new GUI_Container(setToArray(Groups.player), "auto", opt => opt.name), sender, ({data:target}) => { + if(target.admin) fail(`Cannot ban an admin.`); + menu("Confirm", `Are you sure you want to ban ${target.name}?`, [new GUI_Confirm()], sender, ({data:confirm}) => { + if(!confirm) fail("Cancelled."); + admins.banPlayerIP(target.ip()); //this also bans the UUID + api.ban({ip: target.ip(), uuid: target.uuid()}); + Log.info(`${target.ip()}/${target.uuid()} was banned.`); + logAction("banned", sender, target.getInfo()); + outputSuccess(f`Banned player ${target}.`); updateBans(player => `[scarlet]Player [yellow]${player.name}[scarlet] has been whacked by ${sender.prefixedName}.`); - }, false); - }, true, opt => opt.name); + }); + }); } }, @@ -447,10 +460,10 @@ export const commands = commandList({ menu( `Confirm`, `This will kill [scarlet]every ${unit ? unit.localizedName : "unit"}[] on the team ${team.coloredName()}.`, - ["[orange]Kill units[]", "[green]Cancel[]"], + [new GUI_Confirm()], sender, - ({option}) => { - if(option == "[orange]Kill units[]"){ + ({data:confirm}) => { + if(confirm){ if(unit){ let i = 0; team.data().units.each(u => u.type == unit, u => { @@ -464,16 +477,16 @@ export const commands = commandList({ outputSuccess(f`Killed ${before} units on ${team}.`); } } else outputFail(`Cancelled.`); - }, false + }, ); } else { menu( `Confirm`, `This will kill [scarlet]every single ${unit ? unit.localizedName : "unit"}[].`, - ["[orange]Kill all units[]", "[green]Cancel[]"], + [new GUI_Confirm], sender, - ({option}) => { - if(option == "[orange]Kill all units[]"){ + ({data:option}) => { + if(option){ if(unit){ let i = 0; Groups.unit.each(u => u.type == unit, u => { @@ -487,7 +500,7 @@ export const commands = commandList({ outputSuccess(f`Killed ${before} units.`); } } else outputFail(`Cancelled.`); - }, false + }, ); } } @@ -501,29 +514,29 @@ export const commands = commandList({ menu( `Confirm`, `This will kill [scarlet]every building[] on the team ${team.coloredName()}, except cores.`, - ["[orange]Kill buildings[]", "[green]Cancel[]"], + [new GUI_Confirm()], sender, - ({option}) => { - if(option == "[orange]Kill buildings[]"){ + ({data:confirm}) => { + if(confirm){ const count = team.data().buildings.size; team.data().buildings.each(b => !(b.block instanceof CoreBlock), b => b.tile.remove()); outputSuccess(f`Killed ${count} buildings on ${team}`); } else outputFail(`Cancelled.`); - }, false + } ); } else { menu( `Confirm`, `This will kill [scarlet]every building[] except cores.`, - ["[orange]Kill buildings[]", "[green]Cancel[]"], + [new GUI_Confirm()], sender, - ({option}) => { - if(option == "[orange]Kill buildings[]"){ + ({data:confirm}) => { + if(confirm){ const count = Groups.build.size(); Groups.build.each(b => !(b.block instanceof CoreBlock), b => b.tile.remove()); outputSuccess(f`Killed ${count} buildings.`); } else outputFail(`Cancelled.`); - }, false + }, ); } } @@ -870,8 +883,10 @@ Last name used: "${info.plainLastName()}" [gray](${escapeStringColorsClient(info IPs used: ${info.ips.map(i => `[blue]${i}[]`).toString(", ")}` )); }; - if(matches.size > 20) menu("Confirm", `Are you sure you want to view all ${matches.size} matches?`, ["Yes"], sender, displayMatches); - else displayMatches(); + if(matches.size > 20) menu("Confirm", `Are you sure you want to view all ${matches.size} matches?`, [new GUI_Confirm()], sender, ({data:confirm}) => { + if(!confirm) fail(`aborted.`); + displayMatches() + }); } } },