From 751a13a2bf7e5f8052c900b935f239abded0d7ee Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:00:17 +0530 Subject: [PATCH 01/33] Update checkmem command show shouldCache instead of hasData --- build/scripts/commands/console.js | 2 +- src/commands/console.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/scripts/commands/console.js b/build/scripts/commands/console.js index babbf695..1d591c90 100644 --- a/build/scripts/commands/console.js +++ b/build/scripts/commands/console.js @@ -741,7 +741,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ description: "Checks memory usage of various objects.", handler: function (_a) { var output = _a.output; - output("Memory usage:\nTotal: ".concat(Math.round(Core.app.getJavaHeap() / (Math.pow(2, 10))), " KB\nNumber of cached fish players: ").concat(Object.keys(players_1.FishPlayer.cachedPlayers).length, " (has data: ").concat(Object.values(players_1.FishPlayer.cachedPlayers).filter(function (p) { return p.hasData(); }).length, ")\nFish player data string length: ").concat(players_1.FishPlayer.getFishPlayersString.length, " (").concat(Core.settings.getInt("fish-subkeys"), " subkeys)\nLength of tilelog entries: ").concat(Math.round(Object.values(globals_1.tileHistory).reduce(function (acc, a) { return acc + a.length; }, 0) / (Math.pow(2, 10))), " KB")); + output("Memory usage:\nTotal: ".concat(Math.round(Core.app.getJavaHeap() / (Math.pow(2, 10))), " KB\nNumber of cached fish players: ").concat(Object.keys(players_1.FishPlayer.cachedPlayers).length, " (stored locally: ").concat(Object.values(players_1.FishPlayer.cachedPlayers).filter(function (p) { return p.shouldCache(); }).length, ")\nFish player data string length: ").concat(players_1.FishPlayer.getFishPlayersString.length, " (").concat(Core.settings.getInt("fish-subkeys"), " subkeys)\nLength of tilelog entries: ").concat(Math.round(Object.values(globals_1.tileHistory).reduce(function (acc, a) { return acc + a.length; }, 0) / (Math.pow(2, 10))), " KB")); } }, stopplayer: { diff --git a/src/commands/console.ts b/src/commands/console.ts index b22b8277..4065d04b 100644 --- a/src/commands/console.ts +++ b/src/commands/console.ts @@ -512,7 +512,7 @@ export const commands = consoleCommandList({ output( `Memory usage: Total: ${Math.round(Core.app.getJavaHeap() / (2 ** 10))} KB -Number of cached fish players: ${Object.keys(FishPlayer.cachedPlayers).length} (has data: ${Object.values(FishPlayer.cachedPlayers).filter(p => p.hasData()).length}) +Number of cached fish players: ${Object.keys(FishPlayer.cachedPlayers).length} (stored locally: ${Object.values(FishPlayer.cachedPlayers).filter(p => p.shouldCache()).length}) Fish player data string length: ${FishPlayer.getFishPlayersString.length} (${Core.settings.getInt("fish-subkeys")} subkeys) Length of tilelog entries: ${Math.round(Object.values(tileHistory).reduce((acc, a) => acc + a.length, 0) / (2 ** 10))} KB` ); From 35d5a8720203a7de79a85ccaf50f5cdaa92bd5c7 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:02:52 +0530 Subject: [PATCH 02/33] Allow using /say when paused --- build/scripts/commands/console.js | 9 +++++++++ src/commands/console.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/build/scripts/commands/console.js b/build/scripts/commands/console.js index 1d591c90..80faf995 100644 --- a/build/scripts/commands/console.js +++ b/build/scripts/commands/console.js @@ -1049,5 +1049,14 @@ exports.commands = (0, commands_1.consoleCommandList)({ }); } }, + say: { + args: ["message:string"], + description: "Sends a message to the in-game chat.", + handler: function (_a) { + var message = _a.args.message; + Call.sendMessage("[scarlet][[Server]:[] ".concat(message)); + Log.info("&fi&lcServer: &fr&lw".concat(message)); + } + } }); var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15; diff --git a/src/commands/console.ts b/src/commands/console.ts index 4065d04b..3983a0f9 100644 --- a/src/commands/console.ts +++ b/src/commands/console.ts @@ -755,4 +755,13 @@ ${FishPlayer.mapPlayers(p => } } }, + + say: { + args: ["message:string"], + description: "Sends a message to the in-game chat.", + handler({args: {message}}){ + Call.sendMessage(`[scarlet][[Server]:[] ${message}`); + Log.info(`&fi&lcServer: &fr&lw${message}`); + } + } }); From 2b8024fd7a583074d2b1a7b46c01f1ce5e25f66f Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:45:53 +0530 Subject: [PATCH 03/33] Fix boolf and misc type errors --- src/commands/console.ts | 2 +- src/mindustryTypes.ts | 2 +- src/types.ts | 1 - src/utils.ts | 10 +++++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/commands/console.ts b/src/commands/console.ts index 3983a0f9..3fa9d923 100644 --- a/src/commands/console.ts +++ b/src/commands/console.ts @@ -201,7 +201,7 @@ export const commands = consoleCommandList({ output(`&lrIP &c"${args.target}"&lr was banned. Ban was synced to other servers.`); } } else if((range = getIPRange(args.target)) != null){ - if(admins.subnetBans.contains(ip => ip.replace(/\.$/, "") == range)){ + if(admins.subnetBans.contains(boolf(ip => ip.replace(/\.$/, "") == range))){ output(`Subnet &c"${range}"&fr is already banned.`); } else { admins.subnetBans.add(range); diff --git a/src/mindustryTypes.ts b/src/mindustryTypes.ts index 3383954c..a7fabd34 100644 --- a/src/mindustryTypes.ts +++ b/src/mindustryTypes.ts @@ -429,7 +429,7 @@ class Seq { static with(items:Iterable):Seq; add(item:T):this; contains(item:T):boolean; - contains(pred:(item:T) => boolean):boolean; + contains(pred:Boolf):boolean; count(pred:(item:T) => boolean):number; /** @deprecated Use select() or retainAll() */ filter(pred:(item:T) => boolean):Seq; diff --git a/src/types.ts b/src/types.ts index 100c39d0..ba4da409 100644 --- a/src/types.ts +++ b/src/types.ts @@ -100,7 +100,6 @@ export type FlaggedIPData = { moderated: boolean; }; -export type Boolf = (input:T) => boolean; export type Expand = T extends Function ? T : { [K in keyof T]: T[K] }; export type TagFunction = (stringChunks: readonly string[], ...varChunks: readonly Tin[]) =>Tout diff --git a/src/utils.ts b/src/utils.ts index 3f55247e..ba6ae8e5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,7 +10,7 @@ import { fail, PartialFormatString } from "/frameworks/commands"; import { crash, escapeStringColorsServer, escapeTextDiscord, parseError, random, StringIO } from "/funcs"; import { fishState, ipPattern, ipPortPattern, ipRangeCIDRPattern, ipRangeWildcardPattern, maxTime, tileHistory, uuidPattern } from "/globals"; import { FishPlayer } from "/players"; -import { Boolf, SelectEnumClassKeys } from "/types"; +import { SelectEnumClassKeys } from "/types"; export function memoizeChatFilter(impl:(arg:string) => string){ @@ -151,8 +151,8 @@ export function getTeam(team:string):Team | string { /** Attempts to parse an Item from the input. */ export function getItem(item:string):Item | string { - let temp: Item | null; - if(item in Items && (temp = Items[item]) instanceof Item) return temp; + let temp; + if(item in Items && (temp = Items[item as keyof typeof Items]) instanceof Item) return temp; else if((temp = Vars.content.items().find(t => t.name.includes(item.toLowerCase())))) return temp; return `"${item}" is not a valid item.`; } @@ -213,8 +213,8 @@ export function isImpersonator(name:string, isAdmin:boolean):false | string { const replacedText = cleanText(name); const antiEvasionText = cleanText(name, true); //very clean code i know - const filters:Array<[check:Boolf, message:string]> = ( - (input: Array, string]>) => + const filters:Array<[check: (value:string) => boolean, message:string]> = ( + (input: Array boolean), string]>) => input.map(i => Array.isArray(i) ? [ typeof i[0] == "string" ? replacedText => replacedText.includes((i[0] as string)) : From ef1bf922c49a2bdd2e11043e74ffa0fce7c9dca7 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:47:37 +0530 Subject: [PATCH 04/33] Achivements framework --- .vscode/new_achievement.code-snippets | 27 ++ build/scripts/achievements.js | 276 ++++++++++++++++++ build/scripts/commands/console.js | 3 +- build/scripts/config.js | 6 +- build/scripts/frameworks/commands/commands.js | 4 +- build/scripts/globals.js | 3 +- build/scripts/players.js | 7 +- build/scripts/utils.js | 1 + src/achievements.ts | 177 +++++++++++ src/commands/console.ts | 3 +- src/config.ts | 4 +- src/frameworks/commands/commands.ts | 7 +- src/globals.ts | 8 +- src/mindustryTypes.ts | 89 +++++- src/players.ts | 9 +- src/types.ts | 2 + src/utils.ts | 3 +- 17 files changed, 610 insertions(+), 19 deletions(-) create mode 100644 .vscode/new_achievement.code-snippets create mode 100644 build/scripts/achievements.js create mode 100644 src/achievements.ts diff --git a/.vscode/new_achievement.code-snippets b/.vscode/new_achievement.code-snippets new file mode 100644 index 00000000..1de08eb3 --- /dev/null +++ b/.vscode/new_achievement.code-snippets @@ -0,0 +1,27 @@ +{ + // Place your fish-commands workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Achievement": { + "scope": "typescript", + "prefix": "achv", + "body": [ + "${1:string_id}: new Achievement(\"${2:icon}\", \"${3:name}\", \"${4:description}\", {$5})," + ], + "isFileTemplate": false, + "description": "achievement for achievements.ts", + } +} \ No newline at end of file diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js new file mode 100644 index 00000000..fe499187 --- /dev/null +++ b/build/scripts/achievements.js @@ -0,0 +1,276 @@ +"use strict"; +var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + 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)); +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Achievement = void 0; +var config_1 = require("/config"); +var players_1 = require("/players"); +//scrap doesn't count +var serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; +var erekirItems = [Items.beryllium, Items.graphite, Items.silicon, Items.tungsten, Items.oxide, Items.surgeAlloy, Items.thorium, Items.carbide, Items.phaseFabric]; +var usefulItems10k = { + serpulo: serpuloItems.map(function (i) { return new ItemStack(i, 10000); }), + erekir: erekirItems.map(function (i) { return new ItemStack(i, 10000); }), + sun: __spreadArray(__spreadArray([], __read(serpuloItems), false), __read(erekirItems), false).map(function (i) { return new ItemStack(i, 10000); }), +}; +var allItems1k = Vars.content.items().select(function (i) { return !i.hidden; }).toArray().map(function (i) { return new ItemStack(i, 1000); }); +var mixtechItems = Items.serpuloItems.copy(); +Items.erekirItems.each(function (i) { return mixtechItems.add(i); }); +var Achievement = /** @class */ (function () { + function Achievement(icon, name, description, options) { + var _a; + if (options === void 0) { options = {}; } + this.name = name; + this.notify = "player"; + this.hidden = false; + this.disabled = false; + if (Array.isArray(icon)) { + this.icon = "[".concat(icon[0], "]") + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); + } + else if (typeof icon == "number") { + this.icon = String.fromCharCode(icon); + } + else { + this.icon = icon; + } + if (Array.isArray(description)) { + _a = __read(description, 2), this.description = _a[0], this.extendedDescription = _a[1]; + } + else + this.description = description; + this.nid = Achievement._id++; + Object.assign(this, options); + if (options.modes) { + var _b = __read(options.modes), type = _b[0], modes_1 = _b.slice(1); + if (type == "only") + this.allowedModes = modes_1; + else + this.allowedModes = config_1.GamemodeNames.filter(function (m) { return !modes_1.includes(m); }); + } + else { + this.allowedModes = config_1.GamemodeNames; + } + Achievement.all.push(this); + if (this.checkPlayerFrequent || this.checkFrequent) + Achievement.checkFrequent.push(this); + if (this.checkPlayerInfrequent || this.checkInfrequent) + Achievement.checkInfrequent.push(this); + if (this.checkPlayerJoin) + Achievement.checkJoin.push(this); + if (this.checkPlayerGameover || this.checkGameover) + Achievement.checkGameover.push(this); + } + Achievement.prototype.message = function () { + return config_1.FColor.achievement(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Achievement granted!\n[accent]", ": [white]", ""], ["Achievement granted!\\n[accent]", ": [white]", ""])), this.name, this.description); + }; + Achievement.prototype.messageToEveryone = function (player) { + return config_1.FColor.achievement(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Player ", " has completed the achievement \"", "\"."], ["Player ", " has completed the achievement \"", "\"."])), player.prefixedName, this.name); + }; + Achievement.prototype.allowedInMode = function () { + return this.allowedModes.includes(config_1.Gamemode.name()); + }; + Achievement.prototype.grantToAllOnline = function (team) { + var _this = this; + players_1.FishPlayer.forEachPlayer(function (p) { + if (!_this.has(p) && (!team || p.team() == team)) { + if (_this.notify != "none") + p.sendMessage(_this.message()); + _this.setObtained(p); + } + }); + }; + Achievement.prototype.grantTo = function (player) { + if (this.notify == "everyone") + Call.sendMessage(this.messageToEveryone(player)); + else if (this.notify == "player") + player.sendMessage(this.message()); + this.setObtained(player); + }; + Achievement.prototype.setObtained = function (player) { + return player.achievements.set(this.nid); + }; + Achievement.prototype.has = function (player) { + return player.achievements.get(this.nid); + }; + Achievement.all = []; + /** Checked every second. */ + Achievement.checkFrequent = []; + /** Checked every 10 seconds. Use for states that can be gained but not lost, such as "x wins". */ + Achievement.checkInfrequent = []; + Achievement.checkJoin = []; + Achievement.checkGameover = []; + Achievement._id = 0; + return Achievement; +}()); +exports.Achievement = Achievement; +Events.on(EventType.PlayerJoin, function (_a) { + var e_1, _b; + var _c; + var player = _a.player; + try { + for (var _d = __values(Achievement.checkJoin), _e = _d.next(); !_e.done; _e = _d.next()) { + var ach = _e.value; + if (ach.allowedInMode()) { + var fishP = players_1.FishPlayer.get(player); + if (!ach.has(fishP) && ((_c = ach.checkPlayerJoin) === null || _c === void 0 ? void 0 : _c.call(ach, fishP))) { + ach.grantTo(fishP); + } + } + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_e && !_e.done && (_b = _d.return)) _b.call(_d); + } + finally { if (e_1) throw e_1.error; } + } +}); +Events.on(EventType.GameOverEvent, function (_a) { + var e_2, _b; + var _c; + var winner = _a.winner; + var _loop_1 = function (ach) { + if (ach.allowedInMode()) { + if ((_c = ach.checkGameover) === null || _c === void 0 ? void 0 : _c.call(ach, winner)) + ach.grantToAllOnline(); + else + players_1.FishPlayer.forEachPlayer(function (fishP) { + var _a; + if (!ach.has(fishP) && ((_a = ach.checkPlayerGameover) === null || _a === void 0 ? void 0 : _a.call(ach, fishP, winner))) { + ach.grantTo(fishP); + } + }); + } + }; + try { + for (var _d = __values(Achievement.checkGameover), _e = _d.next(); !_e.done; _e = _d.next()) { + var ach = _e.value; + _loop_1(ach); + } + } + catch (e_2_1) { e_2 = { error: e_2_1 }; } + finally { + try { + if (_e && !_e.done && (_b = _d.return)) _b.call(_d); + } + finally { if (e_2) throw e_2.error; } + } +}); +Timer.schedule(function () { + var e_3, _a; + var _loop_2 = function (ach) { + if (ach.allowedInMode()) { + if (ach.checkFrequent) { + if (config_1.Gamemode.pvp()) { + Vars.state.teams.active.each(function (t) { + if (ach.checkFrequent(t)) + ach.grantToAllOnline(t); + }); + } + else { + if (ach.checkFrequent(Vars.state.rules.defaultTeam)) + ach.grantToAllOnline(); + } + } + else { + players_1.FishPlayer.forEachPlayer(function (fishP) { + var _a; + if (!ach.has(fishP) && ((_a = ach.checkPlayerFrequent) === null || _a === void 0 ? void 0 : _a.call(ach, fishP))) + ach.grantTo(fishP); + }); + } + } + }; + try { + for (var _b = __values(Achievement.checkFrequent), _c = _b.next(); !_c.done; _c = _b.next()) { + var ach = _c.value; + _loop_2(ach); + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_3) throw e_3.error; } + } +}, 1, 1); +Timer.schedule(function () { + var e_4, _a; + var _loop_3 = function (ach) { + if (ach.allowedInMode()) { + if (ach.checkInfrequent) { + if (config_1.Gamemode.pvp()) { + Vars.state.teams.active.each(function (t) { + if (ach.checkInfrequent(t)) + ach.grantToAllOnline(t); + }); + } + else { + if (ach.checkInfrequent(Vars.state.rules.defaultTeam)) + ach.grantToAllOnline(); + } + } + else { + players_1.FishPlayer.forEachPlayer(function (fishP) { + var _a; + if (!ach.has(fishP) && ((_a = ach.checkPlayerInfrequent) === null || _a === void 0 ? void 0 : _a.call(ach, fishP))) + ach.grantTo(fishP); + }); + } + } + }; + try { + for (var _b = __values(Achievement.checkInfrequent), _c = _b.next(); !_c.done; _c = _b.next()) { + var ach = _c.value; + _loop_3(ach); + } + } + catch (e_4_1) { e_4 = { error: e_4_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_4) throw e_4.error; } + } +}, 10, 10); +var templateObject_1, templateObject_2; diff --git a/build/scripts/commands/console.js b/build/scripts/commands/console.js index 80faf995..20f245d0 100644 --- a/build/scripts/commands/console.js +++ b/build/scripts/commands/console.js @@ -391,7 +391,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ } } else if ((range = (0, utils_1.getIPRange)(args.target)) != null) { - if (admins.subnetBans.contains(function (ip) { return ip.replace(/\.$/, "") == range; })) { + if (admins.subnetBans.contains(boolf(function (ip) { return ip.replace(/\.$/, "") == range; }))) { output("Subnet &c\"".concat(range, "\"&fr is already banned.")); } else { @@ -1056,6 +1056,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ var message = _a.args.message; Call.sendMessage("[scarlet][[Server]:[] ".concat(message)); Log.info("&fi&lcServer: &fr&lw".concat(message)); + globals_1.FishEvents.fire("serverSays", []); } } }); diff --git a/build/scripts/config.js b/build/scripts/config.js index 2e331d6b..5f65ab75 100644 --- a/build/scripts/config.js +++ b/build/scripts/config.js @@ -33,7 +33,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.rules = exports.tips = exports.FColor = exports.text = exports.prefixes = exports.Gamemode = exports.FishServer = exports.mapRepoURLs = exports.Mode = exports.backendIP = exports.stopAntiEvadeTime = exports.heuristics = exports.adminNames = exports.multiCharSubstitutions = exports.substitutions = exports.bannedWords = void 0; +exports.rules = exports.tips = exports.FColor = exports.text = exports.prefixes = exports.GamemodeNames = exports.Gamemode = exports.FishServer = exports.mapRepoURLs = exports.Mode = exports.backendIP = exports.stopAntiEvadeTime = exports.heuristics = exports.adminNames = exports.multiCharSubstitutions = exports.substitutions = exports.bannedWords = void 0; var globals_1 = require("/globals"); var ranks_1 = require("/ranks"); var funcs_1 = require("/funcs"); @@ -219,6 +219,7 @@ exports.Gamemode = { minigame: function () { return exports.Gamemode.name() == "minigame"; }, name: function () { return Core.settings.get("mode", Vars.state.rules.mode().name()); }, }; +exports.GamemodeNames = Object.keys(exports.Gamemode).filter(function (x) { return x !== "name"; }); //#endregion //#region text content exports.prefixes = { @@ -267,7 +268,7 @@ exports.FColor = (function (data) { varChunks[_i - 1] = arguments[_i]; } return str != null ? - "".concat(c).concat(Array.isArray(str) ? String.raw.apply(String, __spreadArray([{ raw: str }], __read(varChunks), false)) : str, "[]") + "".concat(c).concat(Array.isArray(str) ? String.raw.apply(String, __spreadArray([{ raw: str }], __read(varChunks.map(function (v) { return String(v) + c; })), false)) : str, "[]") : c; } ]; @@ -277,6 +278,7 @@ exports.FColor = (function (data) { /** Used for tips and welcome messages. */ tip: "[gold]", member: "[pink]", + achievement: "[lime]", }); /** Tips that are shown to players randomly. */ exports.tips = { diff --git a/build/scripts/frameworks/commands/commands.js b/build/scripts/frameworks/commands/commands.js index fe133097..977caf3e 100644 --- a/build/scripts/frameworks/commands/commands.js +++ b/build/scripts/frameworks/commands/commands.js @@ -525,8 +525,10 @@ function register(commands, clientHandler, serverHandler) { //Verify authorization //as a bonus, this crashes if data.perm is undefined if (!data.perm.check(fishSender)) { - if (data.customUnauthorizedMessage) + if (data.customUnauthorizedMessage) { (0, utils_1.outputFail)(data.customUnauthorizedMessage, sender); + globals_1.FishEvents.fire("commandUnauthorized", [fishSender, name]); + } else if (data.isHidden) (0, utils_1.outputMessage)(hiddenUnauthorizedMessage, sender); else diff --git a/build/scripts/globals.js b/build/scripts/globals.js index 86463f04..41058d4c 100644 --- a/build/scripts/globals.js +++ b/build/scripts/globals.js @@ -4,7 +4,7 @@ Copyright © BalaM314, 2026. All Rights Reserved. This file contains mutable global variables, and global constants. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.FishEvents = exports.maxTime = exports.ipRangeWildcardPattern = exports.ipRangeCIDRPattern = exports.ipPortPattern = exports.ipPattern = exports.uuidPattern = exports.ipJoins = exports.fishPlugin = exports.fishState = exports.recentWhispers = exports.tileHistory = void 0; +exports.FishEvents = exports.unitsT5 = exports.maxTime = exports.ipRangeWildcardPattern = exports.ipRangeCIDRPattern = exports.ipPortPattern = exports.ipPattern = exports.uuidPattern = exports.ipJoins = exports.fishPlugin = exports.fishState = exports.recentWhispers = exports.tileHistory = void 0; var funcs_1 = require("/funcs"); exports.tileHistory = {}; exports.recentWhispers = {}; @@ -29,4 +29,5 @@ exports.ipPortPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$/; exports.ipRangeCIDRPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/(1[2-9]|2[0-4])$/; //Disallow anything bigger than a /12 exports.ipRangeWildcardPattern = /^(\d{1,3}\.\d{1,3})\.(?:(\d{1,3}\.\*)|\*)$/; //Disallow anything bigger than a /16 exports.maxTime = 9999999999999; +exports.unitsT5 = [UnitTypes.reign, UnitTypes.toxopid, UnitTypes.corvus, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.omura, UnitTypes.navanax, UnitTypes.conquer, UnitTypes.collaris, UnitTypes.disrupt]; exports.FishEvents = new funcs_1.EventEmitter(); diff --git a/build/scripts/players.js b/build/scripts/players.js index 5cd24b0e..64f89e94 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -177,6 +177,7 @@ var FishPlayer = /** @class */ (function () { }; /** Used for the /vanish command. */ this.showRankPrefix = true; + this.achievements = new Bits(); this.uuid = uuid; this.player = player; this.updateData(data); @@ -453,6 +454,8 @@ var FishPlayer = /** @class */ (function () { this.rank = (_a = ranks_1.Rank.getByName(data.rank)) !== null && _a !== void 0 ? _a : ranks_1.Rank.player; if (data.flags != undefined) this.flags = new Set(data.flags.map(ranks_1.RoleFlag.getByName).filter(Boolean)); + if (data.achievements != undefined) + this.achievements = JsonIO.read(Bits, data.achievements); }; FishPlayer.prototype.getData = function () { var _a = this, uuid = _a.uuid, name = _a.name, muted = _a.muted, unmarkTime = _a.unmarkTime, rank = _a.rank, flags = _a.flags, highlight = _a.highlight, rainbow = _a.rainbow, history = _a.history, usid = _a.usid, chatStrictness = _a.chatStrictness, lastJoined = _a.lastJoined, firstJoined = _a.firstJoined, stats = _a.stats, showRankPrefix = _a.showRankPrefix; @@ -471,7 +474,8 @@ var FishPlayer = /** @class */ (function () { stats: stats, showRankPrefix: showRankPrefix, rank: rank.name, - flags: __spreadArray([], __read(flags.values()), false).map(function (f) { return f.name; }) + flags: __spreadArray([], __read(flags.values()), false).map(function (f) { return f.name; }), + achievements: JsonIO.write(this.achievements) }; }; /** Warning: the "update" callback is run twice. */ @@ -656,6 +660,7 @@ var FishPlayer = /** @class */ (function () { } else if ((0, utils_1.cleanText)(player.name, true).includes("hacker")) { fishPlayer.sendMessage("[scarlet]\u26A0 Don't be a script kiddie!"); + globals_1.FishEvents.fire("scriptKiddie", [fishPlayer]); } } fishPlayer.updateAdminStatus(); diff --git a/build/scripts/utils.js b/build/scripts/utils.js index df185b72..39715f51 100644 --- a/build/scripts/utils.js +++ b/build/scripts/utils.js @@ -581,6 +581,7 @@ function definitelyRealMemoryCorruption() { var hexString = Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, "0"); Call.sendMessage("[scarlet]Error: internal server error."); Call.sendMessage("[scarlet]Error: memory corruption: mindustry.world.modules.ItemModule@".concat(hexString)); + globals_1.FishEvents.fire("memoryCorruption", []); } function getEnemyTeam() { if (config_1.Gamemode.pvp()) diff --git a/src/achievements.ts b/src/achievements.ts new file mode 100644 index 00000000..74dd71f2 --- /dev/null +++ b/src/achievements.ts @@ -0,0 +1,177 @@ +import { FColor, Gamemode, GamemodeName, GamemodeNames } from "/config"; +import { Duration } from "/funcs"; +import { FishEvents, unitsT5 } from "/globals"; +import { FishPlayer } from "/players"; +import { Rank } from "/ranks"; + +//scrap doesn't count +const serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; +const erekirItems = [Items.beryllium, Items.graphite, Items.silicon, Items.tungsten, Items.oxide, Items.surgeAlloy, Items.thorium, Items.carbide, Items.phaseFabric]; +const usefulItems10k = { + serpulo: serpuloItems.map(i => new ItemStack(i, 10_000)), + erekir: erekirItems.map(i => new ItemStack(i, 10_000)), + sun: [...serpuloItems, ...erekirItems].map(i => new ItemStack(i, 10_000)), +}; +const allItems1k = Vars.content.items().select(i => !i.hidden).toArray().map(i => new ItemStack(i, 1000)); +const mixtechItems = Items.serpuloItems.copy(); +Items.erekirItems.each(i => mixtechItems.add(i)); + +export class Achievement { + private nid: number; + sid!: string; + + icon: string; + description: string; + extendedDescription?: string; + + checkPlayerInfrequent?: (player:FishPlayer) => boolean; + checkPlayerFrequent?: (player:FishPlayer) => boolean; + checkPlayerJoin?: (player:FishPlayer) => boolean; + checkPlayerGameover?: (player:FishPlayer, winTeam:Team) => boolean; + checkInfrequent?: (team: Team) => boolean; + checkFrequent?: (team: Team) => boolean; + checkGameover?: (winTeam:Team) => boolean; + + notify: "none" | "player" | "everyone" = "player"; + hidden = false; + disabled = false; + allowedModes: GamemodeName[]; + + static all: Achievement[] = []; + /** Checked every second. */ + static checkFrequent: Achievement[] = []; + /** Checked every 10 seconds. Use for states that can be gained but not lost, such as "x wins". */ + static checkInfrequent: Achievement[] = []; + static checkJoin: Achievement[] = []; + static checkGameover: Achievement[] = []; + + private static _id = 0; + constructor( + icon: string | number | [string, number | string], + public name: string, + description: string | [string, string], + options: Partial & { + modes: ["only" | "not", ...GamemodeName[]]; + }> = {}, + ){ + if(Array.isArray(icon)){ + this.icon = `[${icon[0]}]` + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); + } else if(typeof icon == "number"){ + this.icon = String.fromCharCode(icon); + } else { + this.icon = icon; + } + if(Array.isArray(description)){ + [this.description, this.extendedDescription] = description; + } else this.description = description; + this.nid = Achievement._id ++; + Object.assign(this, options); + if(options.modes){ + const [type, ...modes] = options.modes; + if(type == "only") this.allowedModes = modes; + else this.allowedModes = GamemodeNames.filter(m => !modes.includes(m)); + } else { + this.allowedModes = GamemodeNames; + } + Achievement.all.push(this); + if(this.checkPlayerFrequent || this.checkFrequent) Achievement.checkFrequent.push(this); + if(this.checkPlayerInfrequent || this.checkInfrequent) Achievement.checkInfrequent.push(this); + if(this.checkPlayerJoin) Achievement.checkJoin.push(this); + if(this.checkPlayerGameover || this.checkGameover) Achievement.checkGameover.push(this); + } + + message():string { + return FColor.achievement`Achievement granted!\n[accent]${this.name}: [white]${this.description}`; + } + messageToEveryone(player:FishPlayer):string { + return FColor.achievement`Player ${player.prefixedName} has completed the achievement "${this.name}".`; + } + allowedInMode(){ + return this.allowedModes.includes(Gamemode.name()); + } + + public grantToAllOnline(team?: Team){ + FishPlayer.forEachPlayer(p => { + if(!this.has(p) && (!team || p.team() == team)){ + if(this.notify != "none") p.sendMessage(this.message()); + this.setObtained(p); + } + }); + } + public grantTo(player:FishPlayer){ + if(this.notify == "everyone") Call.sendMessage(this.messageToEveryone(player)); + else if(this.notify == "player") player.sendMessage(this.message()); + this.setObtained(player); + } + + private setObtained(player:FishPlayer){ + return player.achievements.set(this.nid); + } + public has(player:FishPlayer){ + return player.achievements.get(this.nid); + } +} + +Events.on(EventType.PlayerJoin, ({player}: {player: mindustryPlayer}) => { + for(const ach of Achievement.checkJoin){ + if(ach.allowedInMode()){ + const fishP = FishPlayer.get(player); + if(!ach.has(fishP) && ach.checkPlayerJoin?.(fishP)){ + ach.grantTo(fishP); + } + } + } +}); +Events.on(EventType.GameOverEvent, ({winner}) => { + for(const ach of Achievement.checkGameover){ + if(ach.allowedInMode()){ + if(ach.checkGameover?.(winner)) ach.grantToAllOnline(); + else FishPlayer.forEachPlayer(fishP => { + if(!ach.has(fishP) && ach.checkPlayerGameover?.(fishP, winner)){ + ach.grantTo(fishP); + } + }); + } + } +}); +Timer.schedule(() => { + for(const ach of Achievement.checkFrequent){ + if(ach.allowedInMode()){ + if(ach.checkFrequent){ + if(Gamemode.pvp()){ + Vars.state.teams.active.each(t => { + if(ach.checkFrequent!(t)) ach.grantToAllOnline(t); + }); + } else { + if(ach.checkFrequent(Vars.state.rules.defaultTeam)) ach.grantToAllOnline(); + } + } else { + FishPlayer.forEachPlayer(fishP => { + if(!ach.has(fishP) && ach.checkPlayerFrequent?.(fishP)) ach.grantTo(fishP); + }); + } + } + } +}, 1, 1); +Timer.schedule(() => { + for(const ach of Achievement.checkInfrequent){ + if(ach.allowedInMode()){ + if(ach.checkInfrequent){ + if(Gamemode.pvp()){ + Vars.state.teams.active.each(t => { + if(ach.checkInfrequent!(t)) ach.grantToAllOnline(t); + }); + } else { + if(ach.checkInfrequent(Vars.state.rules.defaultTeam)) ach.grantToAllOnline(); + } + } else { + FishPlayer.forEachPlayer(fishP => { + if(!ach.has(fishP) && ach.checkPlayerInfrequent?.(fishP)) ach.grantTo(fishP); + }); + } + } + } +}, 10, 10); diff --git a/src/commands/console.ts b/src/commands/console.ts index 3fa9d923..d132aba3 100644 --- a/src/commands/console.ts +++ b/src/commands/console.ts @@ -9,7 +9,7 @@ import { updateMaps } from "/files"; import * as fjsContext from "/fjsContext"; import { consoleCommandList, fail } from "/frameworks/commands"; import { Duration, escapeStringColorsServer, to2DArray } from "/funcs"; -import { fishState, ipPattern, ipPortPattern, maxTime, tileHistory, uuidPattern } from "/globals"; +import { FishEvents, fishState, ipPattern, ipPortPattern, maxTime, tileHistory, uuidPattern } from "/globals"; import { FishPlayer } from "/players"; import { Rank } from "/ranks"; import { colorNumber, fishCommandsRootDirPath, formatTime, formatTimeRelative, formatTimestamp, getAntiBotInfo, getIPRange, logAction, serverRestartLoop, updateBans } from "/utils"; @@ -762,6 +762,7 @@ ${FishPlayer.mapPlayers(p => handler({args: {message}}){ Call.sendMessage(`[scarlet][[Server]:[] ${message}`); Log.info(`&fi&lcServer: &fr&lw${message}`); + FishEvents.fire("serverSays", []); } } }); diff --git a/src/config.ts b/src/config.ts index 4d404cc9..3a05a30d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -228,6 +228,7 @@ export const Gamemode = { minigame: () => Gamemode.name() == "minigame", name: () => Core.settings.get("mode", Vars.state.rules.mode().name()) as "attack" | "survival" | "pvp" | "sandbox" | "hexed" | "hardcore" | "testsrv" | "minigame", }; +export const GamemodeNames: GamemodeName[] = Object.keys(Gamemode).filter((x): x is GamemodeName => x !== "name"); //#endregion //#region text content @@ -283,7 +284,7 @@ export const FColor = ( Object.fromEntries(Object.entries(data).map(([k, c]) => [k, (str?:string | readonly string[], ...varChunks: ReadonlyArray) => str != null ? - `${c}${Array.isArray(str) ? String.raw({ raw: str }, ...varChunks) : (str as string)}[]` + `${c}${Array.isArray(str) ? String.raw({ raw: str }, ...varChunks.map(v => String(v) + c)) : (str as string)}[]` : c ] )) @@ -292,6 +293,7 @@ export const FColor = ( /** Used for tips and welcome messages. */ tip: "[gold]", member: "[pink]", + achievement: "[lime]", }); /** Tips that are shown to players randomly. */ export const tips = { diff --git a/src/frameworks/commands/commands.ts b/src/frameworks/commands/commands.ts index d7538e63..44ca8348 100644 --- a/src/frameworks/commands/commands.ts +++ b/src/frameworks/commands/commands.ts @@ -12,7 +12,7 @@ import type { FishCommandArgType, FishCommandData, FishCommandHandlerData, FishC import { CommandArgType, commandArgTypes } from "/frameworks/commands/types"; import { Menu } from "/frameworks/menus"; import { crash, escapeStringColorsClient, parseError, setToArray } from "/funcs"; -import { uuidPattern } from "/globals"; +import { FishEvents, uuidPattern } from "/globals"; import { FishPlayer } from "/players"; import { Rank, RoleFlag } from "/ranks"; import type { ClientCommandHandler, CommandArg, ServerCommandHandler } from "/types"; @@ -345,9 +345,10 @@ export function register(commands: Record | //Verify authorization //as a bonus, this crashes if data.perm is undefined if(!data.perm.check(fishSender)){ - if(data.customUnauthorizedMessage) + if(data.customUnauthorizedMessage){ outputFail(data.customUnauthorizedMessage, sender); - else if(data.isHidden) + FishEvents.fire("commandUnauthorized", [fishSender, name]); + } else if(data.isHidden) outputMessage(hiddenUnauthorizedMessage, sender); else outputFail(data.perm.unauthorizedMessage, sender); diff --git a/src/globals.ts b/src/globals.ts index f10e45d9..81aab91e 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -4,7 +4,7 @@ This file contains mutable global variables, and global constants. */ import { EventEmitter } from "/funcs"; -import type { FishPlayer } from "/players"; +import { FishPlayer } from "/players"; export const tileHistory:Record = {}; export const recentWhispers:Record = {}; @@ -30,6 +30,7 @@ export const ipPortPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$/; export const ipRangeCIDRPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/(1[2-9]|2[0-4])$/; //Disallow anything bigger than a /12 export const ipRangeWildcardPattern = /^(\d{1,3}\.\d{1,3})\.(?:(\d{1,3}\.\*)|\*)$/; //Disallow anything bigger than a /16 export const maxTime = 9999999999999; +export const unitsT5 = [UnitTypes.reign, UnitTypes.toxopid, UnitTypes.corvus, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.omura, UnitTypes.navanax, UnitTypes.conquer, UnitTypes.collaris, UnitTypes.disrupt]; export const FishEvents = new EventEmitter<{ /** Fired after a team change. The current team is player.team() */ @@ -40,4 +41,9 @@ export const FishEvents = new EventEmitter<{ saveData: []; /** Use this event to mutate things after all the data is loaded */ dataLoaded: []; + commandUnauthorized: [player: FishPlayer, name: string]; + scriptKiddie: [player: FishPlayer]; + memoryCorruption: []; + /** Called when the "say" console command is run. */ + serverSays: []; }>(); diff --git a/src/mindustryTypes.ts b/src/mindustryTypes.ts index a7fabd34..2921c074 100644 --- a/src/mindustryTypes.ts +++ b/src/mindustryTypes.ts @@ -70,6 +70,7 @@ const Vars: { maps: Maps; state: { rules: Rules; + planet: Planet; set(state:State):void; gameOver:boolean; wave:number; @@ -79,7 +80,9 @@ const Vars: { enemies:number; /** Time in ticks, 60/s */ tick:number; - } + teams: Teams; + }; + indexer: BlockIndexer; saveExtension: string; saveDirectory: Fi; modDirectory: Fi; @@ -88,7 +91,19 @@ const Vars: { tilesize: 8; world: World; }; +class Teams { + active: Seq; +} +class BlockIndexer { + getFlagged(team: Team, flag: BlockFlag): Seq; +} +class BlockFlag { + static storage: BlockFlag; +} type State = {__state: ""}; +class Planet { + name: string; +} type Scripts = any; type CommandHandler = any; const CommandHandler: CommandHandler; @@ -179,20 +194,54 @@ class Building { block: Block; tile: Tile; items: ItemModule; + power: PowerModule; + liquids: LiquidModule; team: Team; changeTeam: Team; + enabled: boolean; + ammo?: Seq<{item: Item}>; + warmup?: number; + storageCapacity?: number; + timeScale(): number; kill():void; tileX():number; tileY():number; } -const Items: Record; +const Items: Record<"scrap" | "copper" | "lead" | "graphite" | "coal" | "titanium" | "thorium" | "silicon" | "plastanium" | "phaseFabric" | "surgeAlloy" | "sporePod" | "sand" | "blastCompound" | "pyratite" | "metaglass" | "beryllium" | "tungsten" | "oxide" | "carbide" | "fissileMatter" | "dormantCyst", Item> & { + serpuloItems: Seq; + erekirItems: Seq; +}; class Item { name: string; + hidden: boolean; + emoji(): string; +} +const Liquids: Record; +class Liquid { + name: string; + gas: boolean; +} +class LiquidModule { + current(): Liquid; } class ItemModule { get(item: Item):number; set(item: Item, value: number):void; add(item: Item, value: number):void; + has(item: Item, amount: number): boolean; + has(stacks: ItemStack[]): boolean; +} +class PowerModule { + graph: PowerGraph; +} +class PowerGraph { + lastPowerProduced: number; + lastPowerNeeded: number; + producers: Seq; + consumers: Seq; +} +class ItemStack { + constructor(item: Item, amount: number); } class Team { static derelict:Team; @@ -207,7 +256,7 @@ class Team { active():boolean; data():TeamData; core():Building | null; - items():ItemModule | null; + items():ItemModule; coloredName():string; id:number; static get(index:number):Team; @@ -232,6 +281,7 @@ const Groups: { unit: EntityGroup; fire: EntityGroup; build: EntityGroup; + powerGraph: EntityGroup<{graph: PowerGraph}>; }; type Fire = any; class Vec2 { @@ -428,9 +478,11 @@ class Seq { static with(...items:T[]):Seq; static with(items:Iterable):Seq; add(item:T):this; + addUnique(item:T):this; contains(item:T):boolean; contains(pred:Boolf):boolean; count(pred:(item:T) => boolean):number; + allMatch(pred:(item:T) => boolean):boolean; /** @deprecated Use select() or retainAll() */ filter(pred:(item:T) => boolean):Seq; retainAll(pred:(item:T) => boolean):Seq; @@ -451,6 +503,7 @@ class Seq { random():T | null; get(index:number):T; first():T; + peek():T; firstOpt():T | null; clear():void; } @@ -532,6 +585,8 @@ type Unit = { spawnedByCore: boolean; added: boolean; id: number; + tileOn():Tile | null; + tile?: () => Building; kill():void; add():void; isAdded():boolean; @@ -571,7 +626,9 @@ class Fi { name():string; readBytes():number[]; } - +class Bullet { + owner: Unit | Building; +} class Pattern { static matches(regex:string, target:string):boolean; static compile(regex:string):Pattern; @@ -638,6 +695,7 @@ class UnitType { spawn(team:Team, x:number, y:number):Unit; create(team:Team):Unit; supportsEnv(env:number):boolean; + emoji():string; health: number; hidden: boolean; internal: boolean; @@ -808,4 +866,27 @@ class WorldReloader { end():void; } +class Bits { + constructor(capacity?: number); + get(index:number):boolean; + /** + * @param value Default true + */ + set(index:number, value?:boolean):void; + set(index:number, value:number):void; +} + +type JavaClass = any; + +const JsonIO: { + write(object:{}): string; + read(clazz: JavaClass, data: string): T; +}; + +class Boolf { + constructor(_: {get: (value: T) => boolean}); +} +function boolf(func: (value: T) => boolean): Boolf; + +const Iconc: Record<"rotate" | "modeSurvival" | "power" | "left" | "redditAlien" | "edit" | "downOpen" | "pencil" | "file" | "lockOpen" | "right" | "infoCircle" | "pick" | "settings" | "spray1" | "terrain" | "exit" | "wrench" | "lock" | "discord" | "eye" | "none" | "play" | "diagonal" | "eraser" | "trash" | "liquid" | "fileImage" | "defense" | "layers" | "grid" | "admin" | "steam" | "star" | "chartBar" | "chat" | "android" | "image" | "map" | "logic" | "menu" | "commandRally" | "editor" | "folder" | "units" | "commandAttack" | "copy" | "filter" | "cancel" | "terminal" | "upload" | "eyeOff" | "save" | "planeOutline" | "fill" | "distribution" | "upOpen" | "rightOpen" | "modePvp" | "download" | "list" | "flipX" | "flipY" | "effect" | "paste" | "planet" | "waves" | "up" | "warning" | "tree" | "add" | "down" | "host" | "spray" | "info" | "players" | "resize" | "refresh1" | "production" | "crafting" | "pause" | "googleplay" | "hammer" | "fileText" | "modeAttack" | "move" | "zoom" | "bookOpen" | "refresh" | "ok" | "home" | "githubSquare" | "powerOld" | "github" | "undo" | "box" | "trello" | "book" | "export" | "fileTextFill" | "rightOpenOut" | "turret" | "leftOpen" | "line" | "itchio" | "link" | "filters" | "redo", number>; } \ No newline at end of file diff --git a/src/players.ts b/src/players.ts index a0a59384..01e6dbaf 100644 --- a/src/players.ts +++ b/src/players.ts @@ -3,13 +3,14 @@ Copyright © BalaM314, 2026. All Rights Reserved. This file contains the FishPlayer class, and many player-related functions. */ +import type { Achievement } from "/achievements"; import * as api from "/api"; import { FColor, Gamemode, heuristics, Mode, prefixes, rules, stopAntiEvadeTime, text, tips } from "/config"; import { FishCommandArgType, Perm, PermType } from "/frameworks/commands"; import { Menu } from "/frameworks/menus"; import { crash, Duration, escapeStringColorsClient, parseError, search, setToArray, StringIO } from "/funcs"; import * as globals from "/globals"; -import { uuidPattern } from "/globals"; +import { uuidPattern, FishEvents } from "/globals"; import { Rank, RankName, RoleFlag, RoleFlagName } from "/ranks"; import type { FishPlayerData, PlayerHistoryEntry } from "/types"; import { cleanText, formatTime, formatTimeRelative, isImpersonator, logAction, logHTrip, matchFilter } from "/utils"; @@ -159,6 +160,7 @@ export class FishPlayer { }; /** Used for the /vanish command. */ showRankPrefix:boolean = true; + achievements: Bits = new Bits(); //#endregion constructor(uuid:string, data:Partial, player:mindustryPlayer | null){ @@ -378,13 +380,15 @@ export class FishPlayer { if(data.showRankPrefix != undefined) this.showRankPrefix = data.showRankPrefix; if(data.rank != undefined) this.rank = Rank.getByName(data.rank) ?? Rank.player; if(data.flags != undefined) this.flags = new Set(data.flags.map(RoleFlag.getByName).filter(Boolean)); + if(data.achievements != undefined) this.achievements = JsonIO.read(Bits, data.achievements); } getData():FishPlayerData { const { uuid, name, muted, unmarkTime, rank, flags, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix } = this; return { uuid, name, muted, unmarkTime, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix, rank: rank.name, - flags: [...flags.values()].map(f => f.name) + flags: [...flags.values()].map(f => f.name), + achievements: JsonIO.write(this.achievements) }; } /** Warning: the "update" callback is run twice. */ @@ -526,6 +530,7 @@ export class FishPlayer { fishPlayer.sendMessage(`[scarlet]\u26A0[] [gold]Oh no! Our systems think you are a [scarlet]SUSSY IMPERSONATOR[]!\n[gold]Reason: ${message}\n[gold]Change your name to remove the tag.`); } else if(cleanText(player.name, true).includes("hacker")){ fishPlayer.sendMessage("[scarlet]\u26A0 Don't be a script kiddie!"); + FishEvents.fire("scriptKiddie", [fishPlayer]); } } fishPlayer.updateAdminStatus(); diff --git a/src/types.ts b/src/types.ts index ba4da409..bb681e33 100644 --- a/src/types.ts +++ b/src/types.ts @@ -59,6 +59,8 @@ export type FishPlayerData = { gamesWon: number; }; showRankPrefix: boolean; + /** This field contains long values, store it as a string */ + achievements: string; } export type PlayerHistoryEntry = { diff --git a/src/utils.ts b/src/utils.ts index ba6ae8e5..aa6e199b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,7 +8,7 @@ import * as api from "/api"; import { adminNames, bannedWords, Gamemode, GamemodeName, multiCharSubstitutions, substitutions, text } from "/config"; import { fail, PartialFormatString } from "/frameworks/commands"; import { crash, escapeStringColorsServer, escapeTextDiscord, parseError, random, StringIO } from "/funcs"; -import { fishState, ipPattern, ipPortPattern, ipRangeCIDRPattern, ipRangeWildcardPattern, maxTime, tileHistory, uuidPattern } from "/globals"; +import { FishEvents, fishState, ipPattern, ipPortPattern, ipRangeCIDRPattern, ipRangeWildcardPattern, maxTime, tileHistory, uuidPattern } from "/globals"; import { FishPlayer } from "/players"; import { SelectEnumClassKeys } from "/types"; @@ -438,6 +438,7 @@ export function definitelyRealMemoryCorruption(){ const hexString = Math.floor(Math.random() * 0xFFFFFFFF).toString(16).padStart(8, "0"); Call.sendMessage("[scarlet]Error: internal server error."); Call.sendMessage(`[scarlet]Error: memory corruption: mindustry.world.modules.ItemModule@${hexString}`); + FishEvents.fire("memoryCorruption", []); } export function getEnemyTeam():Team { From 1edaa5268d99b1276560debadcdcda2fe5139231 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:47:57 +0530 Subject: [PATCH 05/33] 57 achievements implemented --- build/scripts/achievements.js | 231 +++++++++++++++++++++++++++++++++- src/achievements.ts | 221 ++++++++++++++++++++++++++++++++ 2 files changed, 451 insertions(+), 1 deletion(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index fe499187..2665d219 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -40,9 +40,12 @@ var __values = (this && this.__values) || function(o) { throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Achievement = void 0; +exports.Achievements = exports.Achievement = void 0; var config_1 = require("/config"); +var funcs_1 = require("/funcs"); +var globals_1 = require("/globals"); var players_1 = require("/players"); +var ranks_1 = require("/ranks"); //scrap doesn't count var serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; var erekirItems = [Items.beryllium, Items.graphite, Items.silicon, Items.tungsten, Items.oxide, Items.surgeAlloy, Items.thorium, Items.carbide, Items.phaseFabric]; @@ -273,4 +276,230 @@ Timer.schedule(function () { finally { if (e_4) throw e_4.error; } } }, 10, 10); +exports.Achievements = { + // =========================== + // ╦ ╦ ╔═╗ ╦═╗ ╔╗╔ ╦ ╔╗╔ ╔═╗ ┬ + // ║║║ ╠═╣ ╠╦╝ ║║║ ║ ║║║ ║ ╦ │ + // ╚╩╝ ╩ ╩ ╩╚═ ╝╚╝ ╩ ╝╚╝ ╚═╝ o + // =========================== + // Do not change the order of any achievements. + // Do not remove any achievements: instead, set the "disabled" option to true. + // Reordering achievements will cause ID shifts. + //Joining based + welcome: new Achievement("_", "Welcome", "Join the server.", { checkPlayerJoin: function () { return true; }, notify: "none" }), + migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { hidden: true }), //TODO + frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", "Join the server 100 times.", { checkPlayerJoin: function (p) { return p.info().timesJoined >= 100; } }), + //Gamemode based + attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { modes: ["only", "attack"] }), + survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { modes: ["only", "survival"] }), + pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { modes: ["only", "pvp"] }), + sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { modes: ["only", "sandbox"], checkPlayerInfrequent: function (p) { return p.stats.timeInGame > funcs_1.Duration.hours(1); } }), + hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { modes: ["only", "hexed"] }), + minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { modes: ["only", "minigame"] }), + //playtime based + playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { checkPlayerInfrequent: function (p) { return p.stats.timeInGame >= funcs_1.Duration.hours(1); } }), + playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { checkPlayerInfrequent: function (p) { return p.stats.timeInGame >= funcs_1.Duration.hours(12); } }), + playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { checkPlayerInfrequent: function (p) { return p.stats.timeInGame >= funcs_1.Duration.days(2); } }), + playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { checkPlayerInfrequent: function (p) { return p.stats.timeInGame >= funcs_1.Duration.days(10); } }), + //victories based + victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { checkPlayerGameover: function (p) { return p.stats.gamesWon >= 1; } }), + victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesWon >= 5; } }), + victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesWon >= 30; } }), + victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesWon >= 100; }, notify: "everyone" }), + //games based + games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesFinished >= 10; } }), + games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesFinished >= 40; } }), + games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesFinished >= 100; } }), + games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesFinished >= 200; }, notify: "everyone" }), + //messages based + messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { notify: "none", checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 1; } }), + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 100; } }), + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 500; } }), + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 2000; } }), + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 5000; }, notify: "everyone" }), + //blocks built based + builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: function (p) { return p.stats.blocksPlaced >= 1; }, notify: "none" }), + builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: function (p) { return p.stats.blocksPlaced > 200; } }), + builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { checkPlayerInfrequent: function (p) { return p.stats.blocksPlaced > 1000; } }), + builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: function (p) { return p.stats.blocksPlaced > 5000; }, notify: "everyone" }), + //units + t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { modes: ["not", "sandbox"], checkPlayerFrequent: function (player) { + var _a; + return globals_1.unitsT5.includes((_a = player.unit()) === null || _a === void 0 ? void 0 : _a.type); + }, }), + dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { modes: ["not", "sandbox"], hidden: false }), //TODO + worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { checkPlayerFrequent: function (player) { + var _a; + return ((_a = player.unit()) === null || _a === void 0 ? void 0 : _a.type) == UnitTypes.latum; + }, }), + //pvp + above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { modes: ["only", "pvp"], checkPlayerInfrequent: function (p) { return p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20; } }), + head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { modes: ["only", "pvp"], hidden: true }), //TODO + one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { modes: ["only", "pvp"], hidden: true }), //TODO + //sandbox + underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { modes: ["only", "sandbox"], checkFrequent: function () { + var found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(function (_a) { + var graph = _a.graph; + //we don't need to actually check for power sources, just assume that ~1mil power is a source + if (graph.lastPowerNeeded > graph.lastPowerProduced && graph.lastPowerNeeded < 1e10 && graph.lastPowerProduced >= 999900) + found = true; + }); + return found; + } }), + //easter eggs + memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { notify: "none" }), + run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { notify: "everyone" }), + script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { notify: "none" }), + hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { hidden: true }), + //items based + items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { + modes: ["not", "sandbox"], + checkPlayerFrequent: function (player) { + var _a; + return ((_a = player.team().items()) === null || _a === void 0 ? void 0 : _a.has(usefulItems10k[Vars.state.planet.name])) || false; + }, + }), + fullVault: new Achievement(["green", Blocks.vault.emoji()], "Well Stocked", ["Fill a vault with every obtainable item.", "Requires mixtech."], { + modes: ["not", "sandbox"], + checkInfrequent: function (team) { + return Vars.indexer.getFlagged(team, BlockFlag.storage).contains(boolf(function (b) { return b.block == Blocks.vault && b.items.has(allItems1k); })); + }, + }), + full_core: new Achievement(["green", Blocks.coreAcropolis.emoji()], "Multiblock Incinerator", "Completely fill the core with all obtainable items on a map with core incineration enabled.", { + modes: ["not", "sandbox"], + checkFrequent: function (team) { + var _a; + var items; + switch (Vars.state.planet.name) { + case "serpulo": + items = Items.serpuloItems; + break; + case "erekir": + items = Items.erekirItems; + break; + case "sun": + items = mixtechItems; + break; + } + var capacity = (_a = team.core()) === null || _a === void 0 ? void 0 : _a.storageCapacity; + if (!capacity) + return false; + var module = team.items(); + return items.allMatch(function (i) { return module.has(i, capacity); }); + }, + }), + siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { modes: ["not", "sandbox"] }), + silicon_100k: new Achievement(["green", Items.silicon.emoji()], "Silicon for days", "Obtain 100k silicon.", { + modes: ["not", "sandbox"], + checkFrequent: function (team) { return team.items().has(Items.silicon, 100000); } + }), + //other players based + alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes"), + join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { + checkPlayerJoin: function () { return Groups.player.size() > 20; }, + }), + meet_staff: new Achievement(["lime", Iconc.hammer], "Griefer Beware", "Meet a staff member in-game", { + checkPlayerJoin: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).ranksAtLeast("mod"); }); }, + }), + meet_fish: new Achievement(["blue", Iconc.admin], "The Big Fish", "Meet >|||>Fish himself in-game", { + checkPlayerJoin: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).ranksAtLeast("fish"); }); }, + hidden: true, + }), + server_speak: new Achievement(["pink", Iconc.host], "It Speaks!", "Hear the server talk in chat."), + see_marked_griefer: new Achievement(["red", Iconc.hammer], "Flying Tonk", "See a marked griefer in-game.", { + checkInfrequent: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).marked(); }); }, + }), + //maps based + beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { notify: "everyone", modes: ["not", "pvp"], checkGameover: function (team) { return team == Vars.state.rules.defaultTeam && !Vars.state.map.custom; } }), + //misc + power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { modes: ["not", "sandbox"], checkFrequent: function () { + var found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(function (_a) { + var graph = _a.graph; + //we don't need to actually check for power sources, just assume that ~1mil power is a source + if (graph.lastPowerProduced > 1e6 && !graph.producers.contains(boolf(function (b) { return b.block == Blocks.powerSource; }))) + found = true; + }); + return found; + }, }), + pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { modes: ["not", "sandbox"], hidden: true }), //TODO + core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { modes: ["not", "sandbox"], hidden: true }), //TODO + enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { modes: ["not", "sandbox"], hidden: true }), //TODO + verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "none" }), + afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without doing anything.", { modes: ["not", "sandbox"], hidden: true }), //TODO + status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { checkPlayerFrequent: function (p) { + var unit = p.unit(); + if (!unit) + return false; + var statuses = Reflect.get(unit, "statuses"); + return statuses.size >= 5; + }, modes: ["not", "sandbox"] }), + drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { notify: "everyone", modes: ["not", "sandbox"] }), + drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", "Drown a Mace in ".concat(Blocks.cryofluid.emoji(), " Cryofluid."), { notify: "everyone", modes: ["not", "sandbox"] }), + max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { checkPlayerFrequent: function (player) { + var _a, _b; + var tile = (_b = (_a = player.unit()) === null || _a === void 0 ? void 0 : _a.tile) === null || _b === void 0 ? void 0 : _b.call(_a); + if (!tile) + return false; + return tile.block == Blocks.duo && tile.ammo.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; + }, notify: "everyone", modes: ["not", "sandbox"] }), + foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { notify: "everyone", modes: ["not", "sandbox"] }), + impacts_15: new Achievement(["green", Blocks.impactReactor.emoji()], "Darthscion's Nightmare", "Run 15 impact reactors at full efficiency.", { + modes: ["not", "sandbox"], + notify: "everyone", + checkInfrequent: function (team) { + var found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(function (_a) { + var graph = _a.graph; + //assume that any network running 15 impacts has at least 2 other power sources + if (graph.producers.size >= 17 && graph.producers.count(function (b) { return b.block == Blocks.impactReactor && b.warmup > 0.99999; }) > 15 && graph.producers.first().team == team) + found = true; + }); + return found; + }, + }), +}; +Object.entries(exports.Achievements).forEach(function (_a) { + var _b = __read(_a, 2), id = _b[0], a = _b[1]; + return a.sid = id; +}); +globals_1.FishEvents.on("commandUnauthorized", function (_, player, name) { + if (name == "js" || name == "fjs") + exports.Achievements.run_js_without_perms.grantTo(player); +}); +Events.on(EventType.UnitDrownEvent, function (_a) { + var _b; + var unit = _a.unit; + if (unit.type == UnitTypes.mace && ((_b = unit.tileOn()) === null || _b === void 0 ? void 0 : _b.floor()) == Blocks.cryofluid) + exports.Achievements.drown_mace_in_cryo.grantToAllOnline(); + else if (unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) + exports.Achievements.drown_big_tank.grantToAllOnline(); +}); +Events.on(EventType.UnitBulletDestroyEvent, function (_a) { + var unit = _a.unit, bullet = _a.bullet; + if (unit.type == UnitTypes.dagger && bullet.owner.block == Blocks.foreshadow) { + var build = bullet.owner; + if (build.liquids.current() == Liquids.cryofluid && build.timeScale() >= 3) + exports.Achievements.foreshadow_overkill.grantToAllOnline(build.team); + } +}); +var siliconReached = Team.all.map(function (_) { return false; }); +Events.on(EventType.GameOverEvent, function () { return siliconReached = Team.all.map(function (_) { return false; }); }); +Timer.schedule(function () { + if (!Vars.state.gameOver) { + Vars.state.teams.active.each(function (t) { + if (t.items().has(Items.silicon, 2000)) + siliconReached[t.id] = true; + else if (t.items().get(Items.silicon) == 0) + exports.Achievements.siligone.grantToAllOnline(t); + }); + } +}, 2, 2); +globals_1.FishEvents.on("scriptKiddie", function (_, p) { return exports.Achievements.script_kiddie.grantTo(p); }); +globals_1.FishEvents.on("memoryCorruption", function () { return exports.Achievements.memory_corruption.grantToAllOnline(); }); +globals_1.FishEvents.on("serverSays", function () { return exports.Achievements.server_speak.grantToAllOnline(); }); var templateObject_1, templateObject_2; diff --git a/src/achievements.ts b/src/achievements.ts index 74dd71f2..38841441 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -175,3 +175,224 @@ Timer.schedule(() => { } } }, 10, 10); + +export const Achievements = { + // =========================== + // ╦ ╦ ╔═╗ ╦═╗ ╔╗╔ ╦ ╔╗╔ ╔═╗ ┬ + // ║║║ ╠═╣ ╠╦╝ ║║║ ║ ║║║ ║ ╦ │ + // ╚╩╝ ╩ ╩ ╩╚═ ╝╚╝ ╩ ╝╚╝ ╚═╝ o + // =========================== + // Do not change the order of any achievements. + // Do not remove any achievements: instead, set the "disabled" option to true. + // Reordering achievements will cause ID shifts. + + + //Joining based + welcome: new Achievement("_", "Welcome", "Join the server.", { checkPlayerJoin: () => true, notify: "none" }), + migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { hidden: true }), //TODO + frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", "Join the server 100 times.", { checkPlayerJoin: p => p.info().timesJoined >= 100 }), + + //Gamemode based + attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { modes: ["only", "attack"] }), + survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { modes: ["only", "survival"] }), + pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { modes: ["only", "pvp"] }), + sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { modes: ["only", "sandbox"], checkPlayerInfrequent: p => p.stats.timeInGame > Duration.hours(1) }), + hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { modes: ["only", "hexed"] }), + minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { modes: ["only", "minigame"] }), + + //playtime based + playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { checkPlayerInfrequent: p => p.stats.timeInGame >= Duration.hours(1) }), + playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { checkPlayerInfrequent: p => p.stats.timeInGame >= Duration.hours(12) }), + playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { checkPlayerInfrequent: p => p.stats.timeInGame >= Duration.days(2) }), + playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { checkPlayerInfrequent: p => p.stats.timeInGame >= Duration.days(10) }), + + //victories based + victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { checkPlayerGameover: p => p.stats.gamesWon >= 1 }), + victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { checkPlayerGameover: p => p.stats.gamesWon >= 5 }), + victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { checkPlayerGameover: p => p.stats.gamesWon >= 30 }), + victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { checkPlayerGameover: p => p.stats.gamesWon >= 100, notify: "everyone" }), + + //games based + games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { checkPlayerGameover: p => p.stats.gamesFinished >= 10 }), + games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { checkPlayerGameover: p => p.stats.gamesFinished >= 40 }), + games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { checkPlayerGameover: p => p.stats.gamesFinished >= 100 }), + games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { checkPlayerGameover: p => p.stats.gamesFinished >= 200, notify: "everyone" }), + + //messages based + messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { notify: "none", checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 1 }), + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 100 }), + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 500 }), + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 2000 }), + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 5000, notify: "everyone" }), + + //blocks built based + builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: p => p.stats.blocksPlaced >= 1, notify: "none" }), + builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: p => p.stats.blocksPlaced > 200 }), + builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { checkPlayerInfrequent: p => p.stats.blocksPlaced > 1000 }), + builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: p => p.stats.blocksPlaced > 5000, notify: "everyone" }), + + //units + t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { modes: ["not", "sandbox"], checkPlayerFrequent(player) { + return (unitsT5 as Array).includes(player.unit()?.type); + }, }), + dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { modes: ["not", "sandbox"], hidden: false }), //TODO + worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { checkPlayerFrequent(player) { + return player.unit()?.type == UnitTypes.latum; + }, }), + + //pvp + above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { modes: ["only", "pvp"], checkPlayerInfrequent: p => p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20 }), + head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { modes: ["only", "pvp"], hidden: true }), //TODO + one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { modes: ["only", "pvp"], hidden: true }), //TODO + + //sandbox + underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { modes: ["only", "sandbox"], checkFrequent(){ + let found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(({graph}) => { + //we don't need to actually check for power sources, just assume that ~1mil power is a source + if(graph.lastPowerNeeded > graph.lastPowerProduced && graph.lastPowerNeeded < 1e10 && graph.lastPowerProduced >= 999_900) + found = true; + }); + return found; + } }), + + //easter eggs + memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { notify: "none" }), + run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { notify: "everyone" }), + script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { notify: "none" }), + hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { hidden: true }), + + //items based + items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { + modes: ["not", "sandbox"], + checkPlayerFrequent(player) { + return player.team().items()?.has(usefulItems10k[Vars.state.planet.name as "serpulo" | "erekir" | "sun"]) || false; + }, + }), + fullVault: new Achievement(["green", Blocks.vault.emoji()], "Well Stocked", ["Fill a vault with every obtainable item.", "Requires mixtech."], { + modes: ["not", "sandbox"], + checkInfrequent(team) { + return Vars.indexer.getFlagged(team, BlockFlag.storage).contains(boolf(b => b.block == Blocks.vault && b.items.has(allItems1k))); + }, + }), + full_core: new Achievement(["green", Blocks.coreAcropolis.emoji()], "Multiblock Incinerator", "Completely fill the core with all obtainable items on a map with core incineration enabled.", { + modes: ["not", "sandbox"], + checkFrequent(team) { + let items; + switch(Vars.state.planet.name as "serpulo" | "erekir" | "sun"){ + case "serpulo": items = Items.serpuloItems; break; + case "erekir": items = Items.erekirItems; break; + case "sun": items = mixtechItems; break; + } + const capacity = team.core()?.storageCapacity; + if(!capacity) return false; + const module = team.items(); + return items.allMatch(i => module.has(i, capacity)); + }, + }), + siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { modes: ["not", "sandbox"] }), + silicon_100k: new Achievement(["green", Items.silicon.emoji()], "Silicon for days", "Obtain 100k silicon.", { + modes: ["not", "sandbox"], + checkFrequent: team => team.items().has(Items.silicon, 100_000) + }), + + //other players based + alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes"), + join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { + checkPlayerJoin: () => Groups.player.size() > 20, + }), + meet_staff: new Achievement(["lime", Iconc.hammer], "Griefer Beware", "Meet a staff member in-game", { + checkPlayerJoin: () => Groups.player.contains(p => FishPlayer.get(p).ranksAtLeast("mod")), + }), + meet_fish: new Achievement(["blue", Iconc.admin], "The Big Fish", "Meet >|||>Fish himself in-game", { + checkPlayerJoin: () => Groups.player.contains(p => FishPlayer.get(p).ranksAtLeast("fish")), + hidden: true, + }), + server_speak: new Achievement(["pink", Iconc.host], "It Speaks!", "Hear the server talk in chat."), + see_marked_griefer: new Achievement(["red", Iconc.hammer], "Flying Tonk", "See a marked griefer in-game.", { + checkInfrequent: () => Groups.player.contains(p => FishPlayer.get(p).marked()), + }), + + //maps based + beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { notify: "everyone", modes: ["not", "pvp"], checkGameover: (team) => team == Vars.state.rules.defaultTeam && !Vars.state.map.custom }), + + //misc + power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { modes: ["not", "sandbox"], checkFrequent(){ + let found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(({graph}) => { + //we don't need to actually check for power sources, just assume that ~1mil power is a source + if(graph.lastPowerProduced > 1e6 && !graph.producers.contains(boolf(b => b.block == Blocks.powerSource))) + found = true; + }); + return found; + }, }), + pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { modes: ["not", "sandbox"], hidden: true }), //TODO + core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { modes: ["not", "sandbox"], hidden: true }), //TODO + enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { modes: ["not", "sandbox"], hidden: true }), //TODO + verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "none" }), + afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without doing anything.", { modes: ["not", "sandbox"], hidden: true }), //TODO + status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { checkPlayerFrequent: p => { + const unit = p.unit(); + if(!unit) return false; + const statuses = Reflect.get(unit, "statuses") as Seq<{ effect: StatusEffect }>; + return statuses.size >= 5; + }, modes: ["not", "sandbox"] }), + drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { notify: "everyone", modes: ["not", "sandbox"] }), + drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", `Drown a Mace in ${Blocks.cryofluid.emoji()} Cryofluid.`, { notify: "everyone", modes: ["not", "sandbox"] }), + max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { checkPlayerFrequent(player) { + const tile = player.unit()?.tile?.(); + if(!tile) return false; + return tile.block == Blocks.duo && tile.ammo!.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; + }, notify: "everyone", modes: ["not", "sandbox"] }), + foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { notify: "everyone", modes: ["not", "sandbox"] }), + impacts_15: new Achievement(["green", Blocks.impactReactor.emoji()], "Darthscion's Nightmare", "Run 15 impact reactors at full efficiency.", { + modes: ["not", "sandbox"], + notify: "everyone", + checkInfrequent(team){ + let found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(({graph}) => { + //assume that any network running 15 impacts has at least 2 other power sources + if(graph.producers.size >= 17 && graph.producers.count(b => b.block == Blocks.impactReactor && b.warmup! > 0.99999) > 15 && graph.producers.first().team == team) + found = true; + }); + return found; + }, + }), + +} satisfies Record; +Object.entries(Achievements).forEach(([id, a]) => a.sid = id); + +FishEvents.on("commandUnauthorized", (_, player, name) => { + if(name == "js" || name == "fjs") Achievements.run_js_without_perms.grantTo(player); +}); + + +Events.on(EventType.UnitDrownEvent, ({unit}:{unit: Unit}) => { + if(unit.type == UnitTypes.mace && unit.tileOn()?.floor() == Blocks.cryofluid) Achievements.drown_mace_in_cryo.grantToAllOnline(); + else if(unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) Achievements.drown_big_tank.grantToAllOnline(); +}); + +Events.on(EventType.UnitBulletDestroyEvent, ({unit, bullet}:{unit:Unit; bullet: Bullet}) => { + if(unit.type == UnitTypes.dagger && (bullet.owner as Building).block == Blocks.foreshadow){ + const build = bullet.owner as Building; + if(build.liquids.current() == Liquids.cryofluid && build.timeScale() >= 3) Achievements.foreshadow_overkill.grantToAllOnline(build.team); + } +}); + +let siliconReached = Team.all.map(_ => false); +Events.on(EventType.GameOverEvent, () => siliconReached = Team.all.map(_ => false)); +Timer.schedule(() => { + if(!Vars.state.gameOver){ + Vars.state.teams.active.each(t => { + if(t.items().has(Items.silicon, 2000)) siliconReached[t.id] = true; + else if(t.items().get(Items.silicon) == 0) Achievements.siligone.grantToAllOnline(t); + }); + } +}, 2, 2); + +FishEvents.on("scriptKiddie", (_, p) => Achievements.script_kiddie.grantTo(p)); +FishEvents.on("memoryCorruption", () => Achievements.memory_corruption.grantToAllOnline()); +FishEvents.on("serverSays", () => Achievements.server_speak.grantToAllOnline()); \ No newline at end of file From 21dd637a5f4e95e6d65ab62ab6c3cea0d5d07811 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:47:09 +0530 Subject: [PATCH 06/33] Logic for "Alone" achievement --- build/scripts/achievements.js | 11 ++++++++++- src/achievements.ts | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index 2665d219..fe589b09 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -396,7 +396,7 @@ exports.Achievements = { checkFrequent: function (team) { return team.items().has(Items.silicon, 100000); } }), //other players based - alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes"), + alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { notify: "none" }), join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { checkPlayerJoin: function () { return Groups.player.size() > 20; }, }), @@ -489,6 +489,7 @@ Events.on(EventType.UnitBulletDestroyEvent, function (_a) { }); var siliconReached = Team.all.map(function (_) { return false; }); Events.on(EventType.GameOverEvent, function () { return siliconReached = Team.all.map(function (_) { return false; }); }); +var isAlone = 0; Timer.schedule(function () { if (!Vars.state.gameOver) { Vars.state.teams.active.each(function (t) { @@ -498,6 +499,14 @@ Timer.schedule(function () { exports.Achievements.siligone.grantToAllOnline(t); }); } + if (Groups.player.size() == 1) { + if (isAlone == 0) + isAlone = Date.now(); + else if (Date.now() > isAlone + funcs_1.Duration.minutes(2)) + exports.Achievements.alone.grantToAllOnline(); + } + else + isAlone = 0; }, 2, 2); globals_1.FishEvents.on("scriptKiddie", function (_, p) { return exports.Achievements.script_kiddie.grantTo(p); }); globals_1.FishEvents.on("memoryCorruption", function () { return exports.Achievements.memory_corruption.grantToAllOnline(); }); diff --git a/src/achievements.ts b/src/achievements.ts index 38841441..1d4b09aa 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -298,7 +298,7 @@ export const Achievements = { }), //other players based - alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes"), + alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { notify: "none" }), join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { checkPlayerJoin: () => Groups.player.size() > 20, }), @@ -384,6 +384,7 @@ Events.on(EventType.UnitBulletDestroyEvent, ({unit, bullet}:{unit:Unit; bullet: let siliconReached = Team.all.map(_ => false); Events.on(EventType.GameOverEvent, () => siliconReached = Team.all.map(_ => false)); +let isAlone = 0; Timer.schedule(() => { if(!Vars.state.gameOver){ Vars.state.teams.active.each(t => { @@ -391,8 +392,13 @@ Timer.schedule(() => { else if(t.items().get(Items.silicon) == 0) Achievements.siligone.grantToAllOnline(t); }); } + if(Groups.player.size() == 1){ + if(isAlone == 0) isAlone = Date.now(); + else if(Date.now() > isAlone + Duration.minutes(2)) Achievements.alone.grantToAllOnline(); + } else isAlone = 0; }, 2, 2); + FishEvents.on("scriptKiddie", (_, p) => Achievements.script_kiddie.grantTo(p)); FishEvents.on("memoryCorruption", () => Achievements.memory_corruption.grantToAllOnline()); FishEvents.on("serverSays", () => Achievements.server_speak.grantToAllOnline()); \ No newline at end of file From 4ac64845cbd6b05e535bca972ae15c066582392c Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:47:18 +0530 Subject: [PATCH 07/33] fix tests --- spec/src/env.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/spec/src/env.ts b/spec/src/env.ts index 69761cc8..4c0cbfec 100644 --- a/spec/src/env.ts +++ b/spec/src/env.ts @@ -843,6 +843,38 @@ const Strings = { }, }; +class UnitType {} + +const UnitTypes = Object.fromEntries( + ["mace", "dagger", "crawler", "fortress", "scepter", "reign", "vela", "nova", "pulsar", "quasar", "corvus", "atrax", "merui", "cleroi", "anthicus", "tecta", "collaris", "spiroct", "arkyid", "toxopid", "elude", "flare", "eclipse", "horizon", "zenith", "antumbra", "avert", "obviate", "mono", "poly", "mega", "evoke", "incite", "emanate", "quell", "disrupt", "quad", "oct", "alpha", "beta", "gamma", "risso", "minke", "bryde", "sei", "omura", "retusa", "oxynoe", "cyerce", "aegires", "navanax", "block", "manifold", "assemblyDrone", "stell", "locus", "precept", "vanquish", "conquer", "missile", "latum", "renale"] + .map(n => [n, new UnitType()]) +); + +class Bits { + bits = new BigUint64Array(1); + constructor(capacity?: number){ /*empty*/ } + get(index:number):boolean { + const word = index >>> 6; + return word < this.bits.length && Boolean(this.bits[word] & BigInt(1 << index)); + } + set(index:number, value:boolean = true){ + const word = index >>> 6; + if(value){ + this.checkCapacity(word); + this.bits[word] |= BigInt(1 << (index & 0x3F)); + } else { + if(word >= this.bits.length) return; + this.bits[word] &= BigInt(~(1 << (index & 0x3F))); + } + } + checkCapacity(len:number){ + if(len >= this.bits.length){ + const newBits = new BigUint64Array(len + 1); + newBits.set(this.bits); + this.bits = newBits; + } + } +} const Packages = { java: { @@ -856,4 +888,4 @@ const Packages = { gen: { Map: MMap } } }; -Object.assign(globalThis, {Pattern, ObjectIntMap, Seq, Fi, Packages, Events, Trigger, Team, EventType, Timer, EffectCallPacket2, LabelReliableCallPacket, Vars, ServerControl, Core, Log, Menus, Time, CommandHandler, Gamemode, Fx, Effect, Vec2, Tmp, Paths, Path, Threads, CommandRunner, Strings}); +Object.assign(globalThis, {Pattern, ObjectIntMap, Seq, Fi, Packages, Events, Trigger, Team, EventType, Timer, EffectCallPacket2, LabelReliableCallPacket, Vars, ServerControl, Core, Log, Menus, Time, CommandHandler, Gamemode, Fx, Effect, Vec2, Tmp, Paths, Path, Threads, CommandRunner, Strings, UnitTypes, Bits}); From a2b9ffe8d89a36a426a4d6d5c953b1b741835725 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:31:57 +0530 Subject: [PATCH 08/33] Store and update aggregate stats / join time --- build/scripts/players.js | 23 +++++++++++++++++++---- build/scripts/utils.js | 4 ++-- src/api.ts | 4 ++-- src/players.ts | 35 ++++++++++++++++++++--------------- src/types.ts | 22 ++++++++++++++-------- src/utils.ts | 4 ++-- 6 files changed, 59 insertions(+), 33 deletions(-) diff --git a/build/scripts/players.js b/build/scripts/players.js index 64f89e94..b6f3413c 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -167,6 +167,10 @@ var FishPlayer = /** @class */ (function () { this.lastJoined = -1; /** -1 represents unknown */ this.firstJoined = -1; + /** -1 represents unknown */ + this.globalLastJoined = -1; + /** -1 represents unknown */ + this.globalFirstJoined = -1; this.stats = { blocksBroken: 0, blocksPlaced: 0, @@ -175,6 +179,7 @@ var FishPlayer = /** @class */ (function () { gamesFinished: 0, gamesWon: 0, }; + this.globalStats = this.stats; /** Used for the /vanish command. */ this.showRankPrefix = true; this.achievements = new Bits(); @@ -436,6 +441,10 @@ var FishPlayer = /** @class */ (function () { this.lastJoined = data.lastJoined; if (data.firstJoined != undefined) this.firstJoined = data.firstJoined; + if (data.globalLastJoined != undefined) + this.globalLastJoined = data.globalLastJoined; + if (data.globalFirstJoined != undefined) + this.globalFirstJoined = data.globalFirstJoined; if (data.highlight != undefined) this.highlight = data.highlight; if (data.history != undefined) @@ -448,6 +457,8 @@ var FishPlayer = /** @class */ (function () { this.chatStrictness = data.chatStrictness; if (data.stats != undefined) this.stats = data.stats; + if (data.globalStats != undefined) + this.globalStats = data.globalStats; if (data.showRankPrefix != undefined) this.showRankPrefix = data.showRankPrefix; if (data.rank != undefined) @@ -730,7 +741,7 @@ var FishPlayer = /** @class */ (function () { //Clear temporary states such as menu and taphandler fishP.activeMenus = []; fishP.tapInfo.commandName = null; - fishP.stats.timeInGame += (Date.now() - fishP.lastJoined); //Time between joining and leaving + fishP.updateStats(function (stats) { return stats.timeInGame += (Date.now() - fishP.lastJoined); }); //Time between joining and leaving fishP.lastJoined = Date.now(); this.recentLeaves.unshift(fishP); if (this.recentLeaves.length > 10) @@ -808,7 +819,7 @@ var FishPlayer = /** @class */ (function () { } } fishP.lastActive = Date.now(); - fishP.stats.chatMessagesSent++; + fishP.updateStats(function (stats) { return stats.chatMessagesSent++; }); }; FishPlayer.onPlayerCommand = function (player, command, unjoinedRawArgs) { if (command == "msg" && unjoinedRawArgs[1] == "Please do not use that logic, as it is attem83 logic and is bad to use. For more information please read www.mindustry.dev/attem") @@ -823,13 +834,13 @@ var FishPlayer = /** @class */ (function () { fishPlayer.tapInfo.commandName = null; //Update stats if (!_this.ignoreGameOver && fishPlayer.team() != Team.derelict && winningTeam != Team.derelict) { - fishPlayer.stats.gamesFinished++; + fishPlayer.updateStats(function (stats) { return stats.gamesFinished++; }); if (fishPlayer.changedTeam) { fishPlayer.sendMessage("Refusing to update stats due to a team change."); } else { if (fishPlayer.team() == winningTeam) - fishPlayer.stats.gamesWon++; + fishPlayer.updateStats(function (stats) { return stats.gamesWon++; }); } } fishPlayer.changedTeam = false; @@ -1573,6 +1584,10 @@ var FishPlayer = /** @class */ (function () { FishPlayer.prototype.joinsLessThan = function (amount) { return this.info().timesJoined < amount; }; + FishPlayer.prototype.updateStats = function (func) { + func(this.stats); + func(this.globalStats); + }; /** * Returns a score between 0 and 1, as an estimate of the player's skill level. * Defaults to 0.2 (guessing that the best trusted players can beat 5 noobs) diff --git a/build/scripts/utils.js b/build/scripts/utils.js index 39715f51..344f6946 100644 --- a/build/scripts/utils.js +++ b/build/scripts/utils.js @@ -774,7 +774,7 @@ exports.addToTileHistory = logErrors("Error while saving a tilelog entry", funct var fishP = players_1.FishPlayer.get(e.unit.player); //TODO move this code fishP.tstats.blocksBroken++; - fishP.stats.blocksBroken++; + fishP.updateStats(function (stats) { return stats.blocksBroken++; }); } } else { @@ -783,7 +783,7 @@ exports.addToTileHistory = logErrors("Error while saving a tilelog entry", funct if ((_k = (_j = e.unit) === null || _j === void 0 ? void 0 : _j.player) === null || _k === void 0 ? void 0 : _k.uuid()) { var fishP = players_1.FishPlayer.get(e.unit.player); //TODO move this code - fishP.stats.blocksPlaced++; + fishP.updateStats(function (stats) { return stats.blocksPlaced++; }); } } } diff --git a/src/api.ts b/src/api.ts index 814c6d68..4632eae2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,7 +6,7 @@ This file contains wrappers over the API calls to the backend server. import { backendIP, Gamemode, Mode } from "/config"; import { FishPlayer } from "/players"; import { Promise } from "/promise"; -import type { FishPlayerData } from "/types"; +import type { FishPlayerData, UploadedFishPlayerData } from "/types"; const cachedIps:Record = {}; @@ -169,7 +169,7 @@ export function getFishPlayerData(uuid:string){ } /** Pushes fish player data to the backend. */ -export function setFishPlayerData(data: FishPlayerData, repeats = 1, ignoreActivelySyncedFields = false) { +export function setFishPlayerData(data: UploadedFishPlayerData, repeats = 1, ignoreActivelySyncedFields = false) { const { promise, resolve, reject } = Promise.withResolvers(); if(Mode.noBackend){ resolve(); diff --git a/src/players.ts b/src/players.ts index 01e6dbaf..8586ff27 100644 --- a/src/players.ts +++ b/src/players.ts @@ -12,7 +12,7 @@ import { crash, Duration, escapeStringColorsClient, parseError, search, setToArr import * as globals from "/globals"; import { uuidPattern, FishEvents } from "/globals"; import { Rank, RankName, RoleFlag, RoleFlagName } from "/ranks"; -import type { FishPlayerData, PlayerHistoryEntry } from "/types"; +import type { FishPlayerData, PlayerHistoryEntry, Stats, UploadedFishPlayerData } from "/types"; import { cleanText, formatTime, formatTimeRelative, isImpersonator, logAction, logHTrip, matchFilter } from "/utils"; @@ -142,15 +142,11 @@ export class FishPlayer { lastJoined:number = -1; /** -1 represents unknown */ firstJoined:number = -1; - stats: { - blocksBroken: number; - blocksPlaced: number; - timeInGame: number; - chatMessagesSent: number; - /** Does not include RTVs */ - gamesFinished: number; - gamesWon: number; - } = { + /** -1 represents unknown */ + globalLastJoined:number = -1; + /** -1 represents unknown */ + globalFirstJoined:number = -1; + stats: Stats = { blocksBroken: 0, blocksPlaced: 0, timeInGame: 0, @@ -158,6 +154,7 @@ export class FishPlayer { gamesFinished: 0, gamesWon: 0, }; + globalStats: Stats = this.stats; /** Used for the /vanish command. */ showRankPrefix:boolean = true; achievements: Bits = new Bits(); @@ -371,18 +368,21 @@ export class FishPlayer { if(data.unmarkTime != undefined) this.unmarkTime = data.unmarkTime; if(data.lastJoined != undefined) this.lastJoined = data.lastJoined; if(data.firstJoined != undefined) this.firstJoined = data.firstJoined; + if(data.globalLastJoined != undefined) this.globalLastJoined = data.globalLastJoined; + if(data.globalFirstJoined != undefined) this.globalFirstJoined = data.globalFirstJoined; if(data.highlight != undefined) this.highlight = data.highlight; if(data.history != undefined) this.history = data.history; if(data.rainbow != undefined) this.rainbow = data.rainbow; if(data.usid != undefined) this.usid = data.usid; if(data.chatStrictness != undefined) this.chatStrictness = data.chatStrictness; if(data.stats != undefined) this.stats = data.stats; + if(data.globalStats != undefined) this.globalStats = data.globalStats; if(data.showRankPrefix != undefined) this.showRankPrefix = data.showRankPrefix; if(data.rank != undefined) this.rank = Rank.getByName(data.rank) ?? Rank.player; if(data.flags != undefined) this.flags = new Set(data.flags.map(RoleFlag.getByName).filter(Boolean)); if(data.achievements != undefined) this.achievements = JsonIO.read(Bits, data.achievements); } - getData():FishPlayerData { + getData():UploadedFishPlayerData { const { uuid, name, muted, unmarkTime, rank, flags, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix } = this; return { uuid, name, muted, unmarkTime, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix, @@ -601,7 +601,7 @@ export class FishPlayer { //Clear temporary states such as menu and taphandler fishP.activeMenus = []; fishP.tapInfo.commandName = null; - fishP.stats.timeInGame += (Date.now() - fishP.lastJoined); //Time between joining and leaving + fishP.updateStats(stats => stats.timeInGame += (Date.now() - fishP.lastJoined)); //Time between joining and leaving fishP.lastJoined = Date.now(); this.recentLeaves.unshift(fishP); if(this.recentLeaves.length > 10) this.recentLeaves.pop(); @@ -694,7 +694,7 @@ export class FishPlayer { } } fishP.lastActive = Date.now(); - fishP.stats.chatMessagesSent ++; + fishP.updateStats(stats => stats.chatMessagesSent ++); } static onPlayerCommand(player:FishPlayer, command:string, unjoinedRawArgs:string[]){ if(command == "msg" && unjoinedRawArgs[1] == "Please do not use that logic, as it is attem83 logic and is bad to use. For more information please read www.mindustry.dev/attem") @@ -709,11 +709,11 @@ export class FishPlayer { fishPlayer.tapInfo.commandName = null; //Update stats if(!this.ignoreGameOver && fishPlayer.team() != Team.derelict && winningTeam != Team.derelict){ - fishPlayer.stats.gamesFinished ++; + fishPlayer.updateStats(stats => stats.gamesFinished ++); if(fishPlayer.changedTeam){ fishPlayer.sendMessage(`Refusing to update stats due to a team change.`); } else { - if(fishPlayer.team() == winningTeam) fishPlayer.stats.gamesWon ++; + if(fishPlayer.team() == winningTeam) fishPlayer.updateStats(stats => stats.gamesWon ++); } } fishPlayer.changedTeam = false; @@ -1384,6 +1384,11 @@ We apologize for the inconvenience.` return this.info().timesJoined < amount; } + updateStats(func:(stats:Stats) => void):void { + func(this.stats); + func(this.globalStats); + } + /** * Returns a score between 0 and 1, as an estimate of the player's skill level. * Defaults to 0.2 (guessing that the best trusted players can beat 5 noobs) diff --git a/src/types.ts b/src/types.ts index bb681e33..0cfc066f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,6 +36,15 @@ export type TileHistoryEntry = { } + +export type Stats = { + blocksBroken: number; + blocksPlaced: number; + timeInGame: number; + chatMessagesSent: number; + gamesFinished: number; + gamesWon: number; +}; export type FishPlayerData = { uuid: string; name: string; @@ -50,18 +59,15 @@ export type FishPlayerData = { chatStrictness: "chat" | "strict"; lastJoined: number; firstJoined: number; - stats: { - blocksBroken: number; - blocksPlaced: number; - timeInGame: number; - chatMessagesSent: number; - gamesFinished: number; - gamesWon: number; - }; + globalLastJoined: number; + globalFirstJoined: number; + stats: Stats; + globalStats: Stats; showRankPrefix: boolean; /** This field contains long values, store it as a string */ achievements: string; } +export type UploadedFishPlayerData = Omit; export type PlayerHistoryEntry = { action:string; diff --git a/src/utils.ts b/src/utils.ts index aa6e199b..7b50606e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -644,7 +644,7 @@ export const addToTileHistory = logErrors("Error while saving a tilelog entry", const fishP = FishPlayer.get(e.unit.player); //TODO move this code fishP.tstats.blocksBroken ++; - fishP.stats.blocksBroken ++; + fishP.updateStats(stats => stats.blocksBroken ++); } } else { action = "built"; @@ -652,7 +652,7 @@ export const addToTileHistory = logErrors("Error while saving a tilelog entry", if(e.unit?.player?.uuid()){ const fishP = FishPlayer.get(e.unit.player); //TODO move this code - fishP.stats.blocksPlaced ++; + fishP.updateStats(stats => stats.blocksPlaced ++); } } } else if(e instanceof EventType.ConfigEvent){ From 1009eb2d5f204cba5ffbf37d9692e9fd1e522855 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:06:33 +0530 Subject: [PATCH 09/33] Use global stats where appropriate --- build/scripts/achievements.js | 42 +++++++++++++++++------------------ build/scripts/players.js | 13 +++++------ src/achievements.ts | 42 +++++++++++++++++------------------ src/players.ts | 12 +++++----- 4 files changed, 54 insertions(+), 55 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index fe589b09..df32622d 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -297,31 +297,31 @@ exports.Achievements = { hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { modes: ["only", "hexed"] }), minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { modes: ["only", "minigame"] }), //playtime based - playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { checkPlayerInfrequent: function (p) { return p.stats.timeInGame >= funcs_1.Duration.hours(1); } }), - playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { checkPlayerInfrequent: function (p) { return p.stats.timeInGame >= funcs_1.Duration.hours(12); } }), - playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { checkPlayerInfrequent: function (p) { return p.stats.timeInGame >= funcs_1.Duration.days(2); } }), - playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { checkPlayerInfrequent: function (p) { return p.stats.timeInGame >= funcs_1.Duration.days(10); } }), + playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(1); } }), + playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(12); } }), + playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(2); } }), + playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(10); } }), //victories based - victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { checkPlayerGameover: function (p) { return p.stats.gamesWon >= 1; } }), - victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesWon >= 5; } }), - victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesWon >= 30; } }), - victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesWon >= 100; }, notify: "everyone" }), + victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 1; } }), + victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 5; } }), + victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 30; } }), + victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 100; }, notify: "everyone" }), //games based - games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesFinished >= 10; } }), - games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesFinished >= 40; } }), - games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesFinished >= 100; } }), - games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { checkPlayerGameover: function (p) { return p.stats.gamesFinished >= 200; }, notify: "everyone" }), + games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 10; } }), + games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 40; } }), + games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 100; } }), + games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 200; }, notify: "everyone" }), //messages based - messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { notify: "none", checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 1; } }), - messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 100; } }), - messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 500; } }), - messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 2000; } }), - messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { checkPlayerInfrequent: function (p) { return p.stats.chatMessagesSent >= 5000; }, notify: "everyone" }), + messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { notify: "none", checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 1; } }), + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 100; } }), + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 500; } }), + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 2000; } }), + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 5000; }, notify: "everyone" }), //blocks built based - builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: function (p) { return p.stats.blocksPlaced >= 1; }, notify: "none" }), - builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: function (p) { return p.stats.blocksPlaced > 200; } }), - builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { checkPlayerInfrequent: function (p) { return p.stats.blocksPlaced > 1000; } }), - builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: function (p) { return p.stats.blocksPlaced > 5000; }, notify: "everyone" }), + builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced >= 1; }, notify: "none" }), + builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 200; } }), + builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 1000; } }), + builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 5000; }, notify: "everyone" }), //units t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { modes: ["not", "sandbox"], checkPlayerFrequent: function (player) { var _a; diff --git a/build/scripts/players.js b/build/scripts/players.js index b6f3413c..06aa619d 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -411,7 +411,7 @@ var FishPlayer = /** @class */ (function () { else { this.originalName = this.name = player.name; } - if (this.firstJoined == -1) + if (this.firstJoined < 1) this.firstJoined = Date.now(); //Do not update USID here this.manualAfk = false; @@ -1144,12 +1144,11 @@ var FishPlayer = /** @class */ (function () { return; var _loop_1 = function (rankToAssign) { if (!this_1.ranksAtLeast(rankToAssign) && rankToAssign.autoRankData) { - if ( //TODO: use global stats - this_1.joinsAtLeast(rankToAssign.autoRankData.joins) && - this_1.stats.blocksPlaced >= rankToAssign.autoRankData.blocksPlaced && - this_1.stats.timeInGame >= rankToAssign.autoRankData.playtime && - this_1.stats.chatMessagesSent >= rankToAssign.autoRankData.chatMessagesSent && - (Date.now() - this_1.firstJoined) >= rankToAssign.autoRankData.timeSinceFirstJoin) { + if (this_1.joinsAtLeast(rankToAssign.autoRankData.joins) && + this_1.globalStats.blocksPlaced >= rankToAssign.autoRankData.blocksPlaced && + this_1.globalStats.timeInGame >= rankToAssign.autoRankData.playtime && + this_1.globalStats.chatMessagesSent >= rankToAssign.autoRankData.chatMessagesSent && + (Date.now() - this_1.globalFirstJoined) >= rankToAssign.autoRankData.timeSinceFirstJoin) { void this_1.setRank(rankToAssign).then(function () { return _this.sendMessage("You have been automatically promoted to rank ".concat(rankToAssign.coloredName(), "!")); }); diff --git a/src/achievements.ts b/src/achievements.ts index 1d4b09aa..2142dbbe 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -201,35 +201,35 @@ export const Achievements = { minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { modes: ["only", "minigame"] }), //playtime based - playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { checkPlayerInfrequent: p => p.stats.timeInGame >= Duration.hours(1) }), - playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { checkPlayerInfrequent: p => p.stats.timeInGame >= Duration.hours(12) }), - playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { checkPlayerInfrequent: p => p.stats.timeInGame >= Duration.days(2) }), - playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { checkPlayerInfrequent: p => p.stats.timeInGame >= Duration.days(10) }), + playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(1) }), + playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(12) }), + playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(2) }), + playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(10) }), //victories based - victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { checkPlayerGameover: p => p.stats.gamesWon >= 1 }), - victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { checkPlayerGameover: p => p.stats.gamesWon >= 5 }), - victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { checkPlayerGameover: p => p.stats.gamesWon >= 30 }), - victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { checkPlayerGameover: p => p.stats.gamesWon >= 100, notify: "everyone" }), + victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { checkPlayerGameover: p => p.globalStats.gamesWon >= 1 }), + victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { checkPlayerGameover: p => p.globalStats.gamesWon >= 5 }), + victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { checkPlayerGameover: p => p.globalStats.gamesWon >= 30 }), + victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { checkPlayerGameover: p => p.globalStats.gamesWon >= 100, notify: "everyone" }), //games based - games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { checkPlayerGameover: p => p.stats.gamesFinished >= 10 }), - games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { checkPlayerGameover: p => p.stats.gamesFinished >= 40 }), - games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { checkPlayerGameover: p => p.stats.gamesFinished >= 100 }), - games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { checkPlayerGameover: p => p.stats.gamesFinished >= 200, notify: "everyone" }), + games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { checkPlayerGameover: p => p.globalStats.gamesFinished >= 10 }), + games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { checkPlayerGameover: p => p.globalStats.gamesFinished >= 40 }), + games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { checkPlayerGameover: p => p.globalStats.gamesFinished >= 100 }), + games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { checkPlayerGameover: p => p.globalStats.gamesFinished >= 200, notify: "everyone" }), //messages based - messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { notify: "none", checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 1 }), - messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 100 }), - messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 500 }), - messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 2000 }), - messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { checkPlayerInfrequent: p => p.stats.chatMessagesSent >= 5000, notify: "everyone" }), + messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { notify: "none", checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 1 }), + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 100 }), + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 500 }), + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 2000 }), + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 5000, notify: "everyone" }), //blocks built based - builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: p => p.stats.blocksPlaced >= 1, notify: "none" }), - builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: p => p.stats.blocksPlaced > 200 }), - builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { checkPlayerInfrequent: p => p.stats.blocksPlaced > 1000 }), - builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: p => p.stats.blocksPlaced > 5000, notify: "everyone" }), + builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced >= 1, notify: "none" }), + builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 200 }), + builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 1000 }), + builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 5000, notify: "everyone" }), //units t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { modes: ["not", "sandbox"], checkPlayerFrequent(player) { diff --git a/src/players.ts b/src/players.ts index 8586ff27..fe2855ae 100644 --- a/src/players.ts +++ b/src/players.ts @@ -345,7 +345,7 @@ export class FishPlayer { } else { this.originalName = this.name = player.name; } - if(this.firstJoined == -1) this.firstJoined = Date.now(); + if(this.firstJoined < 1) this.firstJoined = Date.now(); //Do not update USID here this.manualAfk = false; @@ -987,12 +987,12 @@ We apologize for the inconvenience.` if(this.stelled()) return; for(const rankToAssign of Rank.autoRanks){ if(!this.ranksAtLeast(rankToAssign) && rankToAssign.autoRankData){ - if( //TODO: use global stats + if( this.joinsAtLeast(rankToAssign.autoRankData.joins) && - this.stats.blocksPlaced >= rankToAssign.autoRankData.blocksPlaced && - this.stats.timeInGame >= rankToAssign.autoRankData.playtime && - this.stats.chatMessagesSent >= rankToAssign.autoRankData.chatMessagesSent && - (Date.now() - this.firstJoined) >= rankToAssign.autoRankData.timeSinceFirstJoin + this.globalStats.blocksPlaced >= rankToAssign.autoRankData.blocksPlaced && + this.globalStats.timeInGame >= rankToAssign.autoRankData.playtime && + this.globalStats.chatMessagesSent >= rankToAssign.autoRankData.chatMessagesSent && + (Date.now() - this.globalFirstJoined) >= rankToAssign.autoRankData.timeSinceFirstJoin ){ void this.setRank(rankToAssign).then(() => this.sendMessage(`You have been automatically promoted to rank ${rankToAssign.coloredName()}!`) From 4a31998e4daece5256ee249e41284de1e34cd604 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:06:46 +0530 Subject: [PATCH 10/33] /stats option to show global stats --- build/scripts/commands/general.js | 7 ++++--- src/commands/general.ts | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index 8ec231de..e1227eb6 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1068,12 +1068,13 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { }, }; }), stats: { - args: ["target:player"], + args: ["target:player", "global:boolean?"], perm: commands_1.Perm.none, description: "Views a player's stats.", handler: function (_a) { - var target = _a.args.target, output = _a.output, f = _a.f; - output(f(templateObject_18 || (templateObject_18 = __makeTemplateObject(["[accent]Statistics for player ", ":\n(note: we started recording statistics on 22 Jan 2024)\n[white]--------------[]\nBlocks broken: ", "\nBlocks placed: ", "\nChat messages sent: ", "\nGames finished: ", "\nTime in-game: ", "\nWin rate: ", ""], ["[accent]\\\nStatistics for player ", ":\n(note: we started recording statistics on 22 Jan 2024)\n[white]--------------[]\nBlocks broken: ", "\nBlocks placed: ", "\nChat messages sent: ", "\nGames finished: ", "\nTime in-game: ", "\nWin rate: ", ""])), target, target.stats.blocksBroken, target.stats.blocksPlaced, target.stats.chatMessagesSent, target.stats.gamesFinished, (0, utils_1.formatTime)(target.stats.timeInGame), target.stats.gamesWon / target.stats.gamesFinished)); + var _b = _a.args, target = _b.target, _c = _b.global, global = _c === void 0 ? false : _c, output = _a.output, f = _a.f; + var stats = global ? target.globalStats : target.stats; + output(f(templateObject_18 || (templateObject_18 = __makeTemplateObject(["[accent]Statistics for player ", " ", ":\n(note: we started recording statistics on 22 Jan 2024)\n[white]--------------[]\nBlocks broken: ", "\nBlocks placed: ", "\nChat messages sent: ", "\nGames finished: ", "\nTime in-game: ", "\nWin rate: ", ""], ["[accent]\\\nStatistics for player ", " ", ":\n(note: we started recording statistics on 22 Jan 2024)\n[white]--------------[]\nBlocks broken: ", "\nBlocks placed: ", "\nChat messages sent: ", "\nGames finished: ", "\nTime in-game: ", "\nWin rate: ", ""])), target, global ? "on this server" : "across all servers", stats.blocksBroken, stats.blocksPlaced, stats.chatMessagesSent, stats.gamesFinished, (0, utils_1.formatTime)(stats.timeInGame), stats.gamesWon / stats.gamesFinished)); } }, showworld: { args: ["x:number?", "y:number?", "size:number?"], diff --git a/src/commands/general.ts b/src/commands/general.ts index 3d9f2d29..002f49ec 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -1013,20 +1013,21 @@ ${highestVotedMaps.map(({key:map, value:votes}) => }; }), stats: { - args: ["target:player"], + args: ["target:player", "global:boolean?"], perm: Perm.none, description: "Views a player's stats.", - handler({args:{target}, output, f}){ + handler({args:{target, global = false}, output, f}){ + const stats = global ? target.globalStats : target.stats; output(f`[accent]\ -Statistics for player ${target}: +Statistics for player ${target} ${global ? "on this server" : "across all servers"}: (note: we started recording statistics on 22 Jan 2024) [white]--------------[] -Blocks broken: ${target.stats.blocksBroken} -Blocks placed: ${target.stats.blocksPlaced} -Chat messages sent: ${target.stats.chatMessagesSent} -Games finished: ${target.stats.gamesFinished} -Time in-game: ${formatTime(target.stats.timeInGame)} -Win rate: ${target.stats.gamesWon / target.stats.gamesFinished}` +Blocks broken: ${stats.blocksBroken} +Blocks placed: ${stats.blocksPlaced} +Chat messages sent: ${stats.chatMessagesSent} +Games finished: ${stats.gamesFinished} +Time in-game: ${formatTime(stats.timeInGame)} +Win rate: ${stats.gamesWon / stats.gamesFinished}` ); } }, From 84d0fefebc6414fbc40464d5cea5d4457b8888f3 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:37:42 +0530 Subject: [PATCH 11/33] Track waves survived and map start time as transients --- build/scripts/players.js | 16 +++++++++++++--- src/players.ts | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/build/scripts/players.js b/build/scripts/players.js index 06aa619d..ee4961ee 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -121,6 +121,8 @@ var FishPlayer = /** @class */ (function () { this.tstats = { //remember to clear this in updateSavedInfoFromPlayer! blocksBroken: 0, + lastMapStartTime: 0, + wavesSurvived: 0, }; /** Whether the player has manually marked themselves as AFK. */ this.manualAfk = false; @@ -424,9 +426,7 @@ var FishPlayer = /** @class */ (function () { this.shouldUpdateName = true; this.changedTeam = false; this.ipDetectedVpn = false; - this.tstats = { - blocksBroken: 0 - }; + this.tstats.blocksBroken = 0; this.infoUpdated = true; }; FishPlayer.prototype.updateData = function (data) { @@ -844,6 +844,7 @@ var FishPlayer = /** @class */ (function () { } } fishPlayer.changedTeam = false; + fishPlayer.tstats.wavesSurvived = 0; }); }; FishPlayer.ignoreGameover = function (callback) { @@ -851,6 +852,12 @@ var FishPlayer = /** @class */ (function () { callback(); this.ignoreGameOver = false; }; + FishPlayer.onGameBegin = function () { + var startTime = Date.now(); + FishPlayer.lastMapStartTime = startTime; + //wait 7 seconds for players to join + Timer.schedule(function () { return FishPlayer.forEachPlayer(function (p) { return p.tstats.lastMapStartTime = startTime; }); }, 7); + }; /** Must be run on UnitChangeEvent. */ FishPlayer.onUnitChange = function (player, unit) { if (unit === null || unit === void 0 ? void 0 : unit.spawnedByCore) @@ -1749,6 +1756,7 @@ var FishPlayer = /** @class */ (function () { FishPlayer.antiBotModePersist = false; FishPlayer.antiBotModeOverride = false; FishPlayer.lastBotWhacked = 0; + FishPlayer.lastMapStartTime = 0; FishPlayer.search = (0, funcs_1.search)(function (p, str) { return p.uuid === str; }, function (p, str) { return p.player.id.toString() === str; }, function (p, str) { return p.name.toLowerCase() === str.toLowerCase(); }, // (p, str) => p.cleanedName === str, function (p, str) { return p.cleanedName.toLowerCase() === str.toLowerCase(); }, function (p, str) { return p.name.toLowerCase().includes(str.toLowerCase()); }, @@ -1763,4 +1771,6 @@ var FishPlayer = /** @class */ (function () { return FishPlayer; }()); exports.FishPlayer = FishPlayer; +//TODO convert all the unnecessary event handlers to simple calls to Events.on +Events.on(EventType.WaveEvent, function () { return FishPlayer.forEachPlayer(function (p) { return p.tstats.wavesSurvived++; }); }); var templateObject_1, templateObject_2, templateObject_3, templateObject_4; diff --git a/src/players.ts b/src/players.ts index fe2855ae..0896cf4f 100644 --- a/src/players.ts +++ b/src/players.ts @@ -49,6 +49,7 @@ export class FishPlayer { static antiBotModePersist = false; static antiBotModeOverride = false; static lastBotWhacked = 0; + static lastMapStartTime = 0; //#endregion //#region Transient properties @@ -88,6 +89,8 @@ export class FishPlayer { tstats = { //remember to clear this in updateSavedInfoFromPlayer! blocksBroken: 0, + lastMapStartTime: 0, + wavesSurvived: 0, }; /** Whether the player has manually marked themselves as AFK. */ manualAfk = false; @@ -357,9 +360,7 @@ export class FishPlayer { this.shouldUpdateName = true; this.changedTeam = false; this.ipDetectedVpn = false; - this.tstats = { - blocksBroken: 0 - }; + this.tstats.blocksBroken = 0; this.infoUpdated = true; } updateData(data: Partial){ @@ -717,6 +718,7 @@ export class FishPlayer { } } fishPlayer.changedTeam = false; + fishPlayer.tstats.wavesSurvived = 0; }); } static ignoreGameover(callback:() => unknown){ @@ -724,6 +726,12 @@ export class FishPlayer { callback(); this.ignoreGameOver = false; } + static onGameBegin(){ + const startTime = Date.now(); + FishPlayer.lastMapStartTime = startTime; + //wait 7 seconds for players to join + Timer.schedule(() => FishPlayer.forEachPlayer(p => p.tstats.lastMapStartTime = startTime), 7); + } /** Must be run on UnitChangeEvent. */ static onUnitChange(player:mindustryPlayer, unit:Unit | null){ if(unit?.spawnedByCore) @@ -1509,3 +1517,5 @@ Please look at ${this.position()} and see if they were actually griefing. If the } +//TODO convert all the unnecessary event handlers to simple calls to Events.on +Events.on(EventType.WaveEvent, () => FishPlayer.forEachPlayer(p => p.tstats.wavesSurvived ++)); From 4c7ef1519f2807ca8222c0937022859c64a3fd5a Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:41:13 +0530 Subject: [PATCH 12/33] Implement gamemode achievements, fix formatting --- build/scripts/achievements.js | 274 ++++++++++++++++++++++------- build/scripts/index.js | 1 + src/achievements.ts | 318 +++++++++++++++++++++++++--------- src/index.ts | 1 + 4 files changed, 447 insertions(+), 147 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index df32622d..d4344801 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -286,58 +286,160 @@ exports.Achievements = { // Do not remove any achievements: instead, set the "disabled" option to true. // Reordering achievements will cause ID shifts. //Joining based - welcome: new Achievement("_", "Welcome", "Join the server.", { checkPlayerJoin: function () { return true; }, notify: "none" }), - migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { hidden: true }), //TODO - frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", "Join the server 100 times.", { checkPlayerJoin: function (p) { return p.info().timesJoined >= 100; } }), + welcome: new Achievement("_", "Welcome", "Join the server.", { + checkPlayerJoin: function () { return true; }, + notify: "none" + }), + migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { + hidden: true + }), //TODO + frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", "Join the server 100 times.", { + checkPlayerJoin: function (p) { return p.info().timesJoined >= 100; } + }), //Gamemode based - attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { modes: ["only", "attack"] }), - survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { modes: ["only", "survival"] }), - pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { modes: ["only", "pvp"] }), - sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { modes: ["only", "sandbox"], checkPlayerInfrequent: function (p) { return p.stats.timeInGame > funcs_1.Duration.hours(1); } }), - hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { modes: ["only", "hexed"] }), - minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { modes: ["only", "minigame"] }), + attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { + modes: ["only", "attack"], + checkPlayerGameover: function (player, winTeam) { + return Vars.state.rules.defaultTeam == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; + }, + }), + survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { + modes: ["only", "survival"], + checkPlayerInfrequent: function (player) { + return player.tstats.wavesSurvived >= 50; + }, + }), + pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { + modes: ["only", "pvp"], + checkPlayerGameover: function (player, winTeam) { + return player.team() == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; + }, + }), + sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { + modes: ["only", "sandbox"], + checkPlayerInfrequent: function (p) { return p.stats.timeInGame > funcs_1.Duration.hours(1); }, + }), + hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { + modes: ["only", "hexed"], + checkPlayerGameover: function (player) { + return player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; + }, + }), + minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { + modes: ["only", "minigame"], + checkPlayerGameover: function (player, winTeam) { + return player.team() == winTeam && player.tstats.lastMapStartTime == players_1.FishPlayer.lastMapStartTime; + }, + }), //playtime based - playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(1); } }), - playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(12); } }), - playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(2); } }), - playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(10); } }), + playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { + checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(1); } + }), + playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { + checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.hours(12); } + }), + playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { + checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(2); } + }), + playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { + checkPlayerInfrequent: function (p) { return p.globalStats.timeInGame >= funcs_1.Duration.days(10); } + }), //victories based - victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 1; } }), - victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 5; } }), - victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 30; } }), - victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 100; }, notify: "everyone" }), + victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 1; } + }), + victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 5; } + }), + victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 30; } + }), + victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesWon >= 100; }, + notify: "everyone" + }), //games based - games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 10; } }), - games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 40; } }), - games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 100; } }), - games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 200; }, notify: "everyone" }), + games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 10; } + }), + games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 40; } + }), + games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 100; } + }), + games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { + checkPlayerGameover: function (p) { return p.globalStats.gamesFinished >= 200; }, + notify: "everyone" + }), //messages based - messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { notify: "none", checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 1; } }), - messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 100; } }), - messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 500; } }), - messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 2000; } }), - messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 5000; }, notify: "everyone" }), + messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 1; }, + notify: "none" + }), + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 100; } + }), + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 500; } + }), + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 2000; } + }), + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { + checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 5000; }, + notify: "everyone" + }), //blocks built based - builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced >= 1; }, notify: "none" }), - builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 200; } }), - builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 1000; } }), - builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 5000; }, notify: "everyone" }), + builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { + checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced >= 1; }, + notify: "none" + }), + builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { + checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 200; } + }), + builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { + checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 1000; } + }), + builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { + checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 5000; }, + notify: "everyone" + }), //units - t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { modes: ["not", "sandbox"], checkPlayerFrequent: function (player) { + t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { + modes: ["not", "sandbox"], + checkPlayerFrequent: function (player) { var _a; return globals_1.unitsT5.includes((_a = player.unit()) === null || _a === void 0 ? void 0 : _a.type); - }, }), - dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { modes: ["not", "sandbox"], hidden: false }), //TODO - worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { checkPlayerFrequent: function (player) { + }, + }), + dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { + checkPlayerFrequent: function (player) { var _a; return ((_a = player.unit()) === null || _a === void 0 ? void 0 : _a.type) == UnitTypes.latum; - }, }), + } + }), //pvp - above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { modes: ["only", "pvp"], checkPlayerInfrequent: function (p) { return p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20; } }), - head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { modes: ["only", "pvp"], hidden: true }), //TODO - one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { modes: ["only", "pvp"], hidden: true }), //TODO + above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { + modes: ["only", "pvp"], + checkPlayerInfrequent: function (p) { return p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20; } + }), + head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { + modes: ["only", "pvp"], + hidden: true + }), //TODO + one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { + modes: ["only", "pvp"], + hidden: true + }), //TODO //sandbox - underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { modes: ["only", "sandbox"], checkFrequent: function () { + underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { + modes: ["only", "sandbox"], + checkFrequent: function () { var found = false; //deliberate ordering for performance reasons Groups.powerGraph.each(function (_a) { @@ -347,12 +449,21 @@ exports.Achievements = { found = true; }); return found; - } }), + } + }), //easter eggs - memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { notify: "none" }), - run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { notify: "everyone" }), - script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { notify: "none" }), - hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { hidden: true }), + memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { + notify: "none" + }), + run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { + notify: "everyone" + }), + script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { + notify: "none" + }), + hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { + hidden: true + }), //items based items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { modes: ["not", "sandbox"], @@ -390,13 +501,17 @@ exports.Achievements = { return items.allMatch(function (i) { return module.has(i, capacity); }); }, }), - siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { modes: ["not", "sandbox"] }), + siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { + modes: ["not", "sandbox"] + }), silicon_100k: new Achievement(["green", Items.silicon.emoji()], "Silicon for days", "Obtain 100k silicon.", { modes: ["not", "sandbox"], checkFrequent: function (team) { return team.items().has(Items.silicon, 100000); } }), //other players based - alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { notify: "none" }), + alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { + notify: "none" + }), join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { checkPlayerJoin: function () { return Groups.player.size() > 20; }, }), @@ -412,9 +527,15 @@ exports.Achievements = { checkInfrequent: function () { return Groups.player.contains(function (p) { return players_1.FishPlayer.get(p).marked(); }); }, }), //maps based - beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { notify: "everyone", modes: ["not", "pvp"], checkGameover: function (team) { return team == Vars.state.rules.defaultTeam && !Vars.state.map.custom; } }), + beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { + notify: "everyone", + modes: ["not", "pvp"], + checkGameover: function (team) { return team == Vars.state.rules.defaultTeam && !Vars.state.map.custom; } + }), //misc - power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { modes: ["not", "sandbox"], checkFrequent: function () { + power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { + modes: ["not", "sandbox"], + checkFrequent: function () { var found = false; //deliberate ordering for performance reasons Groups.powerGraph.each(function (_a) { @@ -424,29 +545,60 @@ exports.Achievements = { found = true; }); return found; - }, }), - pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { modes: ["not", "sandbox"], hidden: true }), //TODO - core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { modes: ["not", "sandbox"], hidden: true }), //TODO - enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { modes: ["not", "sandbox"], hidden: true }), //TODO - verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "none" }), - afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without doing anything.", { modes: ["not", "sandbox"], hidden: true }), //TODO - status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { checkPlayerFrequent: function (p) { + } + }), + pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { + checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "none" + }), + afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without doing anything.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { + checkPlayerFrequent: function (p) { var unit = p.unit(); if (!unit) return false; var statuses = Reflect.get(unit, "statuses"); return statuses.size >= 5; - }, modes: ["not", "sandbox"] }), - drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { notify: "everyone", modes: ["not", "sandbox"] }), - drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", "Drown a Mace in ".concat(Blocks.cryofluid.emoji(), " Cryofluid."), { notify: "everyone", modes: ["not", "sandbox"] }), - max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { checkPlayerFrequent: function (player) { + }, + modes: ["not", "sandbox"] + }), + drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { + notify: "everyone", + modes: ["not", "sandbox"] + }), + drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", "Drown a Mace in ".concat(Blocks.cryofluid.emoji(), " Cryofluid."), { + notify: "everyone", + modes: ["not", "sandbox"] + }), + max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { + checkPlayerFrequent: function (player) { var _a, _b; var tile = (_b = (_a = player.unit()) === null || _a === void 0 ? void 0 : _a.tile) === null || _b === void 0 ? void 0 : _b.call(_a); if (!tile) return false; return tile.block == Blocks.duo && tile.ammo.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; - }, notify: "everyone", modes: ["not", "sandbox"] }), - foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { notify: "everyone", modes: ["not", "sandbox"] }), + }, + notify: "everyone", + modes: ["not", "sandbox"] + }), + foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { + notify: "everyone", + modes: ["not", "sandbox"] + }), impacts_15: new Achievement(["green", Blocks.impactReactor.emoji()], "Darthscion's Nightmare", "Run 15 impact reactors at full efficiency.", { modes: ["not", "sandbox"], notify: "everyone", diff --git a/build/scripts/index.js b/build/scripts/index.js index 1a35ecc4..20f336c7 100644 --- a/build/scripts/index.js +++ b/build/scripts/index.js @@ -212,6 +212,7 @@ Events.on(EventType.GameOverEvent, function (e) { } players_1.FishPlayer.onGameOver(e.winner); }); +Events.on(EventType.WorldLoadEvent, function () { return players_1.FishPlayer.onGameBegin(); }); Events.on(EventType.PlayerChatEvent, function (e) { players_1.FishPlayer.onPlayerChat(e.player, e.message); }); diff --git a/src/achievements.ts b/src/achievements.ts index 2142dbbe..fab90d1c 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -188,80 +188,185 @@ export const Achievements = { //Joining based - welcome: new Achievement("_", "Welcome", "Join the server.", { checkPlayerJoin: () => true, notify: "none" }), - migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { hidden: true }), //TODO - frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", "Join the server 100 times.", { checkPlayerJoin: p => p.info().timesJoined >= 100 }), + welcome: new Achievement("_", "Welcome", "Join the server.", { + checkPlayerJoin: () => true, + notify: "none" + }), + migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { + hidden: true + }), //TODO + frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", "Join the server 100 times.", { + checkPlayerJoin: p => p.info().timesJoined >= 100 + }), //Gamemode based - attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { modes: ["only", "attack"] }), - survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { modes: ["only", "survival"] }), - pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { modes: ["only", "pvp"] }), - sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { modes: ["only", "sandbox"], checkPlayerInfrequent: p => p.stats.timeInGame > Duration.hours(1) }), - hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { modes: ["only", "hexed"] }), - minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { modes: ["only", "minigame"] }), + attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { + modes: ["only", "attack"], + checkPlayerGameover: (player, winTeam) => + Vars.state.rules.defaultTeam == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, + }), + survival: new Achievement(Iconc.modeSurvival, "Survival", ["Survive 50 waves in a survival map.", "Must be during the same game."], { + modes: ["only", "survival"], + checkPlayerInfrequent: (player) => + player.tstats.wavesSurvived >= 50, + }), + pvp: new Achievement(Iconc.modePvp, "PVP", ["Win a match of PVP.", "You must be present for the beginning and end of the game."], { + modes: ["only", "pvp"], + checkPlayerGameover: (player, winTeam) => + player.team() == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, + }), + sandbox: new Achievement(Iconc.image, "Sandbox", "Spend 1 hour in Sandbox.", { + modes: ["only", "sandbox"], + checkPlayerInfrequent: p => p.stats.timeInGame > Duration.hours(1), + }), + hexed: new Achievement(Iconc.layers, "Hexed", ["Play a match of Hexed.", "You must be present for the beginning and end of the game."], { + modes: ["only", "hexed"], + checkPlayerGameover: (player) => + player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, + }), + minigame: new Achievement(Iconc.play, "Minigame", ["Win a Minigame.", "You must be present for the beginning and end of the game."], { + modes: ["only", "minigame"], + checkPlayerGameover: (player, winTeam) => + player.team() == winTeam && player.tstats.lastMapStartTime == FishPlayer.lastMapStartTime, + }), //playtime based - playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(1) }), - playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(12) }), - playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(2) }), - playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(10) }), + playtime_1: new Achievement(["white", Iconc.googleplay], "Playtime 1", "Spend 1 hour in-game.", { + checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(1) + }), + playtime_2: new Achievement(["red", Iconc.googleplay], "Playtime 2", "Spend 12 hours in-game.", { + checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.hours(12) + }), + playtime_3: new Achievement(["orange", Iconc.googleplay], "Playtime 3", "Spend 2 days in-game.", { + checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(2) + }), + playtime_4: new Achievement(["yellow", Iconc.googleplay], "Playtime 4", "Spend 10 days in-game.", { + checkPlayerInfrequent: p => p.globalStats.timeInGame >= Duration.days(10) + }), //victories based - victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { checkPlayerGameover: p => p.globalStats.gamesWon >= 1 }), - victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { checkPlayerGameover: p => p.globalStats.gamesWon >= 5 }), - victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { checkPlayerGameover: p => p.globalStats.gamesWon >= 30 }), - victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { checkPlayerGameover: p => p.globalStats.gamesWon >= 100, notify: "everyone" }), + victory_1: new Achievement(["white", Iconc.star], "First Victory", "Win a map run.", { + checkPlayerGameover: p => p.globalStats.gamesWon >= 1 + }), + victory_2: new Achievement(["red", Iconc.star], "Victories 2", "Win 5 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesWon >= 5 + }), + victory_3: new Achievement(["orange", Iconc.star], "Victories 3", "Win 30 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesWon >= 30 + }), + victory_4: new Achievement(["yellow", Iconc.star], "Victories 4", "Win 100 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesWon >= 100, + notify: "everyone" + }), //games based - games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { checkPlayerGameover: p => p.globalStats.gamesFinished >= 10 }), - games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { checkPlayerGameover: p => p.globalStats.gamesFinished >= 40 }), - games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { checkPlayerGameover: p => p.globalStats.gamesFinished >= 100 }), - games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { checkPlayerGameover: p => p.globalStats.gamesFinished >= 200, notify: "everyone" }), + games_1: new Achievement(["white", Iconc.itchio], "Games 1", "Play 10 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesFinished >= 10 + }), + games_2: new Achievement(["red", Iconc.itchio], "Games 2", "Play 40 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesFinished >= 40 + }), + games_3: new Achievement(["orange", Iconc.itchio], "Games 3", "Play 100 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesFinished >= 100 + }), + games_4: new Achievement(["yellow", Iconc.itchio], "Games 4", "Play 200 map runs.", { + checkPlayerGameover: p => p.globalStats.gamesFinished >= 200, + notify: "everyone" + }), //messages based - messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { notify: "none", checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 1 }), - messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 100 }), - messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 500 }), - messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 2000 }), - messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 5000, notify: "everyone" }), + messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 1, + notify: "none" + }), + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 100 + }), + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 500 + }), + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 2000 + }), + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { + checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 5000, + notify: "everyone" + }), //blocks built based - builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced >= 1, notify: "none" }), - builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 200 }), - builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 1000 }), - builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 5000, notify: "everyone" }), + builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { + checkPlayerInfrequent: p => p.globalStats.blocksPlaced >= 1, + notify: "none" + }), + builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { + checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 200 + }), + builds_3: new Achievement(["orange", Iconc.fileText], "The Factory Must Produce", "Construct 1000 buildings.", { + checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 1000 + }), + builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { + checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 5000, + notify: "everyone" + }), //units - t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { modes: ["not", "sandbox"], checkPlayerFrequent(player) { - return (unitsT5 as Array).includes(player.unit()?.type); - }, }), - dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { modes: ["not", "sandbox"], hidden: false }), //TODO - worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { checkPlayerFrequent(player) { - return player.unit()?.type == UnitTypes.latum; - }, }), + t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { + modes: ["not", "sandbox"], checkPlayerFrequent(player) { + return (unitsT5 as Array).includes(player.unit()?.type); + }, + }), + dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { + checkPlayerFrequent(player) { + return player.unit()?.type == UnitTypes.latum; + } + }), //pvp - above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { modes: ["only", "pvp"], checkPlayerInfrequent: p => p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20 }), - head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { modes: ["only", "pvp"], hidden: true }), //TODO - one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { modes: ["only", "pvp"], hidden: true }), //TODO + above_average: new Achievement(Iconc.chartBar, "Above Average", ["Reach a win rate above 50%.", "Must be over at least 20 games of PVP."], { + modes: ["only", "pvp"], + checkPlayerInfrequent: p => p.stats.gamesWon / p.stats.gamesFinished > 0.5 && p.stats.gamesFinished >= 20 + }), + head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { + modes: ["only", "pvp"], + hidden: true + }), //TODO + one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { + modes: ["only", "pvp"], + hidden: true + }), //TODO //sandbox - underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { modes: ["only", "sandbox"], checkFrequent(){ - let found = false; - //deliberate ordering for performance reasons - Groups.powerGraph.each(({graph}) => { - //we don't need to actually check for power sources, just assume that ~1mil power is a source - if(graph.lastPowerNeeded > graph.lastPowerProduced && graph.lastPowerNeeded < 1e10 && graph.lastPowerProduced >= 999_900) - found = true; - }); - return found; - } }), + underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { + modes: ["only", "sandbox"], + checkFrequent(){ + let found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(({graph}) => { + //we don't need to actually check for power sources, just assume that ~1mil power is a source + if(graph.lastPowerNeeded > graph.lastPowerProduced && graph.lastPowerNeeded < 1e10 && graph.lastPowerProduced >= 999_900) + found = true; + }); + return found; + } + }), //easter eggs - memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { notify: "none" }), - run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { notify: "everyone" }), - script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { notify: "none" }), - hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { hidden: true }), + memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { + notify: "none" + }), + run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { + notify: "everyone" + }), + script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { + notify: "none" + }), + hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { + hidden: true + }), //items based items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { @@ -291,14 +396,18 @@ export const Achievements = { return items.allMatch(i => module.has(i, capacity)); }, }), - siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { modes: ["not", "sandbox"] }), + siligone: new Achievement(["red", Items.silicon.emoji()], "Siligone", ["Run out of silicon.", "You must have reached 2000 silicon before running out."], { + modes: ["not", "sandbox"] + }), silicon_100k: new Achievement(["green", Items.silicon.emoji()], "Silicon for days", "Obtain 100k silicon.", { modes: ["not", "sandbox"], checkFrequent: team => team.items().has(Items.silicon, 100_000) }), //other players based - alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { notify: "none" }), + alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { + notify: "none" + }), join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { checkPlayerJoin: () => Groups.player.size() > 20, }), @@ -315,38 +424,75 @@ export const Achievements = { }), //maps based - beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { notify: "everyone", modes: ["not", "pvp"], checkGameover: (team) => team == Vars.state.rules.defaultTeam && !Vars.state.map.custom }), + beat_map_not_in_rotation: new Achievement(["pink", Iconc.map], "How?", "Beat a map that isn't in the list of maps.", { + notify: "everyone", + modes: ["not", "pvp"], + checkGameover: (team) => team == Vars.state.rules.defaultTeam && !Vars.state.map.custom + }), //misc - power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { modes: ["not", "sandbox"], checkFrequent(){ - let found = false; - //deliberate ordering for performance reasons - Groups.powerGraph.each(({graph}) => { - //we don't need to actually check for power sources, just assume that ~1mil power is a source - if(graph.lastPowerProduced > 1e6 && !graph.producers.contains(boolf(b => b.block == Blocks.powerSource))) - found = true; - }); - return found; - }, }), - pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { modes: ["not", "sandbox"], hidden: true }), //TODO - core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { modes: ["not", "sandbox"], hidden: true }), //TODO - enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { modes: ["not", "sandbox"], hidden: true }), //TODO - verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "none" }), - afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without doing anything.", { modes: ["not", "sandbox"], hidden: true }), //TODO - status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { checkPlayerFrequent: p => { - const unit = p.unit(); - if(!unit) return false; - const statuses = Reflect.get(unit, "statuses") as Seq<{ effect: StatusEffect }>; - return statuses.size >= 5; - }, modes: ["not", "sandbox"] }), - drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { notify: "everyone", modes: ["not", "sandbox"] }), - drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", `Drown a Mace in ${Blocks.cryofluid.emoji()} Cryofluid.`, { notify: "everyone", modes: ["not", "sandbox"] }), - max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { checkPlayerFrequent(player) { - const tile = player.unit()?.tile?.(); - if(!tile) return false; - return tile.block == Blocks.duo && tile.ammo!.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; - }, notify: "everyone", modes: ["not", "sandbox"] }), - foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { notify: "everyone", modes: ["not", "sandbox"] }), + power_1mil: new Achievement(["green", Blocks.powerSource.emoji()], "Who needs sources?", "Reach a power production of 1 million without using power sources.", { + modes: ["not", "sandbox"], + checkFrequent(){ + let found = false; + //deliberate ordering for performance reasons + Groups.powerGraph.each(({graph}) => { + //we don't need to actually check for power sources, just assume that ~1mil power is a source + if(graph.lastPowerProduced > 1e6 && !graph.producers.contains(boolf(b => b.block == Blocks.powerSource))) + found = true; + }); + return found; + } + }), + pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { + checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "none" + }), + afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without doing anything.", { + modes: ["not", "sandbox"], + hidden: true + }), //TODO + status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { + checkPlayerFrequent: p => { + const unit = p.unit(); + if(!unit) return false; + const statuses = Reflect.get(unit, "statuses") as Seq<{ effect: StatusEffect }>; + return statuses.size >= 5; + }, + modes: ["not", "sandbox"] + }), + drown_big_tank: new Achievement(["blue", UnitTypes.conquer.emoji()], "Not Waterproof", "Drown an enemy Conquer or Vanquish.", { + notify: "everyone", + modes: ["not", "sandbox"] + }), + drown_mace_in_cryo: new Achievement(["cyan", UnitTypes.mace.emoji()], "Cooldown", `Drown a Mace in ${Blocks.cryofluid.emoji()} Cryofluid.`, { + notify: "everyone", + modes: ["not", "sandbox"] + }), + max_boost_duo: new Achievement(["yellow", Blocks.duo.emoji()], "In Duo We Trust", "Control a Duo with maximum boosts.", { + checkPlayerFrequent(player) { + const tile = player.unit()?.tile?.(); + if(!tile) return false; + return tile.block == Blocks.duo && tile.ammo!.peek().item == Items.silicon && tile.liquids.current() == Liquids.cryofluid && tile.timeScale() >= 2.5; + }, + notify: "everyone", + modes: ["not", "sandbox"] + }), + foreshadow_overkill: new Achievement(["yellow", Blocks.foreshadow.emoji()], "Overkill", ["Kill a Dagger with a maximally boosted Foreshadow.", "Hint: the maximum overdrive is not +150%..."], { + notify: "everyone", + modes: ["not", "sandbox"] + }), impacts_15: new Achievement(["green", Blocks.impactReactor.emoji()], "Darthscion's Nightmare", "Run 15 impact reactors at full efficiency.", { modes: ["not", "sandbox"], notify: "everyone", diff --git a/src/index.ts b/src/index.ts index ad1c0a07..0394773b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -204,6 +204,7 @@ Events.on(EventType.GameOverEvent, (e) => { } FishPlayer.onGameOver(e.winner as Team); }); +Events.on(EventType.WorldLoadEvent, () => FishPlayer.onGameBegin()); Events.on(EventType.PlayerChatEvent, e => { FishPlayer.onPlayerChat(e.player, e.message); }); From f991f9f124e6a29faf5c4f94734cf3fbc8cedf7f Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:58:36 +0530 Subject: [PATCH 13/33] anti abuse --- src/achievements.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/achievements.ts b/src/achievements.ts index fab90d1c..f9247713 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -195,8 +195,8 @@ export const Achievements = { migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { hidden: true }), //TODO - frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", "Join the server 100 times.", { - checkPlayerJoin: p => p.info().timesJoined >= 100 + frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", ["Join the server 100 times.", "Note: Do not reconnect frequently, that will not work. This achievement requires that you have been playing for 1 month."], { + checkPlayerJoin: p => p.info().timesJoined >= 100 && (Date.now() - p.globalFirstJoined > Duration.months(1)) }), //Gamemode based @@ -279,16 +279,16 @@ export const Achievements = { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 1, notify: "none" }), - messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", ["Send 100 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 100 }), - messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", ["Send 500 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 500 }), - messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", ["Send 2000 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 2000 }), - messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", ["Send 5000 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 5000, notify: "everyone" }), @@ -306,7 +306,6 @@ export const Achievements = { }), builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 5000, - notify: "everyone" }), //units From 7b116d59273f6ec8216516302588d4aba5eb78cf Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:59:34 +0530 Subject: [PATCH 14/33] Implement "afk" achievement --- build/scripts/achievements.js | 36 +++++++++++++++++------------------ build/scripts/players.js | 8 ++++++++ build/scripts/utils.js | 12 ++++++++++++ src/achievements.ts | 10 ++++++---- src/globals.ts | 2 ++ src/players.ts | 8 ++++++++ src/utils.ts | 10 ++++++++++ 7 files changed, 64 insertions(+), 22 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index d4344801..5451381c 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -167,13 +167,12 @@ Events.on(EventType.PlayerJoin, function (_a) { finally { if (e_1) throw e_1.error; } } }); -Events.on(EventType.GameOverEvent, function (_a) { - var e_2, _b; - var _c; - var winner = _a.winner; +globals_1.FishEvents.on("gameOver", function (_, winner) { + var e_2, _a; + var _b; var _loop_1 = function (ach) { if (ach.allowedInMode()) { - if ((_c = ach.checkGameover) === null || _c === void 0 ? void 0 : _c.call(ach, winner)) + if ((_b = ach.checkGameover) === null || _b === void 0 ? void 0 : _b.call(ach, winner)) ach.grantToAllOnline(); else players_1.FishPlayer.forEachPlayer(function (fishP) { @@ -185,15 +184,15 @@ Events.on(EventType.GameOverEvent, function (_a) { } }; try { - for (var _d = __values(Achievement.checkGameover), _e = _d.next(); !_e.done; _e = _d.next()) { - var ach = _e.value; + for (var _c = __values(Achievement.checkGameover), _d = _c.next(); !_d.done; _d = _c.next()) { + var ach = _d.value; _loop_1(ach); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { - if (_e && !_e.done && (_b = _d.return)) _b.call(_d); + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_2) throw e_2.error; } } @@ -293,8 +292,8 @@ exports.Achievements = { migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { hidden: true }), //TODO - frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", "Join the server 100 times.", { - checkPlayerJoin: function (p) { return p.info().timesJoined >= 100; } + frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", ["Join the server 100 times.", "Note: Do not reconnect frequently, that will not work. This achievement requires that you have been playing for 1 month."], { + checkPlayerJoin: function (p) { return p.info().timesJoined >= 100 && (Date.now() - p.globalFirstJoined > funcs_1.Duration.months(1)); } }), //Gamemode based attack: new Achievement(Iconc.modeAttack, "Attack", ["Defeat an attack map.", "You must be present for the beginning and end of the game."], { @@ -377,16 +376,16 @@ exports.Achievements = { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 1; }, notify: "none" }), - messages_2: new Achievement(["red", Iconc.chat], "Chat 2", "Send 100 chat messages.", { + messages_2: new Achievement(["red", Iconc.chat], "Chat 2", ["Send 100 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 100; } }), - messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", "Send 500 chat messages.", { + messages_3: new Achievement(["orange", Iconc.chat], "Chat 3", ["Send 500 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 500; } }), - messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", "Send 2000 chat messages.", { + messages_4: new Achievement(["yellow", Iconc.chat], "Chat 4", ["Send 2000 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 2000; } }), - messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", "Send 5000 chat messages.", { + messages_5: new Achievement(["lime", Iconc.chat], "Chat 4", ["Send 5000 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 5000; }, notify: "everyone" }), @@ -403,7 +402,6 @@ exports.Achievements = { }), builds_4: new Achievement(["yellow", Iconc.fileText], "The Factory Must Grow", "Construct 5000 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 5000; }, - notify: "everyone" }), //units t5: new Achievement(Blocks.tetrativeReconstructor.emoji(), "T5", "Control a T5 unit.", { @@ -562,10 +560,12 @@ exports.Achievements = { verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "none" }), - afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without doing anything.", { + afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { modes: ["not", "sandbox"], - hidden: true - }), //TODO + checkPlayerGameover: function (player, winTeam) { + return player.team() == winTeam && player.tstats.blockInteractionsThisMap == 0; + }, + }), status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { checkPlayerFrequent: function (p) { var unit = p.unit(); diff --git a/build/scripts/players.js b/build/scripts/players.js index ee4961ee..1f301de9 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -121,7 +121,9 @@ var FishPlayer = /** @class */ (function () { this.tstats = { //remember to clear this in updateSavedInfoFromPlayer! blocksBroken: 0, + blockInteractionsThisMap: 0, lastMapStartTime: 0, + lastMapPlayedTime: 0, wavesSurvived: 0, }; /** Whether the player has manually marked themselves as AFK. */ @@ -427,6 +429,10 @@ var FishPlayer = /** @class */ (function () { this.changedTeam = false; this.ipDetectedVpn = false; this.tstats.blocksBroken = 0; + if (this.tstats.lastMapPlayedTime != FishPlayer.lastMapStartTime) { + this.tstats.blockInteractionsThisMap = 0; + this.tstats.lastMapPlayedTime = FishPlayer.lastMapStartTime; + } this.infoUpdated = true; }; FishPlayer.prototype.updateData = function (data) { @@ -828,6 +834,7 @@ var FishPlayer = /** @class */ (function () { }; FishPlayer.onGameOver = function (winningTeam) { var _this = this; + globals_1.FishEvents.fire("gameOver", [winningTeam]); this.forEachPlayer(function (fishPlayer) { //Clear temporary states such as menu and taphandler fishPlayer.activeMenus = []; @@ -845,6 +852,7 @@ var FishPlayer = /** @class */ (function () { } fishPlayer.changedTeam = false; fishPlayer.tstats.wavesSurvived = 0; + fishPlayer.tstats.blockInteractionsThisMap = 0; }); }; FishPlayer.ignoreGameover = function (callback) { diff --git a/build/scripts/utils.js b/build/scripts/utils.js index 344f6946..11c06acd 100644 --- a/build/scripts/utils.js +++ b/build/scripts/utils.js @@ -774,6 +774,7 @@ exports.addToTileHistory = logErrors("Error while saving a tilelog entry", funct var fishP = players_1.FishPlayer.get(e.unit.player); //TODO move this code fishP.tstats.blocksBroken++; + fishP.tstats.blockInteractionsThisMap++; fishP.updateStats(function (stats) { return stats.blocksBroken++; }); } } @@ -784,18 +785,29 @@ exports.addToTileHistory = logErrors("Error while saving a tilelog entry", funct var fishP = players_1.FishPlayer.get(e.unit.player); //TODO move this code fishP.updateStats(function (stats) { return stats.blocksPlaced++; }); + fishP.tstats.blockInteractionsThisMap++; } } } else if (e instanceof EventType.ConfigEvent) { tile = e.tile.tile; uuid = (_m = (_l = e.player) === null || _l === void 0 ? void 0 : _l.uuid()) !== null && _m !== void 0 ? _m : "unknown"; + if (uuid != "unknown") { + var fishP = players_1.FishPlayer.getById(uuid); + if (fishP) + fishP.tstats.blockInteractionsThisMap++; + } action = "configured"; type = e.tile.block.name; } else if (e instanceof EventType.BuildRotateEvent) { tile = e.build.tile; uuid = (_s = (_q = (_p = (_o = e.unit) === null || _o === void 0 ? void 0 : _o.player) === null || _p === void 0 ? void 0 : _p.uuid()) !== null && _q !== void 0 ? _q : (_r = e.unit) === null || _r === void 0 ? void 0 : _r.type.name) !== null && _s !== void 0 ? _s : "unknown"; + if (uuid != "unknown") { + var fishP = players_1.FishPlayer.getById(uuid); + if (fishP) + fishP.tstats.blockInteractionsThisMap++; + } action = "rotated"; type = e.build.block.name; } diff --git a/src/achievements.ts b/src/achievements.ts index f9247713..c75b8a50 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -125,7 +125,7 @@ Events.on(EventType.PlayerJoin, ({player}: {player: mindustryPlayer}) => { } } }); -Events.on(EventType.GameOverEvent, ({winner}) => { +FishEvents.on("gameOver", (_, winner) => { for(const ach of Achievement.checkGameover){ if(ach.allowedInMode()){ if(ach.checkGameover?.(winner)) ach.grantToAllOnline(); @@ -458,10 +458,12 @@ export const Achievements = { verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "none" }), - afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without doing anything.", { + afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { modes: ["not", "sandbox"], - hidden: true - }), //TODO + checkPlayerGameover(player, winTeam) { + return player.team() == winTeam && player.tstats.blockInteractionsThisMap == 0; + }, + }), status_effects_5: new Achievement(StatusEffects.electrified.emoji(), "A Furious Cocktail", "Have at least 5 status effects at once.", { checkPlayerFrequent: p => { const unit = p.unit(); diff --git a/src/globals.ts b/src/globals.ts index 81aab91e..68e9c908 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -46,4 +46,6 @@ export const FishEvents = new EventEmitter<{ memoryCorruption: []; /** Called when the "say" console command is run. */ serverSays: []; + /** Fired on gameover, but before player data is reset. */ + gameOver: [winningTeam: Team]; }>(); diff --git a/src/players.ts b/src/players.ts index 0896cf4f..fc6ed588 100644 --- a/src/players.ts +++ b/src/players.ts @@ -89,7 +89,9 @@ export class FishPlayer { tstats = { //remember to clear this in updateSavedInfoFromPlayer! blocksBroken: 0, + blockInteractionsThisMap: 0, lastMapStartTime: 0, + lastMapPlayedTime: 0, wavesSurvived: 0, }; /** Whether the player has manually marked themselves as AFK. */ @@ -361,6 +363,10 @@ export class FishPlayer { this.changedTeam = false; this.ipDetectedVpn = false; this.tstats.blocksBroken = 0; + if(this.tstats.lastMapPlayedTime != FishPlayer.lastMapStartTime){ + this.tstats.blockInteractionsThisMap = 0; + this.tstats.lastMapPlayedTime = FishPlayer.lastMapStartTime; + } this.infoUpdated = true; } updateData(data: Partial){ @@ -704,6 +710,7 @@ export class FishPlayer { } private static ignoreGameOver = false; static onGameOver(winningTeam:Team){ + FishEvents.fire("gameOver", [winningTeam]); this.forEachPlayer((fishPlayer) => { //Clear temporary states such as menu and taphandler fishPlayer.activeMenus = []; @@ -719,6 +726,7 @@ export class FishPlayer { } fishPlayer.changedTeam = false; fishPlayer.tstats.wavesSurvived = 0; + fishPlayer.tstats.blockInteractionsThisMap = 0; }); } static ignoreGameover(callback:() => unknown){ diff --git a/src/utils.ts b/src/utils.ts index 7b50606e..6a322026 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -644,6 +644,7 @@ export const addToTileHistory = logErrors("Error while saving a tilelog entry", const fishP = FishPlayer.get(e.unit.player); //TODO move this code fishP.tstats.blocksBroken ++; + fishP.tstats.blockInteractionsThisMap ++; fishP.updateStats(stats => stats.blocksBroken ++); } } else { @@ -653,16 +654,25 @@ export const addToTileHistory = logErrors("Error while saving a tilelog entry", const fishP = FishPlayer.get(e.unit.player); //TODO move this code fishP.updateStats(stats => stats.blocksPlaced ++); + fishP.tstats.blockInteractionsThisMap ++; } } } else if(e instanceof EventType.ConfigEvent){ tile = e.tile.tile; uuid = e.player?.uuid() ?? "unknown"; + if(uuid != "unknown"){ + const fishP = FishPlayer.getById(uuid); + if(fishP) fishP.tstats.blockInteractionsThisMap ++; + } action = "configured"; type = e.tile.block.name; } else if(e instanceof EventType.BuildRotateEvent){ tile = e.build.tile; uuid = e.unit?.player?.uuid() ?? e.unit?.type.name ?? "unknown"; + if(uuid != "unknown"){ + const fishP = FishPlayer.getById(uuid); + if(fishP) fishP.tstats.blockInteractionsThisMap ++; + } action = "rotated"; type = e.build.block.name; } else if(e instanceof EventType.UnitDestroyEvent){ From 8fcb7f1bd542f6fdbaae36f931bab0657c2d65eb Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:14:49 +0530 Subject: [PATCH 15/33] Rename none to nobody, use disabled instead of hidden where appropriate --- build/scripts/achievements.js | 59 ++++++++++++++++++++--------------- src/achievements.ts | 57 +++++++++++++++++++-------------- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index 5451381c..d18218c2 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -83,23 +83,30 @@ var Achievement = /** @class */ (function () { Object.assign(this, options); if (options.modes) { var _b = __read(options.modes), type = _b[0], modes_1 = _b.slice(1); - if (type == "only") + if (type == "only") { this.allowedModes = modes_1; - else + this.modesText = modes_1.join(", "); + } + else { this.allowedModes = config_1.GamemodeNames.filter(function (m) { return !modes_1.includes(m); }); + this.modesText = "all except ".concat(modes_1.join(", ")); + } } else { this.allowedModes = config_1.GamemodeNames; + this.modesText = "all"; + } + if (!this.disabled) { + Achievement.all.push(this); + if (this.checkPlayerFrequent || this.checkFrequent) + Achievement.checkFrequent.push(this); + if (this.checkPlayerInfrequent || this.checkInfrequent) + Achievement.checkInfrequent.push(this); + if (this.checkPlayerJoin) + Achievement.checkJoin.push(this); + if (this.checkPlayerGameover || this.checkGameover) + Achievement.checkGameover.push(this); } - Achievement.all.push(this); - if (this.checkPlayerFrequent || this.checkFrequent) - Achievement.checkFrequent.push(this); - if (this.checkPlayerInfrequent || this.checkInfrequent) - Achievement.checkInfrequent.push(this); - if (this.checkPlayerJoin) - Achievement.checkJoin.push(this); - if (this.checkPlayerGameover || this.checkGameover) - Achievement.checkGameover.push(this); } Achievement.prototype.message = function () { return config_1.FColor.achievement(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Achievement granted!\n[accent]", ": [white]", ""], ["Achievement granted!\\n[accent]", ": [white]", ""])), this.name, this.description); @@ -114,7 +121,7 @@ var Achievement = /** @class */ (function () { var _this = this; players_1.FishPlayer.forEachPlayer(function (p) { if (!_this.has(p) && (!team || p.team() == team)) { - if (_this.notify != "none") + if (_this.notify != "nobody") p.sendMessage(_this.message()); _this.setObtained(p); } @@ -287,10 +294,10 @@ exports.Achievements = { //Joining based welcome: new Achievement("_", "Welcome", "Join the server.", { checkPlayerJoin: function () { return true; }, - notify: "none" + notify: "nobody" }), migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { - hidden: true + disabled: true }), //TODO frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", ["Join the server 100 times.", "Note: Do not reconnect frequently, that will not work. This achievement requires that you have been playing for 1 month."], { checkPlayerJoin: function (p) { return p.info().timesJoined >= 100 && (Date.now() - p.globalFirstJoined > funcs_1.Duration.months(1)); } @@ -374,7 +381,7 @@ exports.Achievements = { //messages based messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 1; }, - notify: "none" + notify: "nobody" }), messages_2: new Achievement(["red", Iconc.chat], "Chat 2", ["Send 100 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: function (p) { return p.globalStats.chatMessagesSent >= 100; } @@ -392,7 +399,7 @@ exports.Achievements = { //blocks built based builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced >= 1; }, - notify: "none" + notify: "nobody" }), builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: function (p) { return p.globalStats.blocksPlaced > 200; } @@ -413,7 +420,7 @@ exports.Achievements = { }), dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { modes: ["not", "sandbox"], - hidden: true + disabled: true }), //TODO worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { checkPlayerFrequent: function (player) { @@ -428,11 +435,11 @@ exports.Achievements = { }), head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { modes: ["only", "pvp"], - hidden: true + disabled: true }), //TODO one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { modes: ["only", "pvp"], - hidden: true + disabled: true }), //TODO //sandbox underpowered: new Achievement(["red", Blocks.powerSource.emoji()], "Underpowered", "Overload a power source.", { @@ -451,13 +458,13 @@ exports.Achievements = { }), //easter eggs memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { - notify: "none" + notify: "nobody" }), run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { notify: "everyone" }), script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { - notify: "none" + notify: "nobody" }), hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { hidden: true @@ -508,7 +515,7 @@ exports.Achievements = { }), //other players based alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { - notify: "none" + notify: "nobody" }), join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { checkPlayerJoin: function () { return Groups.player.size() > 20; }, @@ -547,18 +554,18 @@ exports.Achievements = { }), pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { modes: ["not", "sandbox"], - hidden: true + disabled: true }), //TODO core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { modes: ["not", "sandbox"], - hidden: true + disabled: true }), //TODO enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { modes: ["not", "sandbox"], - hidden: true + disabled: true }), //TODO verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { - checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "none" + checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "nobody" }), afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { modes: ["not", "sandbox"], diff --git a/src/achievements.ts b/src/achievements.ts index c75b8a50..a7e01399 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -17,7 +17,7 @@ const mixtechItems = Items.serpuloItems.copy(); Items.erekirItems.each(i => mixtechItems.add(i)); export class Achievement { - private nid: number; + nid: number; sid!: string; icon: string; @@ -32,10 +32,11 @@ export class Achievement { checkFrequent?: (team: Team) => boolean; checkGameover?: (winTeam:Team) => boolean; - notify: "none" | "player" | "everyone" = "player"; + notify: "nobody" | "player" | "everyone" = "player"; hidden = false; disabled = false; allowedModes: GamemodeName[]; + modesText: string; static all: Achievement[] = []; /** Checked every second. */ @@ -71,16 +72,24 @@ export class Achievement { Object.assign(this, options); if(options.modes){ const [type, ...modes] = options.modes; - if(type == "only") this.allowedModes = modes; - else this.allowedModes = GamemodeNames.filter(m => !modes.includes(m)); + if(type == "only"){ + this.allowedModes = modes; + this.modesText = modes.join(", "); + } else { + this.allowedModes = GamemodeNames.filter(m => !modes.includes(m)); + this.modesText = `all except ${modes.join(", ")}`; + } } else { this.allowedModes = GamemodeNames; + this.modesText = `all`; + } + if(!this.disabled){ + Achievement.all.push(this); + if(this.checkPlayerFrequent || this.checkFrequent) Achievement.checkFrequent.push(this); + if(this.checkPlayerInfrequent || this.checkInfrequent) Achievement.checkInfrequent.push(this); + if(this.checkPlayerJoin) Achievement.checkJoin.push(this); + if(this.checkPlayerGameover || this.checkGameover) Achievement.checkGameover.push(this); } - Achievement.all.push(this); - if(this.checkPlayerFrequent || this.checkFrequent) Achievement.checkFrequent.push(this); - if(this.checkPlayerInfrequent || this.checkInfrequent) Achievement.checkInfrequent.push(this); - if(this.checkPlayerJoin) Achievement.checkJoin.push(this); - if(this.checkPlayerGameover || this.checkGameover) Achievement.checkGameover.push(this); } message():string { @@ -96,7 +105,7 @@ export class Achievement { public grantToAllOnline(team?: Team){ FishPlayer.forEachPlayer(p => { if(!this.has(p) && (!team || p.team() == team)){ - if(this.notify != "none") p.sendMessage(this.message()); + if(this.notify != "nobody") p.sendMessage(this.message()); this.setObtained(p); } }); @@ -190,10 +199,10 @@ export const Achievements = { //Joining based welcome: new Achievement("_", "Welcome", "Join the server.", { checkPlayerJoin: () => true, - notify: "none" + notify: "nobody" }), migratory_fish: new Achievement(Iconc.exit, "Migratory Fish", "Join all of our servers.", { - hidden: true + disabled: true }), //TODO frequent_visitor: new Achievement(Iconc.planeOutline, "Frequent Visitor", ["Join the server 100 times.", "Note: Do not reconnect frequently, that will not work. This achievement requires that you have been playing for 1 month."], { checkPlayerJoin: p => p.info().timesJoined >= 100 && (Date.now() - p.globalFirstJoined > Duration.months(1)) @@ -277,7 +286,7 @@ export const Achievements = { //messages based messages_1: new Achievement(["white", Iconc.chat], "Hello", "Send your first chat message.", { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 1, - notify: "none" + notify: "nobody" }), messages_2: new Achievement(["red", Iconc.chat], "Chat 2", ["Send 100 chat messages.", "Warning: you will be kicked if you spam the chat."], { checkPlayerInfrequent: p => p.globalStats.chatMessagesSent >= 100 @@ -296,7 +305,7 @@ export const Achievements = { //blocks built based builds_1: new Achievement(["white", Iconc.fileText], "The Factory Must Prepare", "Construct 1 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced >= 1, - notify: "none" + notify: "nobody" }), builds_2: new Achievement(["red", Iconc.fileText], "The Factory Must Begin", "Construct 200 buildings.", { checkPlayerInfrequent: p => p.globalStats.blocksPlaced > 200 @@ -316,7 +325,7 @@ export const Achievements = { }), dibs: new Achievement(["green", Blocks.tetrativeReconstructor.emoji()], "Dibs", "Be the first player to control the first T5 unit made by a reconstructor that you placed.", { modes: ["not", "sandbox"], - hidden: true + disabled: true }), //TODO worm: new Achievement(UnitTypes.latum.emoji(), "Worm", "Control a Latum.", { checkPlayerFrequent(player) { @@ -331,11 +340,11 @@ export const Achievements = { }), head_start: new Achievement(Iconc.commandAttack, "Head Start", ["Win a match of PVP where your opponents have a 5 minute head start.", "Your team must wait for the first 5 minutes without building or descontructing any buildings."], { modes: ["only", "pvp"], - hidden: true + disabled: true }), //TODO one_v_two: new Achievement(["red", Iconc.modePvp], "1v2", "Defeat two (or more) opponents in PVP without help from other players.", { modes: ["only", "pvp"], - hidden: true + disabled: true }), //TODO //sandbox @@ -355,13 +364,13 @@ export const Achievements = { //easter eggs memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { - notify: "none" + notify: "nobody" }), run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { notify: "everyone" }), script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { - notify: "none" + notify: "nobody" }), hacker: new Achievement(["lightgray", Iconc.host], "Hacker", "Find a bug in the server and report it responsibly.", { hidden: true @@ -405,7 +414,7 @@ export const Achievements = { //other players based alone: new Achievement(["red", Iconc.players], "Alone", "Be the only player online for more than two minutes", { - notify: "none" + notify: "nobody" }), join_playercount_20: new Achievement(["lime", Iconc.players], "Is there enough room?", "Join a server with 20 players online", { checkPlayerJoin: () => Groups.player.size() > 20, @@ -445,18 +454,18 @@ export const Achievements = { }), pacifist_crawler: new Achievement(UnitTypes.crawler.emoji(), "Pacifist Crawler", "Control a crawler for 15 minutes without exploding.", { modes: ["not", "sandbox"], - hidden: true + disabled: true }), //TODO core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { modes: ["not", "sandbox"], - hidden: true + disabled: true }), //TODO enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { modes: ["not", "sandbox"], - hidden: true + disabled: true }), //TODO verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { - checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "none" + checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "nobody" }), afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { modes: ["not", "sandbox"], From 17e872d6640e707a910240f011f2aeb557e0402d Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:32:36 +0530 Subject: [PATCH 16/33] /achievement command --- build/scripts/commands/general.js | 36 ++++++++++++++++++++++++++++++- src/commands/general.ts | 35 ++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index e1227eb6..66e07a7b 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -81,6 +81,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.commands = void 0; +var achievements_1 = require("/achievements"); var api = require("/api"); var config_1 = require("/config"); var commands_1 = require("/frameworks/commands"); @@ -1184,5 +1185,38 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { unit.add(); outputSuccess(f(templateObject_19 || (templateObject_19 = __makeTemplateObject(["Spawned a ", " that is partly a ", "."], ["Spawned a ", " that is partly a ", "."])), args.type, args.base)); } + }, achievement: { + args: ["name:string?", "verbose:boolean?"], + description: "Displays information on a specific achievement.", + perm: commands_1.Perm.none, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var matching, achievement, _c; + var _d = _b.args, _e = _d.name, name = _e === void 0 ? "" : _e, _f = _d.verbose, verbose = _f === void 0 ? false : _f, sender = _b.sender, f = _b.f, output = _b.output; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + name = Strings.stripColors(name.toLowerCase()); + matching = achievements_1.Achievement.all.filter(function (a) { return Strings.stripColors(a.name).toLowerCase().includes(name); }); + if (matching.length == 0) + (0, commands_1.fail)(f(templateObject_20 || (templateObject_20 = __makeTemplateObject(["No achievements found with name ", ". To view all achievements, run [accent]/achievements[]."], ["No achievements found with name ", ". To view all achievements, run [accent]/achievements[]."])), name)); + if (!(matching.length > 2)) return [3 /*break*/, 2]; + return [4 /*yield*/, menus_1.Menu.pagedList(sender, "Achievement", "Select an achievement to view", matching, { + onCancel: "reject" + })]; + case 1: + _c = _g.sent(); + return [3 /*break*/, 3]; + case 2: + _c = matching[0]; + _g.label = 3; + case 3: + achievement = _c; + output(config_1.FColor.achievement(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Achievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes:", "\nUnlocked: ", "\n", "", "", ""], ["\\\nAchievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes:", "\nUnlocked: ", "\n", "\\\n", "\\\n", "\\\n"])), achievement.icon, achievement.name, achievement.description + (achievement.extendedDescription ? ("\n" + "[gray]".concat(achievement.extendedDescription)) : ""), achievement.modesText, f.boolGood(achievement.has(sender)), verbose ? "[gray]ID: (".concat(achievement.nid, ")").concat(achievement.sid, "\n") : "", verbose ? "[gray]Notifies: ".concat(achievement.notify, "\n") : "", achievement.hidden ? "This achievement is secret." : "")); + return [2 /*return*/]; + } + }); + }); + } } })); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21; diff --git a/src/commands/general.ts b/src/commands/general.ts index 002f49ec..d4cb12b4 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -3,8 +3,9 @@ Copyright © BalaM314, 2026. All Rights Reserved. This file contains most in-game chat commands that can be run by untrusted players. */ +import { Achievement } from "/achievements"; import * as api from "/api"; -import { FishServer, Gamemode, rules, text } from "/config"; +import { FColor, FishServer, Gamemode, rules, text } from "/config"; import { command, commandList, fail, formatArg, Perm, Req } from "/frameworks/commands"; import type { FishCommandData } from "/frameworks/commands/types"; import { Menu } from "/frameworks/menus"; @@ -1134,5 +1135,35 @@ Win rate: ${stats.gamesWon / stats.gamesFinished}` unit.add(); outputSuccess(f`Spawned a ${args.type} that is partly a ${args.base}.`); } - } + }, + + achievement: { + args: ["name:string?", "verbose:boolean?"], + description: "Displays information on a specific achievement.", + perm: Perm.none, + async handler({args: {name = "", verbose = false}, sender, f, output}){ + name = Strings.stripColors(name.toLowerCase()); + + const matching = Achievement.all.filter(a => Strings.stripColors(a.name).toLowerCase().includes(name)); + if(matching.length == 0) + fail(f`No achievements found with name ${name}. To view all achievements, run [accent]/achievements[].`); + const achievement = matching.length > 2 ? + await Menu.pagedList(sender, "Achievement", "Select an achievement to view", matching, { + onCancel: "reject" + }) + : matching[0]; + + output(FColor.achievement`\ +Achievement ${achievement.icon} ${achievement.name} +[white]--------------[] +${achievement.description + (achievement.extendedDescription ? ("\n" + `[gray]${achievement.extendedDescription}`) : "")} +Allowed modes:${achievement.modesText} +Unlocked: ${f.boolGood(achievement.has(sender))} +${verbose ? `[gray]ID: (${achievement.nid})${achievement.sid}\n` : ""}\ +${verbose ? `[gray]Notifies: ${achievement.notify}\n` : ""}\ +${achievement.hidden ? "This achievement is secret." : ""}\ +`); + //TODO "x% of players have this achievement" tracking, requires backend aggregation endpoint + } + }, }); From 7209fb50751ca52aeeac1f0df647b55e6f977090 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:36:41 +0530 Subject: [PATCH 17/33] save a few bytes when serializing achievements --- build/scripts/players.js | 4 ++-- src/players.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/scripts/players.js b/build/scripts/players.js index 1f301de9..fa779fb2 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -472,7 +472,7 @@ var FishPlayer = /** @class */ (function () { if (data.flags != undefined) this.flags = new Set(data.flags.map(ranks_1.RoleFlag.getByName).filter(Boolean)); if (data.achievements != undefined) - this.achievements = JsonIO.read(Bits, data.achievements); + this.achievements = JsonIO.read(Bits, "{bits:".concat(data.achievements, "}")); }; FishPlayer.prototype.getData = function () { var _a = this, uuid = _a.uuid, name = _a.name, muted = _a.muted, unmarkTime = _a.unmarkTime, rank = _a.rank, flags = _a.flags, highlight = _a.highlight, rainbow = _a.rainbow, history = _a.history, usid = _a.usid, chatStrictness = _a.chatStrictness, lastJoined = _a.lastJoined, firstJoined = _a.firstJoined, stats = _a.stats, showRankPrefix = _a.showRankPrefix; @@ -492,7 +492,7 @@ var FishPlayer = /** @class */ (function () { showRankPrefix: showRankPrefix, rank: rank.name, flags: __spreadArray([], __read(flags.values()), false).map(function (f) { return f.name; }), - achievements: JsonIO.write(this.achievements) + achievements: JsonIO.write(Reflect.get(this.achievements, "bits")) }; }; /** Warning: the "update" callback is run twice. */ diff --git a/src/players.ts b/src/players.ts index fc6ed588..0c66555b 100644 --- a/src/players.ts +++ b/src/players.ts @@ -387,7 +387,7 @@ export class FishPlayer { if(data.showRankPrefix != undefined) this.showRankPrefix = data.showRankPrefix; if(data.rank != undefined) this.rank = Rank.getByName(data.rank) ?? Rank.player; if(data.flags != undefined) this.flags = new Set(data.flags.map(RoleFlag.getByName).filter(Boolean)); - if(data.achievements != undefined) this.achievements = JsonIO.read(Bits, data.achievements); + if(data.achievements != undefined) this.achievements = JsonIO.read(Bits, `{bits:${data.achievements}}`); } getData():UploadedFishPlayerData { const { uuid, name, muted, unmarkTime, rank, flags, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix } = this; @@ -395,7 +395,7 @@ export class FishPlayer { uuid, name, muted, unmarkTime, highlight, rainbow, history, usid, chatStrictness, lastJoined, firstJoined, stats, showRankPrefix, rank: rank.name, flags: [...flags.values()].map(f => f.name), - achievements: JsonIO.write(this.achievements) + achievements: JsonIO.write(Reflect.get(this.achievements, "bits")) }; } /** Warning: the "update" callback is run twice. */ From c9047f7b5cecae2a6704f04e6637aa23630c0b96 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:58:35 +0530 Subject: [PATCH 18/33] Periodically upload FishPlayer data --- build/scripts/achievements.js | 6 ++++-- build/scripts/api.js | 2 -- build/scripts/commands/console.js | 4 ++-- build/scripts/commands/staff.js | 1 + build/scripts/index.js | 1 + build/scripts/players.js | 7 ++++++- build/scripts/timers.js | 1 + src/achievements.ts | 5 +++-- src/api.ts | 2 +- src/commands/console.ts | 4 ++-- src/commands/staff.ts | 1 + src/index.ts | 1 + src/players.ts | 7 ++++++- src/timers.ts | 1 + 14 files changed, 30 insertions(+), 13 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index d18218c2..b850ef5d 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -132,10 +132,12 @@ var Achievement = /** @class */ (function () { Call.sendMessage(this.messageToEveryone(player)); else if (this.notify == "player") player.sendMessage(this.message()); - this.setObtained(player); + if (!this.has(player)) + this.setObtained(player); }; Achievement.prototype.setObtained = function (player) { - return player.achievements.set(this.nid); + //void player.updateSynced(fishP => fishP.achievements.set(this.nid)); + player.achievements.set(this.nid); }; Achievement.prototype.has = function (player) { return player.achievements.get(this.nid); diff --git a/build/scripts/api.js b/build/scripts/api.js index e223487c..8503b71f 100644 --- a/build/scripts/api.js +++ b/build/scripts/api.js @@ -179,8 +179,6 @@ function getFishPlayerData(uuid) { } /** Pushes fish player data to the backend. */ function setFishPlayerData(data, repeats, ignoreActivelySyncedFields) { - if (repeats === void 0) { repeats = 1; } - if (ignoreActivelySyncedFields === void 0) { ignoreActivelySyncedFields = false; } var _a = promise_1.Promise.withResolvers(), promise = _a.promise, resolve = _a.resolve, reject = _a.reject; if (config_1.Mode.noBackend) { resolve(); diff --git a/build/scripts/commands/console.js b/build/scripts/commands/console.js index 20f245d0..d12f4a7f 100644 --- a/build/scripts/commands/console.js +++ b/build/scripts/commands/console.js @@ -592,7 +592,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ (0, commands_1.fail)("Please use the setusid command instead."); var oldusid = player.usid; player.usid = null; - api.setFishPlayerData(player.getData()).then(function () { + api.setFishPlayerData(player.getData(), 1, true).then(function () { outputSuccess("Removed the usid of player ".concat(player.name, "/").concat(player.uuid, " (was ").concat(oldusid, ")")); }).catch(function (err) { Log.err(err); @@ -611,7 +611,7 @@ exports.commands = (0, commands_1.consoleCommandList)({ var player = (_b = players_1.FishPlayer.lastAuthKicked) !== null && _b !== void 0 ? _b : (0, commands_1.fail)("No authorization failures have occurred since the last restart."); var oldusid = player.usid; player.usid = args.usid; - api.setFishPlayerData(player.getData()).then(function () { + api.setFishPlayerData(player.getData(), 1, true).then(function () { outputSuccess("Set the usid of player ".concat(player.name, "/").concat(player.uuid, " to ").concat(args.usid, " (was ").concat(oldusid, ")")); }).catch(function (err) { Log.err(err); diff --git a/build/scripts/commands/staff.js b/build/scripts/commands/staff.js index a850104a..9592e7f9 100644 --- a/build/scripts/commands/staff.js +++ b/build/scripts/commands/staff.js @@ -566,6 +566,7 @@ exports.commands = (0, commands_1.commandList)({ handler: function (_a) { var outputSuccess = _a.outputSuccess; players_1.FishPlayer.saveAll(); + players_1.FishPlayer.uploadAll(); globals_1.FishEvents.fire("saveData", []); var file = Vars.saveDirectory.child("1.".concat(Vars.saveExtension)); SaveIO.save(file); diff --git a/build/scripts/index.js b/build/scripts/index.js index 20f336c7..8f9fcdd2 100644 --- a/build/scripts/index.js +++ b/build/scripts/index.js @@ -172,6 +172,7 @@ Events.on(EventType.ServerLoadEvent, function (e) { globals_1.FishEvents.fire("dataLoaded", []); Core.app.addListener({ dispose: function () { + players_1.FishPlayer.uploadAll(); globals_1.FishEvents.fire("saveData", []); players_1.FishPlayer.saveAll(false); Log.info("Saved on exit."); diff --git a/build/scripts/players.js b/build/scripts/players.js index fa779fb2..77739a2a 100644 --- a/build/scripts/players.js +++ b/build/scripts/players.js @@ -514,7 +514,7 @@ var FishPlayer = /** @class */ (function () { //but it's unlikely to happen //could be fixed by transmitting the update operation to the server as a mongo update command afterFetch === null || afterFetch === void 0 ? void 0 : afterFetch(this); - return [4 /*yield*/, api.setFishPlayerData(this.getData())]; + return [4 /*yield*/, api.setFishPlayerData(this.getData(), 1, false)]; case 2: _a.sent(); return [2 /*return*/]; @@ -1368,6 +1368,11 @@ var FishPlayer = /** @class */ (function () { FishPlayer.prototype.shouldCache = function () { return this.ranksAtLeast("mod"); }; + FishPlayer.uploadAll = function () { + FishPlayer.forEachPlayer(function (fishP) { + return void api.setFishPlayerData(fishP.getData(), 1, true); + }); + }; /** Does not include stats */ FishPlayer.prototype.hasData = function () { return (this.rank != ranks_1.Rank.player) || this.muted || (this.flags.size > 0) || this.chatStrictness != "chat"; diff --git a/build/scripts/timers.js b/build/scripts/timers.js index a7d2b00a..f7c28265 100644 --- a/build/scripts/timers.js +++ b/build/scripts/timers.js @@ -33,6 +33,7 @@ function initializeTimers() { Core.app.post(function () { SaveIO.save(file); players_1.FishPlayer.saveAll(); + players_1.FishPlayer.uploadAll(); Call.sendMessage('[#4fff8f9f]Game saved.'); globals_1.FishEvents.fire("saveData", []); }); diff --git a/src/achievements.ts b/src/achievements.ts index a7e01399..ea2596a0 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -113,11 +113,12 @@ export class Achievement { public grantTo(player:FishPlayer){ if(this.notify == "everyone") Call.sendMessage(this.messageToEveryone(player)); else if(this.notify == "player") player.sendMessage(this.message()); - this.setObtained(player); + if(!this.has(player)) this.setObtained(player); } private setObtained(player:FishPlayer){ - return player.achievements.set(this.nid); + //void player.updateSynced(fishP => fishP.achievements.set(this.nid)); + player.achievements.set(this.nid); } public has(player:FishPlayer){ return player.achievements.get(this.nid); diff --git a/src/api.ts b/src/api.ts index 4632eae2..96ddb5c4 100644 --- a/src/api.ts +++ b/src/api.ts @@ -169,7 +169,7 @@ export function getFishPlayerData(uuid:string){ } /** Pushes fish player data to the backend. */ -export function setFishPlayerData(data: UploadedFishPlayerData, repeats = 1, ignoreActivelySyncedFields = false) { +export function setFishPlayerData(data: UploadedFishPlayerData, repeats:number, ignoreActivelySyncedFields:boolean) { const { promise, resolve, reject } = Promise.withResolvers(); if(Mode.noBackend){ resolve(); diff --git a/src/commands/console.ts b/src/commands/console.ts index d132aba3..b5521c0a 100644 --- a/src/commands/console.ts +++ b/src/commands/console.ts @@ -381,7 +381,7 @@ export const commands = consoleCommandList({ if(player.ranksAtLeast("admin")) fail(`Please use the setusid command instead.`); const oldusid = player.usid; player.usid = null; - api.setFishPlayerData(player.getData()).then(() => { + api.setFishPlayerData(player.getData(), 1, true).then(() => { outputSuccess(`Removed the usid of player ${player.name}/${player.uuid} (was ${oldusid})`); }).catch(err => { Log.err(err); @@ -397,7 +397,7 @@ export const commands = consoleCommandList({ const player = FishPlayer.lastAuthKicked ?? fail(`No authorization failures have occurred since the last restart.`); const oldusid = player.usid; player.usid = args.usid; - api.setFishPlayerData(player.getData()).then(() => { + api.setFishPlayerData(player.getData(), 1, true).then(() => { outputSuccess(`Set the usid of player ${player.name}/${player.uuid} to ${args.usid} (was ${oldusid})`); }).catch(err => { Log.err(err); diff --git a/src/commands/staff.ts b/src/commands/staff.ts index a93a97e0..52a83fbf 100644 --- a/src/commands/staff.ts +++ b/src/commands/staff.ts @@ -344,6 +344,7 @@ export const commands = commandList({ perm: Perm.mod, handler({outputSuccess}){ FishPlayer.saveAll(); + FishPlayer.uploadAll(); FishEvents.fire("saveData", []); const file = Vars.saveDirectory.child(`1.${Vars.saveExtension}`); SaveIO.save(file); diff --git a/src/index.ts b/src/index.ts index 0394773b..fe3d22cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -169,6 +169,7 @@ Events.on(EventType.ServerLoadEvent, (e) => { Core.app.addListener({ dispose(){ + FishPlayer.uploadAll(); FishEvents.fire("saveData", []); FishPlayer.saveAll(false); Log.info("Saved on exit."); diff --git a/src/players.ts b/src/players.ts index 0c66555b..7c903ef7 100644 --- a/src/players.ts +++ b/src/players.ts @@ -413,7 +413,7 @@ export class FishPlayer { //but it's unlikely to happen //could be fixed by transmitting the update operation to the server as a mongo update command afterFetch?.(this); - await api.setFishPlayerData(this.getData()); + await api.setFishPlayerData(this.getData(), 1, false); } //#endregion @@ -1177,6 +1177,11 @@ We apologize for the inconvenience.` shouldCache(){ return this.ranksAtLeast("mod"); } + static uploadAll(){ + FishPlayer.forEachPlayer(fishP => + void api.setFishPlayerData(fishP.getData(), 1, true) + ); + } /** Does not include stats */ hasData(){ return (this.rank != Rank.player) || this.muted || (this.flags.size > 0) || this.chatStrictness != "chat"; diff --git a/src/timers.ts b/src/timers.ts index 919a7d08..d9b193ea 100644 --- a/src/timers.ts +++ b/src/timers.ts @@ -21,6 +21,7 @@ export function initializeTimers(){ Core.app.post(() => { SaveIO.save(file); FishPlayer.saveAll(); + FishPlayer.uploadAll(); Call.sendMessage('[#4fff8f9f]Game saved.'); FishEvents.fire("saveData", []); }); From 9ed600d399331d81e490edeb60478ec85e344a12 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 18:27:02 +0530 Subject: [PATCH 19/33] typedefs: add ArcReflect as alias for Reflect to fix name coliision with ES6+ Reflect --- build/scripts/main.js | 1 + src/main.js | 1 + src/mindustry.d.ts | 11 ----------- src/mindustryTypes.ts | 12 ++++++++++-- 4 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 src/mindustry.d.ts diff --git a/build/scripts/main.js b/build/scripts/main.js index 3f537048..555c48a1 100644 --- a/build/scripts/main.js +++ b/build/scripts/main.js @@ -66,5 +66,6 @@ if(12.34.toFixed(1) !== '12.3'){ }; } +this.ArcReflect = Reflect; this.Promise = require('/promise').Promise; require("index"); diff --git a/src/main.js b/src/main.js index 3f537048..555c48a1 100644 --- a/src/main.js +++ b/src/main.js @@ -66,5 +66,6 @@ if(12.34.toFixed(1) !== '12.3'){ }; } +this.ArcReflect = Reflect; this.Promise = require('/promise').Promise; require("index"); diff --git a/src/mindustry.d.ts b/src/mindustry.d.ts deleted file mode 100644 index 1e3e81d0..00000000 --- a/src/mindustry.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* -Copyright © BalaM314, 2026. All Rights Reserved. -This file contains one single declaration from mindustryTypes.ts that cannot go in there. -*/ - - -//Use a .d.ts to suppress the error from this override -declare const Reflect: { - get(thing:any, key:string):any; - set(thing:any, key:string, value:any):void; -}; diff --git a/src/mindustryTypes.ts b/src/mindustryTypes.ts index 2921c074..fc0b50d5 100644 --- a/src/mindustryTypes.ts +++ b/src/mindustryTypes.ts @@ -70,7 +70,7 @@ const Vars: { maps: Maps; state: { rules: Rules; - planet: Planet; + planet: Planet | null; set(state:State):void; gameOver:boolean; wave:number; @@ -92,7 +92,7 @@ const Vars: { world: World; }; class Teams { - active: Seq; + active: Seq; } class BlockIndexer { getFlagged(team: Team, flag: BlockFlag): Seq; @@ -263,6 +263,7 @@ class Team { cores(): Seq; } type TeamData = { + team: Team; units: Seq; buildings: Seq; cores: Seq; @@ -889,4 +890,11 @@ class Boolf { function boolf(func: (value: T) => boolean): Boolf; const Iconc: Record<"rotate" | "modeSurvival" | "power" | "left" | "redditAlien" | "edit" | "downOpen" | "pencil" | "file" | "lockOpen" | "right" | "infoCircle" | "pick" | "settings" | "spray1" | "terrain" | "exit" | "wrench" | "lock" | "discord" | "eye" | "none" | "play" | "diagonal" | "eraser" | "trash" | "liquid" | "fileImage" | "defense" | "layers" | "grid" | "admin" | "steam" | "star" | "chartBar" | "chat" | "android" | "image" | "map" | "logic" | "menu" | "commandRally" | "editor" | "folder" | "units" | "commandAttack" | "copy" | "filter" | "cancel" | "terminal" | "upload" | "eyeOff" | "save" | "planeOutline" | "fill" | "distribution" | "upOpen" | "rightOpen" | "modePvp" | "download" | "list" | "flipX" | "flipY" | "effect" | "paste" | "planet" | "waves" | "up" | "warning" | "tree" | "add" | "down" | "host" | "spray" | "info" | "players" | "resize" | "refresh1" | "production" | "crafting" | "pause" | "googleplay" | "hammer" | "fileText" | "modeAttack" | "move" | "zoom" | "bookOpen" | "refresh" | "ok" | "home" | "githubSquare" | "powerOld" | "github" | "undo" | "box" | "trello" | "book" | "export" | "fileTextFill" | "rightOpenOut" | "turret" | "leftOpen" | "line" | "itchio" | "link" | "filters" | "redo", number>; + +const ArcReflect: { + get(thing:any, key:string):any; + get(clazz:any, thing:any, key:string):any; + set(thing:any, key:string, value:any):void; +}; + } \ No newline at end of file From ffc387cdc5ce4dd2690d5469403da716da089a40 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:05:14 +0530 Subject: [PATCH 20/33] many bugfixes --- build/scripts/achievements.js | 83 +++++++++++++++++++------------ build/scripts/commands/general.js | 2 +- build/scripts/utils.js | 32 ++++++++++++ src/achievements.ts | 40 +++++++++------ src/commands/general.ts | 2 +- src/utils.ts | 20 ++++++++ 6 files changed, 128 insertions(+), 51 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index b850ef5d..f8627140 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -46,6 +46,7 @@ var funcs_1 = require("/funcs"); var globals_1 = require("/globals"); var players_1 = require("/players"); var ranks_1 = require("/ranks"); +var utils_1 = require("/utils"); //scrap doesn't count var serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; var erekirItems = [Items.beryllium, Items.graphite, Items.silicon, Items.tungsten, Items.oxide, Items.surgeAlloy, Items.thorium, Items.carbide, Items.phaseFabric]; @@ -66,7 +67,7 @@ var Achievement = /** @class */ (function () { this.hidden = false; this.disabled = false; if (Array.isArray(icon)) { - this.icon = "[".concat(icon[0], "]") + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); + this.icon = (icon[0].startsWith("[") ? icon[0] : "[".concat(icon[0], "]")) + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); } else if (typeof icon == "number") { this.icon = String.fromCharCode(icon); @@ -127,6 +128,7 @@ var Achievement = /** @class */ (function () { } }); }; + /** Do not call this in a loop on an achievement set to notify everyone. */ Achievement.prototype.grantTo = function (player) { if (this.notify == "everyone") Call.sendMessage(this.messageToEveryone(player)); @@ -157,15 +159,21 @@ Events.on(EventType.PlayerJoin, function (_a) { var e_1, _b; var _c; var player = _a.player; + var _loop_1 = function (ach) { + if (ach.allowedInMode()) { + var fishP_1 = players_1.FishPlayer.get(player); + if (!ach.has(fishP_1) && ((_c = ach.checkPlayerJoin) === null || _c === void 0 ? void 0 : _c.call(ach, fishP_1))) { + if (fishP_1.dataSynced) + ach.grantTo(fishP_1); + else + Timer.schedule(function () { return ach.grantTo(fishP_1); }, 2); //2 seconds should be enough + } + } + }; try { for (var _d = __values(Achievement.checkJoin), _e = _d.next(); !_e.done; _e = _d.next()) { var ach = _e.value; - if (ach.allowedInMode()) { - var fishP = players_1.FishPlayer.get(player); - if (!ach.has(fishP) && ((_c = ach.checkPlayerJoin) === null || _c === void 0 ? void 0 : _c.call(ach, fishP))) { - ach.grantTo(fishP); - } - } + _loop_1(ach); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } @@ -179,7 +187,7 @@ Events.on(EventType.PlayerJoin, function (_a) { globals_1.FishEvents.on("gameOver", function (_, winner) { var e_2, _a; var _b; - var _loop_1 = function (ach) { + var _loop_2 = function (ach) { if (ach.allowedInMode()) { if ((_b = ach.checkGameover) === null || _b === void 0 ? void 0 : _b.call(ach, winner)) ach.grantToAllOnline(); @@ -195,7 +203,7 @@ globals_1.FishEvents.on("gameOver", function (_, winner) { try { for (var _c = __values(Achievement.checkGameover), _d = _c.next(); !_d.done; _d = _c.next()) { var ach = _d.value; - _loop_1(ach); + _loop_2(ach); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } @@ -208,13 +216,14 @@ globals_1.FishEvents.on("gameOver", function (_, winner) { }); Timer.schedule(function () { var e_3, _a; - var _loop_2 = function (ach) { + var _loop_3 = function (ach) { if (ach.allowedInMode()) { if (ach.checkFrequent) { if (config_1.Gamemode.pvp()) { - Vars.state.teams.active.each(function (t) { - if (ach.checkFrequent(t)) - ach.grantToAllOnline(t); + Vars.state.teams.active.each(function (_a) { + var team = _a.team; + if (ach.checkFrequent(team)) + ach.grantToAllOnline(team); }); } else { @@ -234,7 +243,7 @@ Timer.schedule(function () { try { for (var _b = __values(Achievement.checkFrequent), _c = _b.next(); !_c.done; _c = _b.next()) { var ach = _c.value; - _loop_2(ach); + _loop_3(ach); } } catch (e_3_1) { e_3 = { error: e_3_1 }; } @@ -247,13 +256,14 @@ Timer.schedule(function () { }, 1, 1); Timer.schedule(function () { var e_4, _a; - var _loop_3 = function (ach) { + var _loop_4 = function (ach) { if (ach.allowedInMode()) { if (ach.checkInfrequent) { if (config_1.Gamemode.pvp()) { - Vars.state.teams.active.each(function (t) { - if (ach.checkInfrequent(t)) - ach.grantToAllOnline(t); + Vars.state.teams.active.each(function (_a) { + var team = _a.team; + if (ach.checkInfrequent(team)) + ach.grantToAllOnline(team); }); } else { @@ -273,7 +283,7 @@ Timer.schedule(function () { try { for (var _b = __values(Achievement.checkInfrequent), _c = _b.next(); !_c.done; _c = _b.next()) { var ach = _c.value; - _loop_3(ach); + _loop_4(ach); } } catch (e_4_1) { e_4 = { error: e_4_1 }; } @@ -462,7 +472,7 @@ exports.Achievements = { memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { notify: "nobody" }), - run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { + run_js_without_perms: new Achievement(["yellow", Iconc.warning], "XKCD 838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { notify: "everyone" }), script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { @@ -476,6 +486,8 @@ exports.Achievements = { modes: ["not", "sandbox"], checkPlayerFrequent: function (player) { var _a; + if (!Vars.state.planet) + return false; return ((_a = player.team().items()) === null || _a === void 0 ? void 0 : _a.has(usefulItems10k[Vars.state.planet.name])) || false; }, }), @@ -489,6 +501,8 @@ exports.Achievements = { modes: ["not", "sandbox"], checkFrequent: function (team) { var _a; + if (!Vars.state.planet) + return false; var items; switch (Vars.state.planet.name) { case "serpulo": @@ -580,7 +594,7 @@ exports.Achievements = { var unit = p.unit(); if (!unit) return false; - var statuses = Reflect.get(unit, "statuses"); + var statuses = (0, utils_1.getStatuses)(unit); return statuses.size >= 5; }, modes: ["not", "sandbox"] @@ -635,14 +649,16 @@ globals_1.FishEvents.on("commandUnauthorized", function (_, player, name) { Events.on(EventType.UnitDrownEvent, function (_a) { var _b; var unit = _a.unit; - if (unit.type == UnitTypes.mace && ((_b = unit.tileOn()) === null || _b === void 0 ? void 0 : _b.floor()) == Blocks.cryofluid) - exports.Achievements.drown_mace_in_cryo.grantToAllOnline(); - else if (unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) - exports.Achievements.drown_big_tank.grantToAllOnline(); + if (!config_1.Gamemode.sandbox()) { + if (unit.type == UnitTypes.mace && ((_b = unit.tileOn()) === null || _b === void 0 ? void 0 : _b.floor()) == Blocks.cryofluid) + exports.Achievements.drown_mace_in_cryo.grantToAllOnline(); + else if (unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) + exports.Achievements.drown_big_tank.grantToAllOnline(); + } }); Events.on(EventType.UnitBulletDestroyEvent, function (_a) { var unit = _a.unit, bullet = _a.bullet; - if (unit.type == UnitTypes.dagger && bullet.owner.block == Blocks.foreshadow) { + if (!config_1.Gamemode.sandbox() && unit.type == UnitTypes.dagger && bullet.owner.block == Blocks.foreshadow) { var build = bullet.owner; if (build.liquids.current() == Liquids.cryofluid && build.timeScale() >= 3) exports.Achievements.foreshadow_overkill.grantToAllOnline(build.team); @@ -652,12 +668,13 @@ var siliconReached = Team.all.map(function (_) { return false; }); Events.on(EventType.GameOverEvent, function () { return siliconReached = Team.all.map(function (_) { return false; }); }); var isAlone = 0; Timer.schedule(function () { - if (!Vars.state.gameOver) { - Vars.state.teams.active.each(function (t) { - if (t.items().has(Items.silicon, 2000)) - siliconReached[t.id] = true; - else if (t.items().get(Items.silicon) == 0) - exports.Achievements.siligone.grantToAllOnline(t); + if (!Vars.state.gameOver && !config_1.Gamemode.sandbox()) { + Vars.state.teams.active.each(function (_a) { + var team = _a.team; + if (team.items().has(Items.silicon, 2000)) + siliconReached[team.id] = true; + else if (siliconReached[team.id] && team.items().get(Items.silicon) == 0) + exports.Achievements.siligone.grantToAllOnline(team); }); } if (Groups.player.size() == 1) { @@ -669,7 +686,7 @@ Timer.schedule(function () { else isAlone = 0; }, 2, 2); -globals_1.FishEvents.on("scriptKiddie", function (_, p) { return exports.Achievements.script_kiddie.grantTo(p); }); +globals_1.FishEvents.on("scriptKiddie", function (_, p) { return Timer.schedule(function () { return exports.Achievements.script_kiddie.grantTo(p); }, 2); }); globals_1.FishEvents.on("memoryCorruption", function () { return exports.Achievements.memory_corruption.grantToAllOnline(); }); globals_1.FishEvents.on("serverSays", function () { return exports.Achievements.server_speak.grantToAllOnline(); }); var templateObject_1, templateObject_2; diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index 66e07a7b..3cb0eef0 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1212,7 +1212,7 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { _g.label = 3; case 3: achievement = _c; - output(config_1.FColor.achievement(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Achievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes:", "\nUnlocked: ", "\n", "", "", ""], ["\\\nAchievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes:", "\nUnlocked: ", "\n", "\\\n", "\\\n", "\\\n"])), achievement.icon, achievement.name, achievement.description + (achievement.extendedDescription ? ("\n" + "[gray]".concat(achievement.extendedDescription)) : ""), achievement.modesText, f.boolGood(achievement.has(sender)), verbose ? "[gray]ID: (".concat(achievement.nid, ")").concat(achievement.sid, "\n") : "", verbose ? "[gray]Notifies: ".concat(achievement.notify, "\n") : "", achievement.hidden ? "This achievement is secret." : "")); + output(config_1.FColor.achievement(templateObject_21 || (templateObject_21 = __makeTemplateObject(["Achievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "", "", ""], ["\\\nAchievement ", " ", "\n[white]--------------[]\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n", "\\\n", "\\\n"])), achievement.icon, achievement.name, achievement.description + (achievement.extendedDescription ? ("\n" + "[gray]".concat(achievement.extendedDescription)) : ""), achievement.modesText, f.boolGood(achievement.has(sender)), verbose ? "[gray]ID: (".concat(achievement.nid, ")").concat(achievement.sid, "\n") : "", verbose ? "[gray]Notifies: ".concat(achievement.notify, "\n") : "", achievement.hidden ? "This achievement is secret." : "")); return [2 /*return*/]; } }); diff --git a/build/scripts/utils.js b/build/scripts/utils.js index 11c06acd..68f0be26 100644 --- a/build/scripts/utils.js +++ b/build/scripts/utils.js @@ -86,6 +86,7 @@ exports.getHash = getHash; exports.match = match; exports.fishCommandsRootDirPath = fishCommandsRootDirPath; exports.applyEffectMode = applyEffectMode; +exports.getStatuses = getStatuses; var api = require("/api"); var config_1 = require("/config"); var commands_1 = require("/frameworks/commands"); @@ -1061,3 +1062,34 @@ function applyEffectMode(mode, unit, ticks) { } } } +var sources = [ + Packages.mindustry.gen.UnitEntity, + Packages.mindustry.gen.MechUnit, + Packages.mindustry.gen.LegsUnit, + Packages.mindustry.gen.CrawlUnit, + Packages.mindustry.gen.UnitWaterMove, + Packages.mindustry.gen.BlockUnitUnit, + Packages.mindustry.gen.ElevationMoveUnit, + Packages.mindustry.gen.BuildingTetherPayloadUnit, + Packages.mindustry.gen.TimedKillUnit, + Packages.mindustry.gen.PayloadUnit, + Packages.mindustry.gen.TankUnit, +]; +function getStatuses(unit) { + var e_8, _a; + try { + for (var sources_1 = __values(sources), sources_1_1 = sources_1.next(); !sources_1_1.done; sources_1_1 = sources_1.next()) { + var clazz = sources_1_1.value; + if (unit instanceof clazz) + return ArcReflect.get(clazz, unit, "statuses"); + } + } + catch (e_8_1) { e_8 = { error: e_8_1 }; } + finally { + try { + if (sources_1_1 && !sources_1_1.done && (_a = sources_1.return)) _a.call(sources_1); + } + finally { if (e_8) throw e_8.error; } + } + return new Seq(); +} diff --git a/src/achievements.ts b/src/achievements.ts index ea2596a0..e73a713a 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -3,6 +3,7 @@ import { Duration } from "/funcs"; import { FishEvents, unitsT5 } from "/globals"; import { FishPlayer } from "/players"; import { Rank } from "/ranks"; +import { getStatuses } from "/utils"; //scrap doesn't count const serpuloItems = [Items.copper, Items.lead, Items.graphite, Items.silicon, Items.metaglass, Items.titanium, Items.plastanium, Items.thorium, Items.surgeAlloy, Items.phaseFabric]; @@ -59,7 +60,7 @@ export class Achievement { }> = {}, ){ if(Array.isArray(icon)){ - this.icon = `[${icon[0]}]` + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); + this.icon = (icon[0].startsWith("[") ? icon[0] : `[${icon[0]}]`) + (typeof icon[1] == "number" ? String.fromCharCode(icon[1]) : icon[1]); } else if(typeof icon == "number"){ this.icon = String.fromCharCode(icon); } else { @@ -110,6 +111,7 @@ export class Achievement { } }); } + /** Do not call this in a loop on an achievement set to notify everyone. */ public grantTo(player:FishPlayer){ if(this.notify == "everyone") Call.sendMessage(this.messageToEveryone(player)); else if(this.notify == "player") player.sendMessage(this.message()); @@ -130,7 +132,8 @@ Events.on(EventType.PlayerJoin, ({player}: {player: mindustryPlayer}) => { if(ach.allowedInMode()){ const fishP = FishPlayer.get(player); if(!ach.has(fishP) && ach.checkPlayerJoin?.(fishP)){ - ach.grantTo(fishP); + if(fishP.dataSynced) ach.grantTo(fishP); + else Timer.schedule(() => ach.grantTo(fishP), 2); //2 seconds should be enough } } } @@ -152,8 +155,8 @@ Timer.schedule(() => { if(ach.allowedInMode()){ if(ach.checkFrequent){ if(Gamemode.pvp()){ - Vars.state.teams.active.each(t => { - if(ach.checkFrequent!(t)) ach.grantToAllOnline(t); + Vars.state.teams.active.each(({team}) => { + if(ach.checkFrequent!(team)) ach.grantToAllOnline(team); }); } else { if(ach.checkFrequent(Vars.state.rules.defaultTeam)) ach.grantToAllOnline(); @@ -171,8 +174,8 @@ Timer.schedule(() => { if(ach.allowedInMode()){ if(ach.checkInfrequent){ if(Gamemode.pvp()){ - Vars.state.teams.active.each(t => { - if(ach.checkInfrequent!(t)) ach.grantToAllOnline(t); + Vars.state.teams.active.each(({team}) => { + if(ach.checkInfrequent!(team)) ach.grantToAllOnline(team); }); } else { if(ach.checkInfrequent(Vars.state.rules.defaultTeam)) ach.grantToAllOnline(); @@ -367,7 +370,7 @@ export const Achievements = { memory_corruption: new Achievement(["red", Iconc.host], "Is the server OK?", "Witness a memory corruption.", { notify: "nobody" }), - run_js_without_perms: new Achievement(["yellow", Iconc.warning], "838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { + run_js_without_perms: new Achievement(["yellow", Iconc.warning], "XKCD 838", ["Receive a warning from the server that an incident will be reported.", "One of the admin commands has a custom error message."], { notify: "everyone" }), script_kiddie: new Achievement(["red", Iconc.warning], "Script Kiddie", ["Pretend to be a hacker. The server will disagree.", "Change your name to something including \"hacker\"."], { @@ -381,6 +384,7 @@ export const Achievements = { items_10k: new Achievement(["green", Iconc.distribution], "Cornucopia", "Obtain 10k of every useful resource.", { modes: ["not", "sandbox"], checkPlayerFrequent(player) { + if(!Vars.state.planet) return false; return player.team().items()?.has(usefulItems10k[Vars.state.planet.name as "serpulo" | "erekir" | "sun"]) || false; }, }), @@ -393,6 +397,7 @@ export const Achievements = { full_core: new Achievement(["green", Blocks.coreAcropolis.emoji()], "Multiblock Incinerator", "Completely fill the core with all obtainable items on a map with core incineration enabled.", { modes: ["not", "sandbox"], checkFrequent(team) { + if(!Vars.state.planet) return false; let items; switch(Vars.state.planet.name as "serpulo" | "erekir" | "sun"){ case "serpulo": items = Items.serpuloItems; break; @@ -478,7 +483,7 @@ export const Achievements = { checkPlayerFrequent: p => { const unit = p.unit(); if(!unit) return false; - const statuses = Reflect.get(unit, "statuses") as Seq<{ effect: StatusEffect }>; + const statuses = getStatuses(unit); return statuses.size >= 5; }, modes: ["not", "sandbox"] @@ -528,12 +533,14 @@ FishEvents.on("commandUnauthorized", (_, player, name) => { Events.on(EventType.UnitDrownEvent, ({unit}:{unit: Unit}) => { - if(unit.type == UnitTypes.mace && unit.tileOn()?.floor() == Blocks.cryofluid) Achievements.drown_mace_in_cryo.grantToAllOnline(); - else if(unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) Achievements.drown_big_tank.grantToAllOnline(); + if(!Gamemode.sandbox()){ + if(unit.type == UnitTypes.mace && unit.tileOn()?.floor() == Blocks.cryofluid) Achievements.drown_mace_in_cryo.grantToAllOnline(); + else if(unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) Achievements.drown_big_tank.grantToAllOnline(); + } }); Events.on(EventType.UnitBulletDestroyEvent, ({unit, bullet}:{unit:Unit; bullet: Bullet}) => { - if(unit.type == UnitTypes.dagger && (bullet.owner as Building).block == Blocks.foreshadow){ + if(!Gamemode.sandbox() && unit.type == UnitTypes.dagger && (bullet.owner as Building).block == Blocks.foreshadow){ const build = bullet.owner as Building; if(build.liquids.current() == Liquids.cryofluid && build.timeScale() >= 3) Achievements.foreshadow_overkill.grantToAllOnline(build.team); } @@ -543,10 +550,11 @@ let siliconReached = Team.all.map(_ => false); Events.on(EventType.GameOverEvent, () => siliconReached = Team.all.map(_ => false)); let isAlone = 0; Timer.schedule(() => { - if(!Vars.state.gameOver){ - Vars.state.teams.active.each(t => { - if(t.items().has(Items.silicon, 2000)) siliconReached[t.id] = true; - else if(t.items().get(Items.silicon) == 0) Achievements.siligone.grantToAllOnline(t); + if(!Vars.state.gameOver && !Gamemode.sandbox()){ + Vars.state.teams.active.each(({team}) => { + if(team.items().has(Items.silicon, 2000)) siliconReached[team.id] = true; + else if(siliconReached[team.id] && team.items().get(Items.silicon) == 0) + Achievements.siligone.grantToAllOnline(team); }); } if(Groups.player.size() == 1){ @@ -556,6 +564,6 @@ Timer.schedule(() => { }, 2, 2); -FishEvents.on("scriptKiddie", (_, p) => Achievements.script_kiddie.grantTo(p)); +FishEvents.on("scriptKiddie", (_, p) => Timer.schedule(() => Achievements.script_kiddie.grantTo(p), 2)); FishEvents.on("memoryCorruption", () => Achievements.memory_corruption.grantToAllOnline()); FishEvents.on("serverSays", () => Achievements.server_speak.grantToAllOnline()); \ No newline at end of file diff --git a/src/commands/general.ts b/src/commands/general.ts index d4cb12b4..dd1e3605 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -1157,7 +1157,7 @@ Win rate: ${stats.gamesWon / stats.gamesFinished}` Achievement ${achievement.icon} ${achievement.name} [white]--------------[] ${achievement.description + (achievement.extendedDescription ? ("\n" + `[gray]${achievement.extendedDescription}`) : "")} -Allowed modes:${achievement.modesText} +Allowed modes: ${achievement.modesText} Unlocked: ${f.boolGood(achievement.has(sender))} ${verbose ? `[gray]ID: (${achievement.nid})${achievement.sid}\n` : ""}\ ${verbose ? `[gray]Notifies: ${achievement.notify}\n` : ""}\ diff --git a/src/utils.ts b/src/utils.ts index 6a322026..4067d56f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -890,3 +890,23 @@ export function applyEffectMode(mode:string, unit:Unit, ticks:number){ } } +const sources = [ + Packages.mindustry.gen.UnitEntity, + Packages.mindustry.gen.MechUnit, + Packages.mindustry.gen.LegsUnit, + Packages.mindustry.gen.CrawlUnit, + Packages.mindustry.gen.UnitWaterMove, + Packages.mindustry.gen.BlockUnitUnit, + Packages.mindustry.gen.ElevationMoveUnit, + Packages.mindustry.gen.BuildingTetherPayloadUnit, + Packages.mindustry.gen.TimedKillUnit, + Packages.mindustry.gen.PayloadUnit, + Packages.mindustry.gen.TankUnit, +]; +export function getStatuses(unit:Unit):Seq<{ effect: StatusEffect }> { + for(const clazz of sources){ + if(unit instanceof clazz) + return ArcReflect.get(clazz, unit, "statuses") as Seq<{ effect: StatusEffect }>; + } + return new Seq(); +} From 33c90fce0d4332b4ac579c465ca43b8a349476f9 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:23:50 +0530 Subject: [PATCH 21/33] /achievementlist command --- build/scripts/commands/general.js | 26 ++++++++++++++++++++++++-- src/commands/general.ts | 22 +++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index 3cb0eef0..6a8097b0 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1202,7 +1202,9 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { (0, commands_1.fail)(f(templateObject_20 || (templateObject_20 = __makeTemplateObject(["No achievements found with name ", ". To view all achievements, run [accent]/achievements[]."], ["No achievements found with name ", ". To view all achievements, run [accent]/achievements[]."])), name)); if (!(matching.length > 2)) return [3 /*break*/, 2]; return [4 /*yield*/, menus_1.Menu.pagedList(sender, "Achievement", "Select an achievement to view", matching, { - onCancel: "reject" + onCancel: "reject", + columns: 2, + optionStringifier: function (a) { return "".concat(a.icon, "[] ").concat(a.name); } })]; case 1: _c = _g.sent(); @@ -1218,5 +1220,25 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { }); }); } + }, achievementlist: { + args: ["target:player?"], + description: "Shows all achievements in a paged menu.", + perm: commands_1.Perm.none, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var sender = _b.sender, _c = _b.args.target, target = _c === void 0 ? sender : _c, f = _b.f; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: return [4 /*yield*/, menus_1.Menu.textPages(sender, achievements_1.Achievement.all.map(function (a) { return [ + "".concat(a.icon, "[] ").concat(a.name), + function () { return config_1.FColor.achievement(templateObject_22 || (templateObject_22 = __makeTemplateObject(["", "\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : ""); } + ]; }))]; + case 1: + _d.sent(); + return [2 /*return*/]; + } + }); + }); + } } })); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22; diff --git a/src/commands/general.ts b/src/commands/general.ts index dd1e3605..913b054e 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -1149,7 +1149,9 @@ Win rate: ${stats.gamesWon / stats.gamesFinished}` fail(f`No achievements found with name ${name}. To view all achievements, run [accent]/achievements[].`); const achievement = matching.length > 2 ? await Menu.pagedList(sender, "Achievement", "Select an achievement to view", matching, { - onCancel: "reject" + onCancel: "reject", + columns: 2, + optionStringifier: a => `${a.icon}[] ${a.name}` }) : matching[0]; @@ -1166,4 +1168,22 @@ ${achievement.hidden ? "This achievement is secret." : ""}\ //TODO "x% of players have this achievement" tracking, requires backend aggregation endpoint } }, + + achievementlist: { + args: ["target:player?"], + description: "Shows all achievements in a paged menu.", + perm: Perm.none, + async handler({sender, args: { target = sender }, f}){ + await Menu.textPages(sender, Achievement.all.map(a => [ + `${a.icon}[] ${a.name}`, + () => FColor.achievement`\ +${a.description + (a.extendedDescription ? ("\n" + `[gray]${a.extendedDescription}`) : "")} +Allowed modes: ${a.modesText} +Unlocked: ${f.boolGood(a.has(target))} +${a.hidden ? "This achievement is secret." : ""}\ +` + ])); + } + }, + }); From 68b605ffbab2e4651707ae5652af1321e676c63d Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:14:50 +0530 Subject: [PATCH 22/33] Update Menus.scroll --- build/scripts/frameworks/menus.js | 19 ++++++++++++------- src/frameworks/menus.ts | 21 +++++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/build/scripts/frameworks/menus.js b/build/scripts/frameworks/menus.js index aafd8244..60f0a486 100644 --- a/build/scripts/frameworks/menus.js +++ b/build/scripts/frameworks/menus.js @@ -290,24 +290,29 @@ exports.Menu = { var width = options[0].length; function showPage(x, y) { var _a, _b; - var opts = __spreadArray(__spreadArray([], __read(options.slice(y, y + rows).map(function (r) { return r.slice(x, x + cols).map(function (d) { return ({ text: d.text, data: [d.data] }); }); })), false), [ + var opts = __spreadArray(__spreadArray([], __read(options.slice(y, y + rows).map(function (r) { return r.concat(Array(width - r.length).fill({ data: "blank", text: "" })); }).map(function (r) { + return r.slice(x, x + cols).map(function (d) { return ({ text: d.text, data: [d.data] }); }); + })), false), [ [ { data: "blank", text: "" }, - { data: "up", text: "[".concat(y == 0 ? "gray" : "accent", "]^\n|") }, + ], + [ + { data: "blank", text: "" }, + { data: "up", text: "[".concat(y == 0 ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.up)) }, { data: "blank", text: "" }, ], [ - { data: "left", text: "[".concat(x == 0 ? "gray" : "accent", "]<--") }, + { data: "left", text: "[".concat(x == 0 ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.left)) }, { data: "blank", text: (_b = (_a = cfg.getCenterText) === null || _a === void 0 ? void 0 : _a.call(cfg, x, y)) !== null && _b !== void 0 ? _b : '' }, - { data: "right", text: "[".concat(x == width - cols ? "gray" : "accent", "]-->") } + { data: "right", text: "[".concat(x == width - cols ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.right)) }, ], [ { data: "blank", text: "" }, - { data: "down", text: "[".concat(y == height - rows ? "gray" : "accent", "]|\nV") }, + { data: "down", text: "[".concat(y == height - rows ? "gray" : "accent", "]").concat(String.fromCharCode(Iconc.down)) }, { data: "blank", text: "" }, ] ], false); - void exports.Menu.buttons(target, title, description, opts, __assign(__assign({}, cfg), { onCancel: "null" })).then(function (response) { + void exports.Menu.buttons(target, title, description, opts, cfg).then(function (response) { if (response instanceof Array) - resolve(response[0]); + resolve([response[0], x, y]); else if (response === "right") showPage(Math.min(x + 1, width - cols), y); else if (response === "left") diff --git a/src/frameworks/menus.ts b/src/frameworks/menus.ts index aa4b5613..eeaebd58 100644 --- a/src/frameworks/menus.ts +++ b/src/frameworks/menus.ts @@ -278,7 +278,7 @@ export const Menu = { } = {}, ){ const { promise, reject, resolve } = Promise.withResolvers< - (TCancelBehavior extends "null" ? null : never) | TOption, + (TCancelBehavior extends "null" ? null : never) | [TOption, x:number, y:number], TCancelBehavior extends "reject" ? "cancel" : never >(); const { rows = 5, columns: cols = 5 } = cfg; @@ -286,23 +286,28 @@ export const Menu = { const width = options[0].length; function showPage(x:number, y:number){ const opts:{ data: "left" | "blank" | "right" | "up" | "down" | readonly [TOption]; text: string; }[][] = [ - ...options.slice(y, y + rows).map(r => r.slice(x, x + cols).map(d => ({ text: d.text, data: [d.data] as const }))), + ...options.slice(y, y + rows).map(r => r.concat(Array(width - r.length).fill({ data: "blank", text: `` }))).map(r => + r.slice(x, x + cols).map(d => ({ text: d.text, data: [d.data] as const })) + ), + [ + { data: "blank", text: `` }, + ], [ { data: "blank", text: `` }, - { data: "up", text: `[${y == 0 ? "gray" : "accent"}]^\n|` }, + { data: "up", text: `[${y == 0 ? "gray" : "accent"}]${String.fromCharCode(Iconc.up)}` }, { data: "blank", text: `` }, ],[ - { data: "left", text: `[${x == 0 ? "gray" : "accent"}]<--` }, + { data: "left", text: `[${x == 0 ? "gray" : "accent"}]${String.fromCharCode(Iconc.left)}` }, { data: "blank", text: cfg.getCenterText?.(x, y) ?? '' }, - { data: "right", text: `[${x == width - cols ? "gray" : "accent"}]-->` } + { data: "right", text: `[${x == width - cols ? "gray" : "accent"}]${String.fromCharCode(Iconc.right)}` }, ],[ { data: "blank", text: `` }, - { data: "down", text: `[${y == height - rows ? "gray" : "accent"}]|\nV` }, + { data: "down", text: `[${y == height - rows ? "gray" : "accent"}]${String.fromCharCode(Iconc.down)}` }, { data: "blank", text: `` }, ] ]; - void Menu.buttons(target, title, description, opts, {...cfg, onCancel: "null"}).then(response => { - if(response instanceof Array) resolve(response[0]); + void Menu.buttons(target, title, description, opts, cfg).then(response => { + if(response instanceof Array) resolve([response[0], x, y]); else if(response === "right") showPage(Math.min(x + 1, width - cols), y); else if(response === "left") showPage(Math.max(x - 1, 0), y); else if(response === "up") showPage(x, Math.max(y - 1, 0)); From f67de0fd0f28f41d23c4941836a903afcfea6827 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:15:05 +0530 Subject: [PATCH 23/33] /achievementgrid --- build/scripts/commands/general.js | 34 ++++++++++++++++++++++++++++++- src/commands/general.ts | 33 +++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index 6a8097b0..1d95cc55 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1240,5 +1240,37 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { }); }); } + }, achievementgrid: { + args: ["target:player?"], + description: "Shows all achievements in a 2D scrolling menu.", + perm: commands_1.Perm.none, + handler: function (_a) { + return __awaiter(this, arguments, void 0, function (_b) { + var options, x, y, a; + var _c; + var sender = _b.sender, _d = _b.args.target, target = _d === void 0 ? sender : _d, f = _b.f; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + options = (0, funcs_1.to2DArray)(achievements_1.Achievement.all, 6).map(function (row) { return row.map(function (a) { return ({ + data: a, + text: a.has(target) ? a.icon : "[gray]".concat(Strings.stripColors(a.icon)), + }); }); }); + x = 0, y = 0; + a = null; + _e.label = 1; + case 1: + if (!true) return [3 /*break*/, 3]; + return [4 /*yield*/, menus_1.Menu.scroll(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : "Click an achievement icon to show more information.", options, { onCancel: "reject", columns: 4, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; + case 2: + _c = __read.apply(void 0, [_e.sent(), 3]), a = _c[0], x = _c[1], y = _c[2]; + if (a == achievements_1.Achievements.click_me && target == sender) + a.grantTo(sender); + return [3 /*break*/, 1]; + case 3: return [2 /*return*/]; + } + }); + }); + } } })); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23; diff --git a/src/commands/general.ts b/src/commands/general.ts index 913b054e..f769db90 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -3,7 +3,7 @@ Copyright © BalaM314, 2026. All Rights Reserved. This file contains most in-game chat commands that can be run by untrusted players. */ -import { Achievement } from "/achievements"; +import { Achievement, Achievements } from "/achievements"; import * as api from "/api"; import { FColor, FishServer, Gamemode, rules, text } from "/config"; import { command, commandList, fail, formatArg, Perm, Req } from "/frameworks/commands"; @@ -1185,5 +1185,36 @@ ${a.hidden ? "This achievement is secret." : ""}\ ])); } }, + + achievementgrid: { + args: ["target:player?"], + description: "Shows all achievements in a 2D scrolling menu.", + perm: Perm.none, + async handler({sender, args: { target = sender }, f}){ + const options = to2DArray(Achievement.all, 6).map(row => row.map(a => ({ + data: a, + text: a.has(target) ? a.icon : `[gray]${Strings.stripColors(a.icon)}`, + }))); + let x = 0, y = 0; + let a: Achievement | null = null; + while(true){ + [a, x, y] = await Menu.scroll( + sender, "Achievements", + a ? FColor.achievement`\ +${a.icon} ${a.name} + +${a.description + (a.extendedDescription ? ("\n" + `[gray]${a.extendedDescription}`) : "")} + +Allowed modes: ${a.modesText} +Unlocked: ${f.boolGood(a.has(target))} +${a.hidden ? "This achievement is secret." : ""}\ +` : "Click an achievement icon to show more information.", + options, + { onCancel: "reject", columns: 4, rows: 4, getCenterText: () => String.fromCharCode(Iconc.settings), x, y } + ); + if(a == Achievements.click_me && target == sender) a.grantTo(sender); + } + } + }, }); From bcdc9313f39a720f9372493082b1f35d81cc6500 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:15:12 +0530 Subject: [PATCH 24/33] "click me" achievement --- build/scripts/achievements.js | 3 ++- src/achievements.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index f8627140..7ba0672c 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -110,7 +110,7 @@ var Achievement = /** @class */ (function () { } } Achievement.prototype.message = function () { - return config_1.FColor.achievement(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Achievement granted!\n[accent]", ": [white]", ""], ["Achievement granted!\\n[accent]", ": [white]", ""])), this.name, this.description); + return config_1.FColor.achievement(templateObject_1 || (templateObject_1 = __makeTemplateObject(["Achievement granted!\n[accent]", "[white]: ", ""], ["Achievement granted!\\n[accent]", "[white]: ", ""])), this.name, this.description); }; Achievement.prototype.messageToEveryone = function (player) { return config_1.FColor.achievement(templateObject_2 || (templateObject_2 = __makeTemplateObject(["Player ", " has completed the achievement \"", "\"."], ["Player ", " has completed the achievement \"", "\"."])), player.prefixedName, this.name); @@ -583,6 +583,7 @@ exports.Achievements = { verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "nobody" }), + click_me: new Achievement(Iconc.bookOpen, "Clicked", "Run /achievementsgrid and click this achievement."), afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { modes: ["not", "sandbox"], checkPlayerGameover: function (player, winTeam) { diff --git a/src/achievements.ts b/src/achievements.ts index e73a713a..6db47927 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -94,7 +94,7 @@ export class Achievement { } message():string { - return FColor.achievement`Achievement granted!\n[accent]${this.name}: [white]${this.description}`; + return FColor.achievement`Achievement granted!\n[accent]${this.name}[white]: ${this.description}`; } messageToEveryone(player:FishPlayer):string { return FColor.achievement`Player ${player.prefixedName} has completed the achievement "${this.name}".`; @@ -473,6 +473,7 @@ export const Achievements = { verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "nobody" }), + click_me: new Achievement(Iconc.bookOpen, "Clicked", `Run /achievementsgrid and click this achievement.`), afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { modes: ["not", "sandbox"], checkPlayerGameover(player, winTeam) { From fdc5592a07f5461c5ad790e43d26b4b5a2ccb83f Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:26:03 +0530 Subject: [PATCH 25/33] Add jsdoc comments to the menu functions --- build/scripts/commands/general.js | 4 ++-- build/scripts/frameworks/menus.js | 34 ++++++++++++++++++++++++-- src/commands/general.ts | 4 ++-- src/frameworks/menus.ts | 40 +++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index 1d95cc55..6a45044c 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1093,7 +1093,7 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { data: null, }); }), Vars.world.width()).reverse(); var height = Vars.world.height(); - void menus_1.Menu.scroll(sender, "The World", "Use the arrow keys to navigate around the world. Click a blank square to exit.", options, { + void menus_1.Menu.scroll2D(sender, "The World", "Use the arrow keys to navigate around the world. Click a blank square to exit.", options, { columns: size, rows: size, x: x ? x - Math.trunc(size / 2) : 0, @@ -1261,7 +1261,7 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { _e.label = 1; case 1: if (!true) return [3 /*break*/, 3]; - return [4 /*yield*/, menus_1.Menu.scroll(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : "Click an achievement icon to show more information.", options, { onCancel: "reject", columns: 4, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; + return [4 /*yield*/, menus_1.Menu.scroll2D(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : "Click an achievement icon to show more information.", options, { onCancel: "reject", columns: 4, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; case 2: _c = __read.apply(void 0, [_e.sent(), 3]), a = _c[0], x = _c[1], y = _c[2]; if (a == achievements_1.Achievements.click_me && target == sender) diff --git a/build/scripts/frameworks/menus.js b/build/scripts/frameworks/menus.js index 60f0a486..a03ec72e 100644 --- a/build/scripts/frameworks/menus.js +++ b/build/scripts/frameworks/menus.js @@ -163,7 +163,7 @@ exports.Menu = { Call.menu(target.con, registeredListeners.generic, title, description, stringifiedOptions); return promise; }, - /** Displays a menu to a player, returning a Promise. Arranges options into a 2D array, and can add a Cancel option. */ + /** Displays a menu to a player, returning a Promise. Arranges provided options into a 2D array, and can add a Cancel option. */ menu: function (title, description, options, target, _a) { var _b = _a === void 0 ? {} : _a, _c = _b.includeCancel, includeCancel = _c === void 0 ? false : _c, _d = _b.optionStringifier, optionStringifier = _d === void 0 ? String : _d, _e = _b.columns, columns = _e === void 0 ? 3 : _e, _f = _b.onCancel, onCancel = _f === void 0 ? "ignore" : _f, _g = _b.cancelOptionId, cancelOptionId = _g === void 0 ? -1 : _g; //Set up the 2D array of options, and maybe add cancel @@ -196,10 +196,19 @@ exports.Menu = { var _b = _a.confirmText, confirmText = _b === void 0 ? "[red]Confirm" : _b, _c = _a.cancelText, cancelText = _c === void 0 ? "[green]Cancel" : _c, rest = __rest(_a, ["confirmText", "cancelText"]); return exports.Menu.confirm(target, description, __assign({ cancelText: cancelText, confirmText: confirmText }, rest)); }, + /** + * Displays a menu to a player, returning a Promise. + * Accepts pre-generated data and text. Alternative to optionStringifier if the text is already generated. + */ buttons: function (target, title, description, options, cfg) { if (cfg === void 0) { cfg = {}; } return exports.Menu.raw(title, description, options, target, __assign(__assign({}, cfg), { optionStringifier: function (o) { return o.text; } })).then(function (o) { return o === null || o === void 0 ? void 0 : o.data; }); }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. + * Shows different options based on the page. + */ pages: function (target, title, description, options, cfg) { var _a = promise_1.Promise.withResolvers(), promise = _a.promise, reject = _a.reject, resolve = _a.resolve; function showPage(index) { @@ -230,6 +239,12 @@ exports.Menu = { showPage(0); return promise; }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. + * Does not support options. + * Shows different text based on the current page. + */ textPages: function (target, pages, cfg) { if (cfg === void 0) { cfg = {}; } var _a = promise_1.Promise.withResolvers(), promise = _a.promise, reject = _a.reject, resolve = _a.resolve; @@ -281,7 +296,13 @@ exports.Menu = { showPage(index); return promise; }, - scroll: function (target, title, description, options, cfg) { + /** + * Displays a menu to a player, returning a Promise. + * Accepts a 2D array of options and shows a region of that 2D grid. + * Adds arrows to scroll left/right/up/down. + * Resolves to the selected option. + */ + scroll2D: function (target, title, description, options, cfg) { var _a, _b; if (cfg === void 0) { cfg = {}; } var _c = promise_1.Promise.withResolvers(), promise = _c.promise, reject = _c.reject, resolve = _c.resolve; @@ -334,6 +355,11 @@ exports.Menu = { showPage(Math.min((_a = cfg.x) !== null && _a !== void 0 ? _a : 0, width - cols), Math.min((_b = cfg.y) !== null && _b !== void 0 ? _b : 0, height - rows)); return promise; }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. Automatically paginates provided options. + * Accepts pre-generated data and text. Alternative to optionStringifier if the text is already generated. + */ pagedListButtons: function (target, title, description, options, _a) { var _b; var _c = _a.rowsPerPage, rowsPerPage = _c === void 0 ? 10 : _c, _d = _a.columns, columns = _d === void 0 ? 3 : _d, cfg = __rest(_a, ["rowsPerPage", "columns"]); @@ -343,6 +369,10 @@ exports.Menu = { return exports.Menu.buttons(target, title, description, (_b = pages[0]) !== null && _b !== void 0 ? _b : [], cfg); return exports.Menu.pages(target, title, description, pages, cfg); }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. Automatically paginates provided options. + */ pagedList: function (target, title, description, options, _a) { var _b; if (_a === void 0) { _a = {}; } diff --git a/src/commands/general.ts b/src/commands/general.ts index f769db90..40f620f0 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -1045,7 +1045,7 @@ Win rate: ${stats.gamesWon / stats.gamesFinished}` data: null, })), Vars.world.width()).reverse(); const height = Vars.world.height(); - void Menu.scroll(sender, "The World", "Use the arrow keys to navigate around the world. Click a blank square to exit.", options, { + void Menu.scroll2D(sender, "The World", "Use the arrow keys to navigate around the world. Click a blank square to exit.", options, { columns: size, rows: size, x: x ? x - Math.trunc(size / 2) : 0, @@ -1198,7 +1198,7 @@ ${a.hidden ? "This achievement is secret." : ""}\ let x = 0, y = 0; let a: Achievement | null = null; while(true){ - [a, x, y] = await Menu.scroll( + [a, x, y] = await Menu.scroll2D( sender, "Achievements", a ? FColor.achievement`\ ${a.icon} ${a.name} diff --git a/src/frameworks/menus.ts b/src/frameworks/menus.ts index eeaebd58..c05fac58 100644 --- a/src/frameworks/menus.ts +++ b/src/frameworks/menus.ts @@ -126,7 +126,7 @@ export const Menu = { Call.menu(target.con, registeredListeners.generic, title, description, stringifiedOptions); return promise; }, - /** Displays a menu to a player, returning a Promise. Arranges options into a 2D array, and can add a Cancel option. */ + /** Displays a menu to a player, returning a Promise. Arranges provided options into a 2D array, and can add a Cancel option. */ menu( this:void, title:string, description:string, options:TOption[], target:FishPlayer, { @@ -175,6 +175,10 @@ export const Menu = { }:MenuConfirmProps = {}){ return Menu.confirm(target, description, { cancelText, confirmText, ...rest }); }, + /** + * Displays a menu to a player, returning a Promise. + * Accepts pre-generated data and text. Alternative to optionStringifier if the text is already generated. + */ buttons( this:void, target:FishPlayer, title:string, description:string, options:{ data: TButtonData; text: string; }[][], @@ -185,6 +189,11 @@ export const Menu = { optionStringifier: o => o.text, }).then(o => o?.data as TButtonData | (TCancelBehavior extends "null" ? null : never)); }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. + * Shows different options based on the page. + */ pages( this:void, target:FishPlayer, title:string, description:string, options:{ data: TOption; text: string; }[][][], @@ -218,15 +227,21 @@ export const Menu = { showPage(0); return promise; }, - textPages( + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. + * Does not support options. + * Shows different text based on the current page. + */ + textPages( this:void, target:FishPlayer, pages:Array string]>, - cfg: Pick, "onCancel"> & { + cfg: Pick, "onCancel"> & { /** Index or title of the initial page. */ startPage?: number | string; } = {}, ){ const { promise, reject, resolve } = Promise.withResolvers< - (TCancelBehavior extends "null" ? null : never) | TOption, + (TCancelBehavior extends "null" ? null : never), TCancelBehavior extends "reject" ? "cancel" : never >(); const pageSkipSize = Math.max(Math.floor(pages.length / 8), 5); @@ -267,7 +282,13 @@ export const Menu = { showPage(index); return promise; }, - scroll( + /** + * Displays a menu to a player, returning a Promise. + * Accepts a 2D array of options and shows a region of that 2D grid. + * Adds arrows to scroll left/right/up/down. + * Resolves to the selected option. + */ + scroll2D( this:void, target:FishPlayer, title:string, description:string, options:{ data: TOption; text: string; }[][], cfg: Pick, "onCancel" | "columns"> & { @@ -323,6 +344,11 @@ export const Menu = { showPage(Math.min(cfg.x ?? 0, width - cols), Math.min(cfg.y ?? 0, height - rows)); return promise; }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. Automatically paginates provided options. + * Accepts pre-generated data and text. Alternative to optionStringifier if the text is already generated. + */ pagedListButtons( this:void, target:FishPlayer, title:string, description:string, options:Array<{ data: TButtonData; text: string; }>, @@ -336,6 +362,10 @@ export const Menu = { if(pages.length <= 1) return Menu.buttons(target, title, description, pages[0] ?? [], cfg); return Menu.pages(target, title, description, pages, cfg); }, + /** + * Displays a menu to a player, returning a Promise. + * Adds left and right arrows to switch pages. Automatically paginates provided options. + */ pagedList( this:void, target:FishPlayer, title:string, description:string, options:TButtonData[], From a3735e76e26ddb27ea04f415a35e716cbc9efa69 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 10 Jan 2026 20:56:42 +0530 Subject: [PATCH 26/33] Make hidden work --- build/scripts/commands/general.js | 5 +++-- src/commands/general.ts | 14 +++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index 6a45044c..d89d8ea0 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1229,7 +1229,8 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { var sender = _b.sender, _c = _b.args.target, target = _c === void 0 ? sender : _c, f = _b.f; return __generator(this, function (_d) { switch (_d.label) { - case 0: return [4 /*yield*/, menus_1.Menu.textPages(sender, achievements_1.Achievement.all.map(function (a) { return [ + case 0: return [4 /*yield*/, menus_1.Menu.textPages(sender, achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }) + .map(function (a) { return [ "".concat(a.icon, "[] ").concat(a.name), function () { return config_1.FColor.achievement(templateObject_22 || (templateObject_22 = __makeTemplateObject(["", "\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", "\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : ""); } ]; }))]; @@ -1252,7 +1253,7 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { return __generator(this, function (_e) { switch (_e.label) { case 0: - options = (0, funcs_1.to2DArray)(achievements_1.Achievement.all, 6).map(function (row) { return row.map(function (a) { return ({ + options = (0, funcs_1.to2DArray)(achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }), 6).map(function (row) { return row.map(function (a) { return ({ data: a, text: a.has(target) ? a.icon : "[gray]".concat(Strings.stripColors(a.icon)), }); }); }); diff --git a/src/commands/general.ts b/src/commands/general.ts index 40f620f0..41015ae8 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -1174,15 +1174,19 @@ ${achievement.hidden ? "This achievement is secret." : ""}\ description: "Shows all achievements in a paged menu.", perm: Perm.none, async handler({sender, args: { target = sender }, f}){ - await Menu.textPages(sender, Achievement.all.map(a => [ - `${a.icon}[] ${a.name}`, - () => FColor.achievement`\ + await Menu.textPages( + sender, + Achievement.all.filter(a => !a.hidden || a.has(target)) + .map(a => [ + `${a.icon}[] ${a.name}`, + () => FColor.achievement`\ ${a.description + (a.extendedDescription ? ("\n" + `[gray]${a.extendedDescription}`) : "")} Allowed modes: ${a.modesText} Unlocked: ${f.boolGood(a.has(target))} ${a.hidden ? "This achievement is secret." : ""}\ ` - ])); + ]) + ); } }, @@ -1191,7 +1195,7 @@ ${a.hidden ? "This achievement is secret." : ""}\ description: "Shows all achievements in a 2D scrolling menu.", perm: Perm.none, async handler({sender, args: { target = sender }, f}){ - const options = to2DArray(Achievement.all, 6).map(row => row.map(a => ({ + const options = to2DArray(Achievement.all.filter(a => !a.hidden || a.has(target)), 6).map(row => row.map(a => ({ data: a, text: a.has(target) ? a.icon : `[gray]${Strings.stripColors(a.icon)}`, }))); From 9f3f8c28858c0302f0126c07d0a739c28fb723f5 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:12:07 +0530 Subject: [PATCH 27/33] Fixes --- build/scripts/achievements.js | 13 ++++++++++--- src/achievements.ts | 10 ++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index 7ba0672c..892aa482 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -583,7 +583,7 @@ exports.Achievements = { verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "nobody" }), - click_me: new Achievement(Iconc.bookOpen, "Clicked", "Run /achievementsgrid and click this achievement."), + click_me: new Achievement(Iconc.bookOpen, "Clicked", "Run /achievementgrid and click this achievement."), afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { modes: ["not", "sandbox"], checkPlayerGameover: function (player, winTeam) { @@ -653,8 +653,15 @@ Events.on(EventType.UnitDrownEvent, function (_a) { if (!config_1.Gamemode.sandbox()) { if (unit.type == UnitTypes.mace && ((_b = unit.tileOn()) === null || _b === void 0 ? void 0 : _b.floor()) == Blocks.cryofluid) exports.Achievements.drown_mace_in_cryo.grantToAllOnline(); - else if (unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) - exports.Achievements.drown_big_tank.grantToAllOnline(); + else if (unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) { + if (config_1.Gamemode.pvp()) { + Vars.state.teams.active.map(function (t) { return t.team; }).select(function (t) { return t !== unit.team; }).each(function (t) { return exports.Achievements.drown_big_tank.grantToAllOnline(t); }); + } + else { + if (unit.team !== Vars.state.rules.defaultTeam) + exports.Achievements.drown_big_tank.grantToAllOnline(); + } + } } }); Events.on(EventType.UnitBulletDestroyEvent, function (_a) { diff --git a/src/achievements.ts b/src/achievements.ts index 6db47927..4756ff60 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -473,7 +473,7 @@ export const Achievements = { verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "nobody" }), - click_me: new Achievement(Iconc.bookOpen, "Clicked", `Run /achievementsgrid and click this achievement.`), + click_me: new Achievement(Iconc.bookOpen, "Clicked", `Run /achievementgrid and click this achievement.`), afk: new Achievement(["yellow", Iconc.lock], "AFK?", "Win a game without interacting with any blocks.", { modes: ["not", "sandbox"], checkPlayerGameover(player, winTeam) { @@ -536,7 +536,13 @@ FishEvents.on("commandUnauthorized", (_, player, name) => { Events.on(EventType.UnitDrownEvent, ({unit}:{unit: Unit}) => { if(!Gamemode.sandbox()){ if(unit.type == UnitTypes.mace && unit.tileOn()?.floor() == Blocks.cryofluid) Achievements.drown_mace_in_cryo.grantToAllOnline(); - else if(unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish) Achievements.drown_big_tank.grantToAllOnline(); + else if(unit.type == UnitTypes.conquer || unit.type == UnitTypes.vanquish){ + if(Gamemode.pvp()){ + Vars.state.teams.active.map(t => t.team).select(t => t !== unit.team).each(t => Achievements.drown_big_tank.grantToAllOnline(t)); + } else { + if(unit.team !== Vars.state.rules.defaultTeam) Achievements.drown_big_tank.grantToAllOnline(); + } + } } }); From d1fda5f7105f9979eafb50072f5c37c108b333ee Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:25:14 +0530 Subject: [PATCH 28/33] update env this is starting to get out of hand --- spec/src/env.ts | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/spec/src/env.ts b/spec/src/env.ts index 4c0cbfec..64a30b64 100644 --- a/spec/src/env.ts +++ b/spec/src/env.ts @@ -687,7 +687,9 @@ const Vars = { modDirectory: new Fi('/mods'), customMapDirectory: new Fi('/maps'), content: { - + items(){ + return new Seq(Object.values(Items).filter(i => i instanceof Item)); + } }, tilesize: 8, world: { @@ -843,13 +845,46 @@ const Strings = { }, }; -class UnitType {} - +class UnitType { + emoji(){return "e";} +} const UnitTypes = Object.fromEntries( ["mace", "dagger", "crawler", "fortress", "scepter", "reign", "vela", "nova", "pulsar", "quasar", "corvus", "atrax", "merui", "cleroi", "anthicus", "tecta", "collaris", "spiroct", "arkyid", "toxopid", "elude", "flare", "eclipse", "horizon", "zenith", "antumbra", "avert", "obviate", "mono", "poly", "mega", "evoke", "incite", "emanate", "quell", "disrupt", "quad", "oct", "alpha", "beta", "gamma", "risso", "minke", "bryde", "sei", "omura", "retusa", "oxynoe", "cyerce", "aegires", "navanax", "block", "manifold", "assemblyDrone", "stell", "locus", "precept", "vanquish", "conquer", "missile", "latum", "renale"] .map(n => [n, new UnitType()]) ); +class Item { + emoji(){return "e";} +} +const Items = Object.fromEntries( + ["scrap", "copper", "lead", "graphite", "coal", "titanium", "thorium", "silicon", "plastanium", "phaseFabric", "surgeAlloy", "sporePod", "sand", "blastCompound", "pyratite", "metaglass", "beryllium", "tungsten", "oxide", "carbide", "fissileMatter", "dormantCyst"] + .map(n => [n, new Item()]) +) as any; +Items.serpuloItems = new Seq(Object.values(Items).slice(0, -6)); +Items.erekirItems = new Seq([Items.graphite, Items.thorium, Items.silicon, Items.phaseFabric, Items.surgeAlloy, Items.beryllium, Items.tungsten, Items.oxide, Items.carbide, Items.sand]); + +class Block { + emoji(){return "e";} +} +const Blocks = Object.fromEntries( + ["air", "spawn", "removeWall", "removeOre", "cliff", "deepwater", "water", "taintedWater", "deepTaintedWater", "tar", "slag", "cryofluid", "stone", "craters", "charr", "sand", "darksand", "dirt", "mud", "ice", "snow", "darksandTaintedWater", "space", "empty", "dacite", "rhyolite", "rhyoliteCrater", "roughRhyolite", "regolith", "yellowStone", "redIce", "redStone", "denseRedStone", "arkyciteFloor", "arkyicStone", "redmat", "bluemat", "stoneWall", "dirtWall", "sporeWall", "iceWall", "daciteWall", "sporePine", "snowPine", "pine", "shrubs", "whiteTree", "whiteTreeDead", "sporeCluster", "redweed", "purbush", "yellowCoral", "rhyoliteVent", "carbonVent", "arkyicVent", "yellowStoneVent", "redStoneVent", "crystallineVent", "stoneVent", "basaltVent", "regolithWall", "yellowStoneWall", "rhyoliteWall", "carbonWall", "redIceWall", "ferricStoneWall", "beryllicStoneWall", "arkyicWall", "crystallineStoneWall", "redStoneWall", "redDiamondWall", "ferricStone", "ferricCraters", "carbonStone", "beryllicStone", "crystallineStone", "crystalFloor", "yellowStonePlates", "iceSnow", "sandWater", "darksandWater", "duneWall", "sandWall", "moss", "sporeMoss", "shale", "shaleWall", "grass", "salt", "coreZone", "shaleBoulder", "sandBoulder", "daciteBoulder", "boulder", "snowBoulder", "basaltBoulder", "carbonBoulder", "ferricBoulder", "beryllicBoulder", "yellowStoneBoulder", "arkyicBoulder", "crystalCluster", "vibrantCrystalCluster", "crystalBlocks", "crystalOrbs", "crystallineBoulder", "redIceBoulder", "rhyoliteBoulder", "redStoneBoulder", "metalFloor", "metalFloorDamaged", "metalFloor2", "metalFloor3", "metalFloor4", "metalFloor5", "basalt", "magmarock", "hotrock", "snowWall", "saltWall", "darkPanel1", "darkPanel2", "darkPanel3", "darkPanel4", "darkPanel5", "darkPanel6", "darkMetal", "metalTiles1", "metalTiles2", "metalTiles3", "metalTiles4", "metalTiles5", "metalTiles6", "metalTiles7", "metalTiles8", "metalTiles9", "metalTiles10", "metalTiles11", "metalTiles12", "metalTiles13", "metalWall1", "metalWall2", "metalWall3", "metalWall4", "coloredFloor", "coloredWall", "characterOverlayGray", "characterOverlayWhite", "runeOverlay", "cruxRuneOverlay", "pebbles", "tendrils", "oreCopper", "oreLead", "oreScrap", "oreCoal", "oreTitanium", "oreThorium", "oreBeryllium", "oreTungsten", "oreCrystalThorium", "wallOreThorium", "wallOreBeryllium", "graphiticWall", "wallOreTungsten", "siliconSmelter", "siliconCrucible", "kiln", "graphitePress", "plastaniumCompressor", "multiPress", "phaseWeaver", "surgeSmelter", "pyratiteMixer", "blastMixer", "cryofluidMixer", "melter", "separator", "disassembler", "sporePress", "pulverizer", "incinerator", "coalCentrifuge", "siliconArcFurnace", "electrolyzer", "oxidationChamber", "atmosphericConcentrator", "electricHeater", "slagHeater", "phaseHeater", "heatRedirector", "smallHeatRedirector", "heatRouter", "slagIncinerator", "carbideCrucible", "slagCentrifuge", "surgeCrucible", "cyanogenSynthesizer", "phaseSynthesizer", "heatReactor", "powerSource", "powerVoid", "itemSource", "itemVoid", "liquidSource", "liquidVoid", "payloadSource", "payloadVoid", "illuminator", "heatSource", "copperWall", "copperWallLarge", "titaniumWall", "titaniumWallLarge", "plastaniumWall", "plastaniumWallLarge", "thoriumWall", "thoriumWallLarge", "door", "doorLarge", "phaseWall", "phaseWallLarge", "surgeWall", "surgeWallLarge", "berylliumWall", "berylliumWallLarge", "tungstenWall", "tungstenWallLarge", "blastDoor", "reinforcedSurgeWall", "reinforcedSurgeWallLarge", "carbideWall", "carbideWallLarge", "shieldedWall", "mender", "mendProjector", "overdriveProjector", "overdriveDome", "forceProjector", "shockMine", "scrapWall", "scrapWallLarge", "scrapWallHuge", "scrapWallGigantic", "thruster", "ok", "these", "names", "are", "getting", "ridiculous", "but", "at", "least", "I", "don", "t", "have", "humongous", "walls", "yet", "radar", "buildTower", "regenProjector", "barrierProjector", "shockwaveTower", "shieldProjector", "largeShieldProjector", "shieldBreaker", "conveyor", "titaniumConveyor", "plastaniumConveyor", "armoredConveyor", "distributor", "junction", "itemBridge", "phaseConveyor", "sorter", "invertedSorter", "router", "overflowGate", "underflowGate", "unloader", "massDriver", "duct", "armoredDuct", "ductRouter", "overflowDuct", "underflowDuct", "ductBridge", "ductUnloader", "surgeConveyor", "surgeRouter", "unitCargoLoader", "unitCargoUnloadPoint", "mechanicalPump", "rotaryPump", "impulsePump", "conduit", "pulseConduit", "platedConduit", "liquidRouter", "liquidContainer", "liquidTank", "liquidJunction", "bridgeConduit", "phaseConduit", "reinforcedPump", "reinforcedConduit", "reinforcedLiquidJunction", "reinforcedBridgeConduit", "reinforcedLiquidRouter", "reinforcedLiquidContainer", "reinforcedLiquidTank", "combustionGenerator", "thermalGenerator", "steamGenerator", "differentialGenerator", "rtgGenerator", "solarPanel", "largeSolarPanel", "thoriumReactor", "impactReactor", "battery", "batteryLarge", "powerNode", "powerNodeLarge", "surgeTower", "diode", "turbineCondenser", "ventCondenser", "chemicalCombustionChamber", "pyrolysisGenerator", "fluxReactor", "neoplasiaReactor", "beamNode", "beamTower", "beamLink", "mechanicalDrill", "pneumaticDrill", "laserDrill", "blastDrill", "waterExtractor", "oilExtractor", "cultivator", "cliffCrusher", "largeCliffCrusher", "plasmaBore", "largePlasmaBore", "impactDrill", "eruptionDrill", "coreShard", "coreFoundation", "coreNucleus", "vault", "container", "coreBastion", "coreCitadel", "coreAcropolis", "reinforcedContainer", "reinforcedVault", "duo", "scatter", "scorch", "hail", "arc", "wave", "lancer", "swarmer", "salvo", "fuse", "ripple", "cyclone", "foreshadow", "spectre", "meltdown", "segment", "parallax", "tsunami", "breach", "diffuse", "sublimate", "titan", "disperse", "afflict", "lustre", "scathe", "smite", "malign", "groundFactory", "airFactory", "navalFactory", "additiveReconstructor", "multiplicativeReconstructor", "exponentialReconstructor", "tetrativeReconstructor", "repairPoint", "repairTurret", "tankFabricator", "shipFabricator", "mechFabricator", "tankRefabricator", "shipRefabricator", "mechRefabricator", "primeRefabricator", "tankAssembler", "shipAssembler", "mechAssembler", "basicAssemblerModule", "unitRepairTower", "payloadConveyor", "payloadRouter", "reinforcedPayloadConveyor", "reinforcedPayloadRouter", "payloadMassDriver", "largePayloadMassDriver", "smallDeconstructor", "deconstructor", "constructor", "largeConstructor", "payloadLoader", "payloadUnloader", "message", "switchBlock", "microProcessor", "logicProcessor", "hyperProcessor", "largeLogicDisplay", "logicDisplay", "tileLogicDisplay", "memoryCell", "memoryBank", "canvas", "reinforcedMessage", "worldProcessor", "worldCell", "worldMessage", "worldSwitch", "launchPad", "advancedLaunchPad", "landingPad", "interplanetaryAccelerator"] + .map(n => [n, new Block()]) +) as any; + +class StatusEffect { + emoji(){return "e";} +} +const StatusEffects = Object.fromEntries( + ["none", "burning", "freezing", "unmoving", "slow", "fast", "wet", "muddy", "melting", "sapped", "tarred", "overdrive", "overclock", "shielded", "shocked", "blasted", "corroded", "boss", "sporeSlowed", "disarmed", "electrified", "invincible", "dynamic"] + .map(n => [n, new StatusEffect()]) +) as any; + +class ItemStack { + constructor( + public item: Item, public amount: number, + ){} +} + class Bits { bits = new BigUint64Array(1); constructor(capacity?: number){ /*empty*/ } @@ -876,6 +911,8 @@ class Bits { } } +const Iconc = Object.fromEntries(["rotate", "modeSurvival", "power", "left", "redditAlien", "edit", "downOpen", "pencil", "file", "lockOpen", "right", "infoCircle", "pick", "settings", "spray1", "terrain", "exit", "wrench", "lock", "discord", "eye", "none", "play", "diagonal", "eraser", "trash", "liquid", "fileImage", "defense", "layers", "grid", "admin", "steam", "star", "chartBar", "chat", "android", "image", "map", "logic", "menu", "commandRally", "editor", "folder", "units", "commandAttack", "copy", "filter", "cancel", "terminal", "upload", "eyeOff", "save", "planeOutline", "fill", "distribution", "upOpen", "rightOpen", "modePvp", "download", "list", "flipX", "flipY", "effect", "paste", "planet", "waves", "up", "warning", "tree", "add", "down", "host", "spray", "info", "players", "resize", "refresh1", "production", "crafting", "pause", "googleplay", "hammer", "fileText", "modeAttack", "move", "zoom", "bookOpen", "refresh", "ok", "home", "githubSquare", "powerOld", "github", "undo", "box", "trello", "book", "export", "fileTextFill", "rightOpenOut", "turret", "leftOpen", "line", "itchio", "link", "filters", "redo"].map(n => [n, 0xFE60])); + const Packages = { java: { net: { NetworkInterface, Inet4Address }, @@ -888,4 +925,4 @@ const Packages = { gen: { Map: MMap } } }; -Object.assign(globalThis, {Pattern, ObjectIntMap, Seq, Fi, Packages, Events, Trigger, Team, EventType, Timer, EffectCallPacket2, LabelReliableCallPacket, Vars, ServerControl, Core, Log, Menus, Time, CommandHandler, Gamemode, Fx, Effect, Vec2, Tmp, Paths, Path, Threads, CommandRunner, Strings, UnitTypes, Bits}); +Object.assign(globalThis, {Pattern, ObjectIntMap, Seq, Fi, Packages, Events, Trigger, Team, EventType, Timer, EffectCallPacket2, LabelReliableCallPacket, Vars, ServerControl, Core, Log, Menus, Time, CommandHandler, Gamemode, Fx, Effect, Vec2, Tmp, Paths, Path, Threads, CommandRunner, Strings, UnitTypes, Bits, Items, ItemStack, Iconc, Blocks, StatusEffects}); From 1b30d4b86d3e0eb17621f7711cd21d55ce48a236 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:18:53 +0530 Subject: [PATCH 29/33] Update /achievementgrid --- build/scripts/commands/general.js | 14 ++++++++++---- src/commands/general.ts | 12 +++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index d89d8ea0..c683bf29 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1247,22 +1247,28 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { perm: commands_1.Perm.none, handler: function (_a) { return __awaiter(this, arguments, void 0, function (_b) { - var options, x, y, a; + var visibleAchievements, options, numberAchievements, totalAchievements, x, y, a; var _c; var sender = _b.sender, _d = _b.args.target, target = _d === void 0 ? sender : _d, f = _b.f; return __generator(this, function (_e) { switch (_e.label) { case 0: - options = (0, funcs_1.to2DArray)(achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }), 6).map(function (row) { return row.map(function (a) { return ({ + visibleAchievements = achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }); + options = (0, funcs_1.to2DArray)(visibleAchievements, 6).map(function (row) { return row.map(function (a) { return ({ data: a, text: a.has(target) ? a.icon : "[gray]".concat(Strings.stripColors(a.icon)), }); }); }); + numberAchievements = achievements_1.Achievement.all.filter(function (a) { return a.has(target); }).length; + totalAchievements = visibleAchievements.length; x = 0, y = 0; a = null; _e.label = 1; case 1: if (!true) return [3 /*break*/, 3]; - return [4 /*yield*/, menus_1.Menu.scroll2D(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : "Click an achievement icon to show more information.", options, { onCancel: "reject", columns: 4, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; + return [4 /*yield*/, menus_1.Menu.scroll2D(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : + (target == sender ? "You have ".concat(numberAchievements, "/").concat(totalAchievements, " achievements.") + : config_1.FColor.achievement(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Player ", " has ", "/", " achievements."], ["Player ", " has ", "/", " achievements."])), target.prefixedName, numberAchievements, totalAchievements)) + + "\nClick an achievement icon to show more information.", options, { onCancel: "ignore", columns: 4, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; case 2: _c = __read.apply(void 0, [_e.sent(), 3]), a = _c[0], x = _c[1], y = _c[2]; if (a == achievements_1.Achievements.click_me && target == sender) @@ -1274,4 +1280,4 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { }); } } })); -var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23; +var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10, templateObject_11, templateObject_12, templateObject_13, templateObject_14, templateObject_15, templateObject_16, templateObject_17, templateObject_18, templateObject_19, templateObject_20, templateObject_21, templateObject_22, templateObject_23, templateObject_24; diff --git a/src/commands/general.ts b/src/commands/general.ts index 41015ae8..4cc1e1b0 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -1195,10 +1195,13 @@ ${a.hidden ? "This achievement is secret." : ""}\ description: "Shows all achievements in a 2D scrolling menu.", perm: Perm.none, async handler({sender, args: { target = sender }, f}){ - const options = to2DArray(Achievement.all.filter(a => !a.hidden || a.has(target)), 6).map(row => row.map(a => ({ + const visibleAchievements = Achievement.all.filter(a => !a.hidden || a.has(target)); + const options = to2DArray(visibleAchievements, 6).map(row => row.map(a => ({ data: a, text: a.has(target) ? a.icon : `[gray]${Strings.stripColors(a.icon)}`, }))); + const numberAchievements = Achievement.all.filter(a => a.has(target)).length; + const totalAchievements = visibleAchievements.length; let x = 0, y = 0; let a: Achievement | null = null; while(true){ @@ -1212,9 +1215,12 @@ ${a.description + (a.extendedDescription ? ("\n" + `[gray]${a.extendedDescriptio Allowed modes: ${a.modesText} Unlocked: ${f.boolGood(a.has(target))} ${a.hidden ? "This achievement is secret." : ""}\ -` : "Click an achievement icon to show more information.", +` : + (target == sender ? `You have ${numberAchievements}/${totalAchievements} achievements.` + : FColor.achievement`Player ${target.prefixedName} has ${numberAchievements}/${totalAchievements} achievements.`) + + "\nClick an achievement icon to show more information.", options, - { onCancel: "reject", columns: 4, rows: 4, getCenterText: () => String.fromCharCode(Iconc.settings), x, y } + { onCancel: "ignore", columns: 4, rows: 4, getCenterText: () => String.fromCharCode(Iconc.settings), x, y } ); if(a == Achievements.click_me && target == sender) a.grantTo(sender); } From f8a9c194a2896319dc3753b95bc6e803de90b8e4 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:20:00 +0530 Subject: [PATCH 30/33] Update icon for welcome --- build/scripts/achievements.js | 2 +- src/achievements.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index 892aa482..5cb72516 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -304,7 +304,7 @@ exports.Achievements = { // Do not remove any achievements: instead, set the "disabled" option to true. // Reordering achievements will cause ID shifts. //Joining based - welcome: new Achievement("_", "Welcome", "Join the server.", { + welcome: new Achievement(["gold", Iconc.infoCircle], "Welcome", "Join the server.", { checkPlayerJoin: function () { return true; }, notify: "nobody" }), diff --git a/src/achievements.ts b/src/achievements.ts index 4756ff60..f26f5b2d 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -201,7 +201,7 @@ export const Achievements = { //Joining based - welcome: new Achievement("_", "Welcome", "Join the server.", { + welcome: new Achievement(["gold", Iconc.infoCircle], "Welcome", "Join the server.", { checkPlayerJoin: () => true, notify: "nobody" }), From 5349e958ffa4b96efb82c67bc68c32f83f54e6f9 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:28:48 +0530 Subject: [PATCH 31/33] Update and bugfix /achievementgrid --- build/scripts/commands/general.js | 26 ++++++++++++++------ src/commands/general.ts | 41 +++++++++++++++++-------------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/build/scripts/commands/general.js b/build/scripts/commands/general.js index c683bf29..4d7b2392 100644 --- a/build/scripts/commands/general.js +++ b/build/scripts/commands/general.js @@ -1247,14 +1247,14 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { perm: commands_1.Perm.none, handler: function (_a) { return __awaiter(this, arguments, void 0, function (_b) { - var visibleAchievements, options, numberAchievements, totalAchievements, x, y, a; + var visibleAchievements, options, numberAchievements, totalAchievements, x, y, a, err_1; var _c; var sender = _b.sender, _d = _b.args.target, target = _d === void 0 ? sender : _d, f = _b.f; return __generator(this, function (_e) { switch (_e.label) { case 0: visibleAchievements = achievements_1.Achievement.all.filter(function (a) { return !a.hidden || a.has(target); }); - options = (0, funcs_1.to2DArray)(visibleAchievements, 6).map(function (row) { return row.map(function (a) { return ({ + options = (0, funcs_1.to2DArray)(visibleAchievements, 7).map(function (row) { return row.map(function (a) { return ({ data: a, text: a.has(target) ? a.icon : "[gray]".concat(Strings.stripColors(a.icon)), }); }); }); @@ -1264,17 +1264,29 @@ exports.commands = (0, commands_1.commandList)(__assign(__assign({ about: { a = null; _e.label = 1; case 1: - if (!true) return [3 /*break*/, 3]; - return [4 /*yield*/, menus_1.Menu.scroll2D(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", ""], ["\\\n", " ", "\n\n", "\n\nAllowed modes: ", "\nUnlocked: ", "\n", "\\\n"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : + if (!true) return [3 /*break*/, 6]; + _e.label = 2; + case 2: + _e.trys.push([2, 4, , 5]); + return [4 /*yield*/, menus_1.Menu.scroll2D(sender, "Achievements", a ? config_1.FColor.achievement(templateObject_23 || (templateObject_23 = __makeTemplateObject(["\t", " ", "\n\t\n\t", "\n\t\n\tAllowed modes: ", "\n\tUnlocked: ", "\n\t", "\t"], ["\\\n\t", " ", "\n\t\n\t", "\n\t\n\tAllowed modes: ", "\n\tUnlocked: ", "\n\t", "\\\n\t"])), a.icon, a.name, a.description + (a.extendedDescription ? ("\n" + "[gray]".concat(a.extendedDescription)) : ""), a.modesText, f.boolGood(a.has(target)), a.hidden ? "This achievement is secret." : "") : (target == sender ? "You have ".concat(numberAchievements, "/").concat(totalAchievements, " achievements.") : config_1.FColor.achievement(templateObject_24 || (templateObject_24 = __makeTemplateObject(["Player ", " has ", "/", " achievements."], ["Player ", " has ", "/", " achievements."])), target.prefixedName, numberAchievements, totalAchievements)) - + "\nClick an achievement icon to show more information.", options, { onCancel: "ignore", columns: 4, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; - case 2: + + "\nClick an achievement icon to show more information.", options, { onCancel: "reject", columns: 5, rows: 4, getCenterText: function () { return String.fromCharCode(Iconc.settings); }, x: x, y: y })]; + case 3: _c = __read.apply(void 0, [_e.sent(), 3]), a = _c[0], x = _c[1], y = _c[2]; + return [3 /*break*/, 5]; + case 4: + err_1 = _e.sent(); + if (err_1 == "cancel") + return [2 /*return*/]; //TODO replace this string "cancel" with a symbol + else + throw err_1; + return [3 /*break*/, 5]; + case 5: if (a == achievements_1.Achievements.click_me && target == sender) a.grantTo(sender); return [3 /*break*/, 1]; - case 3: return [2 /*return*/]; + case 6: return [2 /*return*/]; } }); }); diff --git a/src/commands/general.ts b/src/commands/general.ts index 4cc1e1b0..b5e89f8f 100644 --- a/src/commands/general.ts +++ b/src/commands/general.ts @@ -1196,7 +1196,7 @@ ${a.hidden ? "This achievement is secret." : ""}\ perm: Perm.none, async handler({sender, args: { target = sender }, f}){ const visibleAchievements = Achievement.all.filter(a => !a.hidden || a.has(target)); - const options = to2DArray(visibleAchievements, 6).map(row => row.map(a => ({ + const options = to2DArray(visibleAchievements, 7).map(row => row.map(a => ({ data: a, text: a.has(target) ? a.icon : `[gray]${Strings.stripColors(a.icon)}`, }))); @@ -1205,23 +1205,28 @@ ${a.hidden ? "This achievement is secret." : ""}\ let x = 0, y = 0; let a: Achievement | null = null; while(true){ - [a, x, y] = await Menu.scroll2D( - sender, "Achievements", - a ? FColor.achievement`\ -${a.icon} ${a.name} - -${a.description + (a.extendedDescription ? ("\n" + `[gray]${a.extendedDescription}`) : "")} - -Allowed modes: ${a.modesText} -Unlocked: ${f.boolGood(a.has(target))} -${a.hidden ? "This achievement is secret." : ""}\ -` : - (target == sender ? `You have ${numberAchievements}/${totalAchievements} achievements.` - : FColor.achievement`Player ${target.prefixedName} has ${numberAchievements}/${totalAchievements} achievements.`) - + "\nClick an achievement icon to show more information.", - options, - { onCancel: "ignore", columns: 4, rows: 4, getCenterText: () => String.fromCharCode(Iconc.settings), x, y } - ); + try { + [a, x, y] = await Menu.scroll2D( + sender, "Achievements", + a ? FColor.achievement`\ + ${a.icon} ${a.name} + + ${a.description + (a.extendedDescription ? ("\n" + `[gray]${a.extendedDescription}`) : "")} + + Allowed modes: ${a.modesText} + Unlocked: ${f.boolGood(a.has(target))} + ${a.hidden ? "This achievement is secret." : ""}\ + ` : + (target == sender ? `You have ${numberAchievements}/${totalAchievements} achievements.` + : FColor.achievement`Player ${target.prefixedName} has ${numberAchievements}/${totalAchievements} achievements.`) + + "\nClick an achievement icon to show more information.", + options, + { onCancel: "reject", columns: 5, rows: 4, getCenterText: () => String.fromCharCode(Iconc.settings), x, y } + ); + } catch(err){ + if(err == "cancel") return; //TODO replace this string "cancel" with a symbol + else throw err; + } if(a == Achievements.click_me && target == sender) a.grantTo(sender); } } From f57c1c46b49dd280279d3536fb32ccf5c9a9c842 Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:47:07 -0500 Subject: [PATCH 32/33] change "until in forever" to "forever" --- build/scripts/commands/console.js | 4 +++- src/commands/console.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build/scripts/commands/console.js b/build/scripts/commands/console.js index d12f4a7f..540774bc 100644 --- a/build/scripts/commands/console.js +++ b/build/scripts/commands/console.js @@ -161,7 +161,9 @@ exports.commands = (0, commands_1.consoleCommandList)({ var outputString = [""]; var _loop_1 = function (playerInfo, fishP) { var flagsText = [ - (fishP === null || fishP === void 0 ? void 0 : fishP.marked()) && "&lris marked&fr until ".concat((0, utils_1.formatTimeRelative)(fishP.unmarkTime)), + (fishP === null || fishP === void 0 ? void 0 : fishP.marked()) && (globals_1.maxTime - fishP.unmarkTime < 20000 ? + "&lris marked forever&fr" + : "&lris marked&fr until ".concat((0, utils_1.formatTimeRelative)(fishP.unmarkTime))), (fishP === null || fishP === void 0 ? void 0 : fishP.muted) && "&lris muted&fr", (fishP === null || fishP === void 0 ? void 0 : fishP.hasFlag("member")) && "&lmis member&fr", (fishP === null || fishP === void 0 ? void 0 : fishP.autoflagged) && "&lris autoflagged&fr", diff --git a/src/commands/console.ts b/src/commands/console.ts index b5521c0a..752ebe3b 100644 --- a/src/commands/console.ts +++ b/src/commands/console.ts @@ -91,7 +91,9 @@ export const commands = consoleCommandList({ const outputString:string[] = [""]; for(const [playerInfo, fishP] of infoList){ const flagsText = [ - fishP?.marked() && `&lris marked&fr until ${formatTimeRelative(fishP.unmarkTime)}`, + fishP?.marked() && (maxTime - fishP.unmarkTime < 20_000 ? + `&lris marked forever&fr` + : `&lris marked&fr until ${formatTimeRelative(fishP.unmarkTime)}`), fishP?.muted && "&lris muted&fr", fishP?.hasFlag("member") && "&lmis member&fr", fishP?.autoflagged && "&lris autoflagged&fr", From 9d2f7d2072cbcf7016c57335ba1f06f301f3773b Mon Sep 17 00:00:00 2001 From: BalaM314 <71201189+BalaM314@users.noreply.github.com> Date: Sat, 31 Jan 2026 17:27:12 -0500 Subject: [PATCH 33/33] Impl core low hp and enemy cor low hp --- build/scripts/achievements.js | 38 +++++++++++++++++++++++++++++------ src/achievements.ts | 34 +++++++++++++++++++++++++------ src/mindustryTypes.ts | 5 +++++ 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/build/scripts/achievements.js b/build/scripts/achievements.js index 5cb72516..59f70ef1 100644 --- a/build/scripts/achievements.js +++ b/build/scripts/achievements.js @@ -572,14 +572,12 @@ exports.Achievements = { modes: ["not", "sandbox"], disabled: true }), //TODO - core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { + core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 50 health, but survive.", { modes: ["not", "sandbox"], - disabled: true - }), //TODO - enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { + }), + enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 50 health, but survive.", { modes: ["not", "sandbox"], - disabled: true - }), //TODO + }), verified: new Achievement([ranks_1.Rank.active.color, Iconc.ok], "Verified", "Be promoted automatically to ".concat(ranks_1.Rank.active.coloredName(), " rank."), { checkPlayerJoin: function (p) { return p.ranksAtLeast("active"); }, notify: "nobody" }), @@ -694,6 +692,34 @@ Timer.schedule(function () { else isAlone = 0; }, 2, 2); +var coreHealthTime = new ObjectIntMap(); +if (!config_1.Gamemode.sandbox()) + Timer.schedule(function () { + coreHealthTime.forEach(function (_a) { + var core = _a.key, value = _a.value; + if (Date.now() > value) { + if (core.dead) { + coreHealthTime.remove(core); + } + else if (core.health > 50) { + //grant achievement + players_1.FishPlayer.forEachPlayer(function (p) { + if (core.team == p.team()) + exports.Achievements.core_low_hp.grantTo(p); + else + exports.Achievements.enemy_core_low_hp.grantTo(p); + }); + coreHealthTime.remove(core); + } + } + }); + Vars.state.teams.active.flatMap(function (t) { return t.cores; }).each(function (core) { + if (core.health < 50 && !coreHealthTime.get(core)) + coreHealthTime.put(core, Date.now() + 12000); + }); + }, 1, 1); +Events.on(EventType.GameOverEvent, function () { return coreHealthTime.clear(); }); +Events.on(EventType.WorldLoadEvent, function () { return coreHealthTime.clear(); }); globals_1.FishEvents.on("scriptKiddie", function (_, p) { return Timer.schedule(function () { return exports.Achievements.script_kiddie.grantTo(p); }, 2); }); globals_1.FishEvents.on("memoryCorruption", function () { return exports.Achievements.memory_corruption.grantToAllOnline(); }); globals_1.FishEvents.on("serverSays", function () { return exports.Achievements.server_speak.grantToAllOnline(); }); diff --git a/src/achievements.ts b/src/achievements.ts index f26f5b2d..dcc5b28d 100644 --- a/src/achievements.ts +++ b/src/achievements.ts @@ -462,14 +462,12 @@ export const Achievements = { modes: ["not", "sandbox"], disabled: true }), //TODO - core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 1% health, but survive.", { + core_low_hp: new Achievement(["yellow", Blocks.coreNucleus.emoji()], "Close Call", "Have your core reach less than 50 health, but survive.", { modes: ["not", "sandbox"], - disabled: true - }), //TODO - enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 1% health, but survive.", { + }), + enemy_core_low_hp: new Achievement(["red", Blocks.coreNucleus.emoji()], "So Close", "Cause the enemy core to reach less than 50 health, but survive.", { modes: ["not", "sandbox"], - disabled: true - }), //TODO + }), verified: new Achievement([Rank.active.color, Iconc.ok], "Verified", `Be promoted automatically to ${Rank.active.coloredName()} rank.`, { checkPlayerJoin: p => p.ranksAtLeast("active"), notify: "nobody" }), @@ -556,6 +554,7 @@ Events.on(EventType.UnitBulletDestroyEvent, ({unit, bullet}:{unit:Unit; bullet: let siliconReached = Team.all.map(_ => false); Events.on(EventType.GameOverEvent, () => siliconReached = Team.all.map(_ => false)); let isAlone = 0; + Timer.schedule(() => { if(!Vars.state.gameOver && !Gamemode.sandbox()){ Vars.state.teams.active.each(({team}) => { @@ -570,6 +569,29 @@ Timer.schedule(() => { } else isAlone = 0; }, 2, 2); +const coreHealthTime = new ObjectIntMap(); +if(!Gamemode.sandbox()) Timer.schedule(() => { + coreHealthTime.forEach(({key: core, value}) => { + if(Date.now() > value){ + if(core.dead){ + coreHealthTime.remove(core); + } else if(core.health > 50){ + //grant achievement + FishPlayer.forEachPlayer(p => { + if(core.team == p.team()) Achievements.core_low_hp.grantTo(p); + else Achievements.enemy_core_low_hp.grantTo(p); + }); + coreHealthTime.remove(core); + } + } + }); + Vars.state.teams.active.flatMap(t => t.cores).each(core => { + if(core.health < 50 && !coreHealthTime.get(core)) coreHealthTime.put(core, Date.now() + 12_000); + }); +}, 1, 1); +Events.on(EventType.GameOverEvent, () => coreHealthTime.clear()); +Events.on(EventType.WorldLoadEvent, () => coreHealthTime.clear()); + FishEvents.on("scriptKiddie", (_, p) => Timer.schedule(() => Achievements.script_kiddie.grantTo(p), 2)); FishEvents.on("memoryCorruption", () => Achievements.memory_corruption.grantToAllOnline()); diff --git a/src/mindustryTypes.ts b/src/mindustryTypes.ts index fc0b50d5..bf60edf7 100644 --- a/src/mindustryTypes.ts +++ b/src/mindustryTypes.ts @@ -199,9 +199,11 @@ class Building { team: Team; changeTeam: Team; enabled: boolean; + health: number; ammo?: Seq<{item: Item}>; warmup?: number; storageCapacity?: number; + dead: boolean; timeScale(): number; kill():void; tileX():number; @@ -496,6 +498,7 @@ class Seq { each(pred:(item:T) => boolean, func:(item:T) => unknown):void; isEmpty():boolean; map(mapFunc:(item:T) => R):Seq; + flatMap(mapFunc:(item:T) => Seq):Seq; toString(separator?:string, stringifier?:(item:T) => string):string; toArray():T[]; copy():Seq; @@ -537,7 +540,9 @@ class ObjectIntMap { get(key:K):number; increment(key:K):void; clear():void; + remove(key:K):number | null; size:number; + forEach(func:(_:{key:K, value:number}) => void):void; entries(): { toArray():Seq>; };