From 7114bd4825c8acd7fe8b404592cfe0d8df985af2 Mon Sep 17 00:00:00 2001 From: bnm5g Date: Mon, 20 Jul 2026 09:48:38 +0530 Subject: [PATCH 1/7] finally learnt branches are a thing --- package-lock.json | 235 +++++++++++++++++++++++++++++++++ src/ps/commands/games/core.tsx | 51 +++++++ src/ps/games/types.ts | 14 ++ src/ps/index.ts | 2 +- 4 files changed, 301 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index fe9d904e..308bdd3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -544,6 +544,26 @@ "@parcel/watcher-win32-x64": "2.5.1" } }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", @@ -563,6 +583,138 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/watcher-linux-x64-glibc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", @@ -583,6 +735,89 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", diff --git a/src/ps/commands/games/core.tsx b/src/ps/commands/games/core.tsx index 9fb379ea..a999e674 100644 --- a/src/ps/commands/games/core.tsx +++ b/src/ps/commands/games/core.tsx @@ -11,10 +11,50 @@ import { toId } from '@/utils/toId'; import type { NoTranslate, TranslationFn } from '@/i18n/types'; import type { CommonGame } from '@/ps/games/game'; import type { BaseModEntry } from '@/ps/games/mods'; +import type { HTPDropdown, GameHTPData } from '@/ps/games/types'; import type { PSCommand, PSCommandChild } from '@/types/chat'; import type { Room } from 'ps-client'; import type { HTMLopts } from 'ps-client/classes/common'; +const HTPSection = ({ section }: { section: HTPDropdown }) => { + return ( +
+ + {section.title} + +
+ {section.lines.map((line, idx) => ( +
{line}
+ ))} + {section.subsections?.map((sub, idx) => ( + + ))} +
+
+ ); +}; + +const HowToPlayBox = ({ gameName, data }: { gameName: string; data: GameHTPData }) => { + return ( +
+
+ Goal: {data.goal} +
+ + {data.sections.map((sec, idx) => ( + + ))} +
+ ); +}; + type SearchContext = | { action: 'start'; user: string } | { action: 'join'; user: string } @@ -223,6 +263,17 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): } }, }, + help: { + name: 'help', + aliases: ['htp', 'howtoplay'], + help: 'Shows the how-to-play for this game.', + syntax: 'CMD', + run: ({ message, $T }) => { + const htpData = Game.meta.htp; + if (!htpData) throw new ChatError($T('GAME.NO_HTP')); + return message.reply(); + }, + }, ...conditionalCommand( Game.meta.autostart === false, { diff --git a/src/ps/games/types.ts b/src/ps/games/types.ts index 08f4b74c..545ecaff 100644 --- a/src/ps/games/types.ts +++ b/src/ps/games/types.ts @@ -2,6 +2,17 @@ import type { TranslatedText } from '@/i18n/types'; import type { ModData, ModEnum } from '@/ps/games/mods'; import type { Satisfies } from '@/types/common'; +export interface HTPDropdown { + title: string; + lines: string[]; + subsections?: HTPDropdown[]; +} + +export interface GameHTPData { + goal: string; + sections: HTPDropdown[]; +} + export type Theme> = Partial>> = { id: string; name: string; @@ -33,6 +44,9 @@ export type Meta = Readonly< /** Enables `offerdraw`: pending offer expires after 1 minute or when any player makes a move. */ canOfferDraws?: boolean; + /** How to play instructions. */ + htp?: GameHTPData; + // UGO-CODE /** * Metadata for automatic UGO points. diff --git a/src/ps/index.ts b/src/ps/index.ts index 219cdad4..2db0ccca 100644 --- a/src/ps/index.ts +++ b/src/ps/index.ts @@ -8,7 +8,7 @@ import { transformHTML } from '@/ps/handlers/html'; import loadPS from '@/ps/loaders'; import { Logger } from '@/utils/logger'; -const PS = new Client({ username, password, rooms, transformHTML, avatar }); +const PS = new Client({ username, password, rooms, transformHTML, avatar, server: 'localhost:8000', serverProtocol: 'ws' }); PS.on('login', () => Logger.log(`Connected to PS! [${PS.status.username}]`)); if (IS_ENABLED.PS) loadPS(PS).then(() => PS.connect()); From 7a9659abf4ec81263e1113c5a3255faf59c71115 Mon Sep 17 00:00:00 2001 From: bnm5g Date: Fri, 7 Aug 2026 10:36:32 +0530 Subject: [PATCH 2/7] feat: add how-to-play commands for games --- package.json | 3 +- src/ps/commands/games/core.tsx | 37 +++++++++++++++--- src/ps/games/azul/meta.ts | 44 ++++++++++++++++++++++ src/ps/games/battleship/meta.ts | 23 ++++++++++++ src/ps/games/chess/meta.ts | 4 ++ src/ps/games/connectfour/meta.ts | 12 ++++++ src/ps/games/lightsout/meta.ts | 14 +++++++ src/ps/games/mastermind/meta.ts | 25 +++++++++++++ src/ps/games/othello/meta.ts | 28 ++++++++++++++ src/ps/games/scrabble/meta.ts | 55 +++++++++++++++++++++++++++ src/ps/games/snakesladders/meta.ts | 14 +++++++ src/ps/games/splendor/meta.ts | 60 ++++++++++++++++++++++++++++++ src/ps/index.ts | 2 +- 13 files changed, 312 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 0aecdf0c..43c04d8f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "secrets-check": "sh scripts/secrets_check.sh", "prettify": "prettier -w", "prepare": "sh ./scripts/setup.sh", - "prestart": "bash ./scripts/prestart.sh", "start": "ts-node --swc src/index.ts", "tailwind": "npx @tailwindcss/cli -i ./src/web/styles/tailwind.css -o ./src/web/react/compiled/styles.css", "test": "npm run lint && npm run tsc", @@ -72,4 +71,4 @@ "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", "lightningcss-linux-x64-gnu": "^1.29.1" } -} +} \ No newline at end of file diff --git a/src/ps/commands/games/core.tsx b/src/ps/commands/games/core.tsx index a999e674..9555ee4a 100644 --- a/src/ps/commands/games/core.tsx +++ b/src/ps/commands/games/core.tsx @@ -8,7 +8,7 @@ import { fromHumanTime, toHumanTime } from '@/utils/humanTime'; import { Timer } from '@/utils/timer'; import { toId } from '@/utils/toId'; -import type { NoTranslate, TranslationFn } from '@/i18n/types'; +import type { NoTranslate, TranslationFn, ToTranslate } from '@/i18n/types'; import type { CommonGame } from '@/ps/games/game'; import type { BaseModEntry } from '@/ps/games/mods'; import type { HTPDropdown, GameHTPData } from '@/ps/games/types'; @@ -16,6 +16,12 @@ import type { PSCommand, PSCommandChild } from '@/types/chat'; import type { Room } from 'ps-client'; import type { HTMLopts } from 'ps-client/classes/common'; + +import { Button, Username } from '@/utils/components/ps'; +import { prefix } from '@/config/ps'; +import type { ReactElement } from 'react'; +import { groupSub } from '@/utils/groupSub'; + const HTPSection = ({ section }: { section: HTPDropdown }) => { return (
@@ -192,8 +198,26 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): }, help: `Game module for ${Game.meta.name}. See subcommands.`, syntax: 'CMD', - async run({ run, command }) { - return run(`help ${command.join(' ')}`); + async run({ run, command, calledFrom, broadcastHTML, $T, message }) { + const sendHTML = (HTML: ReactElement) => { + if (calledFrom && calledFrom.at(-1) !== 'help') return message.replyHTML(HTML); + else return broadcastHTML(HTML); + }; + const Bot = message.parent; + return sendHTML( +
+

+ {`Hi, to make a game of ${gameId}, you ${Game.meta.players === 'single' ? `can use ,${gameId} create! ` : 'need to ask a staff member!'}`} +

+

+ If you want to learn how to play, check{' '} + {' '} + out! +

+
+ ); }, categories: ['game'], children: { @@ -267,11 +291,12 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): name: 'help', aliases: ['htp', 'howtoplay'], help: 'Shows the how-to-play for this game.', + flags: { allowPMs: true }, syntax: 'CMD', - run: ({ message, $T }) => { + async run({ message, arg }) { const htpData = Game.meta.htp; - if (!htpData) throw new ChatError($T('GAME.NO_HTP')); - return message.reply(); + if (!htpData) throw new ChatError('GAME.NO_DATA' as ToTranslate); + return message.replyHTML(); }, }, ...conditionalCommand( diff --git a/src/ps/games/azul/meta.ts b/src/ps/games/azul/meta.ts index 55369368..7be922c4 100644 --- a/src/ps/games/azul/meta.ts +++ b/src/ps/games/azul/meta.ts @@ -24,6 +24,50 @@ export const meta: Meta = { pokeTimer: fromHumanTime('30 sec'), timer: fromHumanTime('45 sec'), + htp: { + goal: 'Be the first to get 7 points.', + sections: [ + { + title: 'Setup', + lines: [ + '- The game is played over several rounds, each consisting of two phases: drafting tiles and then scoring walls.', + '- Game begins with 4 tiles placed in each factory.', + ] + }, + { + title: 'Drafting Phase', + lines: [''], + subsections: [ + { + title: 'Picking tiles', + lines: [ + '- Choose a tile from a factory to take all tiles of the same type from it.', + '- The remaining tiles from that factory are moved to the waste.', + '- The first to pick from the waste must also take the start marker, giving them the first turn in the next round.' + ], + }, + { + title: 'Placing tiles', + lines: [ + '- You can only place the tiles in one of the rows on your player board.', + '- You must place all tiles of the same color in a row.', + '- Any overflow of tiles will be moved to the penalty line.', + ] + } + ], + }, + { + title: 'Scoring', + lines: [ + '- Happens when there are no tiles left in factories.', + '- For each filled row, place one tile on the wall in the corresponding row and type.', + '- Every row/column of tiles gives you points equal to the number of tiles in the row/column.', + '- Every filled row gives 2 points, every column 7, every type 10.', + '- The game ends when a row is filled.' + ] + }, + ], + }, // UGO-CODE ugo: { cap: 12, diff --git a/src/ps/games/battleship/meta.ts b/src/ps/games/battleship/meta.ts index bb2d1b6e..1d7c656b 100644 --- a/src/ps/games/battleship/meta.ts +++ b/src/ps/games/battleship/meta.ts @@ -19,6 +19,29 @@ export const meta: Meta = { pokeTimer: fromHumanTime('30 sec'), timer: fromHumanTime('1 min'), + htp: { + goal: "Sink all of your opponent's ships before they sink yours!", + sections: [ + { + title: 'Setup', + lines: [ + '- 5 ships each to be placed on a 10x10 grid.', + '- They are: (C)arrier (5), (B)attleship (4), (D)estroyer (3), (S)ubmarine (3), and (P)atrol (2).', + '- Ships can be placed horizontally or vertically, but not diagonally.', + '- Ships cannot overlap, touch each other, or go out of bounds.', + ] + }, + { + title: 'Gameplay', + lines: [ + '- Players take turns shooting at coordinates on their opponent\'s board.', + '- If a ship is hit, it is marked as hit.', + '- If all ships on one side are sunk, that player loses, and the game ends.', + ] + } + ], + }, + // UGO-CODE ugo: { cap: 12, diff --git a/src/ps/games/chess/meta.ts b/src/ps/games/chess/meta.ts index adfb0b21..05a47dc4 100644 --- a/src/ps/games/chess/meta.ts +++ b/src/ps/games/chess/meta.ts @@ -23,6 +23,10 @@ export const meta: Meta = { themes: CHESS_THEMES, defaultTheme, + htp: { + goal: 'Checkmate the opponent\'s king!', + sections: [{ title: 'Check this guide out for basics!', lines: ['https://www.chess.com/learn-how-to-play-chess'] }] + }, // UGO-CODE ugo: { cap: 12, diff --git a/src/ps/games/connectfour/meta.ts b/src/ps/games/connectfour/meta.ts index 8e4571e8..a84ae06e 100644 --- a/src/ps/games/connectfour/meta.ts +++ b/src/ps/games/connectfour/meta.ts @@ -19,6 +19,18 @@ export const meta: Meta = { pokeTimer: fromHumanTime('30 sec'), timer: fromHumanTime('1 min'), + htp: { + goal: 'Get four of their colored discs in a row—horizontally, vertically, or diagonally.', + sections: [ + { + title: 'Gameplay', + lines: [ + '- Players take turns dropping one disc into one of the seven columns.', + '- Discs stack on top of each other in the selected column.' + ], + }, + ], + }, // UGO-CODE ugo: { cap: 12, diff --git a/src/ps/games/lightsout/meta.ts b/src/ps/games/lightsout/meta.ts index ae20f450..b5ddb5b5 100644 --- a/src/ps/games/lightsout/meta.ts +++ b/src/ps/games/lightsout/meta.ts @@ -9,6 +9,20 @@ export const meta: Meta = { abbr: 'lo', players: 'single', + htp: { + goal: 'Turn all the lights off and see if you can beat PartBot or not :p', + sections: [ + { + title: 'How to Play', + lines: [ + '- The game is played on a 5×5 grid of lights.', + '- When you click on a light, it toggles (turns on or off), including the adjacent lights (up, down, left, right).', + '- A helpful guide: https://www.logicgamesonline.com/lightsout/tutorial.html' + ] + }, + ] + }, + // UGO-CODE ugo: null, }; diff --git a/src/ps/games/mastermind/meta.ts b/src/ps/games/mastermind/meta.ts index 681a6575..28204588 100644 --- a/src/ps/games/mastermind/meta.ts +++ b/src/ps/games/mastermind/meta.ts @@ -9,6 +9,31 @@ export const meta: Meta = { abbr: 'mm', players: 'single', + htp: { + goal: 'Guess the 4 digit code!', + sections: [ + { + title: 'How to Play', + lines: [ + '- Numbers go from 0-7, and they can repeat.', + '- Red pin - correct number correct position', + '- White pin - correct number wrong position', + '- No pin - incorrect number', + '- Reds/Whites don\'t specify that the number in the corresponding position is correct.', + ] + }, + { + title: 'Example', + lines: [ + 'Let\'s assume the code is 0167...', + '- Guess: 1234. Result: White. (1 is the only correct number but in the wrong position.)', + '- Guess: 2567. Result: Red-Red. (6,7 are correct numbers and correct positions.)', + '- Guess: 6723. Result: White-White. (6,7 are correct numbers but in the wrong positions.)', + '- Guess: 2345. Result: Nothing. (All numbers are incorrect.)', + ] + } + ] + }, // UGO-CODE ugo: null }; diff --git a/src/ps/games/othello/meta.ts b/src/ps/games/othello/meta.ts index 49a64bff..8f13ab95 100644 --- a/src/ps/games/othello/meta.ts +++ b/src/ps/games/othello/meta.ts @@ -18,6 +18,34 @@ export const meta: Meta = { pokeTimer: fromHumanTime('30 sec'), timer: fromHumanTime('1 min'), + htp: { + goal: 'Place discs on the board to surround and flip your opponent\'s discs.', + sections: [ + { + title: 'Setup', + lines: [ + 'Game begins with 4 discs placed in the center of the board with the same colored disks on opposite corners of the 2x2 central area.', + ] + }, + { + title: 'Gameplay', + lines: [ + '- Players take turns placing discs on the board.', + '- A valid move must outflank at least one of your opponent\'s discs vertically, horizontally, or diagonally.', + '- Outflank means to place a disc on the board so that your opponent\'s row (or rows) of disc(s) is bordered at each end by a disc of your colour. ', + '- When a disc is placed, it flips all of the opponent\'s discs that are in a straight line with the new disc.', + '- If a player does not have a valid move, then the turn passes to the other player.', + ] + }, + { + title: 'Win conditions', + lines: [ + '- The game ends when no more valid moves can be made.', + '- The player with the most discs at the end wins.', + ], + }, + ], + }, // UGO-CODE ugo: { cap: 12, diff --git a/src/ps/games/scrabble/meta.ts b/src/ps/games/scrabble/meta.ts index d939ea61..9df88729 100644 --- a/src/ps/games/scrabble/meta.ts +++ b/src/ps/games/scrabble/meta.ts @@ -4,6 +4,8 @@ import { GamesList } from '@/ps/games/types'; import { fromHumanTime } from '@/utils/humanTime'; import type { Meta } from '@/ps/games/types'; +import { setUGOPlayed } from '@/cache/ugo'; +import { SocketAddress } from 'net'; export const meta: Meta = { name: 'Scrabble', @@ -24,6 +26,59 @@ export const meta: Meta = { pokeTimer: fromHumanTime('1 min'), timer: fromHumanTime('5 min'), + htp: { + goal: 'Get the most points by placing letters on the board to form words.', + sections: [ + { + title: 'Setup', + lines: [ + '- Each player gets 7 letter from A-Z and blanks (represent any letter) to use', + '- First word must be at least 2 letters, covering center star square' + ] + }, + { + title: 'Gameplay', + lines: [ + '- Players take turn using letters to form words vertically or horizontally', + '- Every new word must connect to at least one letter already on the board', + '- All words formed by your play must be valid Scrabble words', + '- After turn end, draw new tiles to bring your hand back up to 7 tiles', + '- You may also choose to swap at least 2 letters instead of making a word, or pass the turn', + ], + }, + { + title: 'Scores', + lines: [ + '- Points are based on letter values + board multipliers', + '- Playing all 7 letters in one turn gives bonus 50 points', + ], + subsections: [ + { + title: 'Letter values:', + lines: [ + 'A=1, B=3, C=3, D=2, E=1, F=4, G=2, H=4, I=1, J=8, K=5, L=1, M=3, N=1, O=1, P=3, Q=10, R=1, S=1, T=1, U=1, V=4, W=4, X=8, Y=4, Z=10' + ] + }, + { + title: 'Board multipliers:', + lines: [ + '- Double letter score (DLS): Double the value of one letter', + '- Triple letter score (TLS): Triple the value of one letter', + '- Double word score (DWS): Double the score of the entire word', + '- Triple word score (TWS): Triple the score of the entire word', + ] + } + ] + }, + { + title: 'Win conditions', + lines: [ + '- Game ends when all tiles are drawn and one player empties their rack, or when no player can make a move', + '- Highest score wins', + ] + } + ], + }, // UGO-CODE ugo: { cap: 6, diff --git a/src/ps/games/snakesladders/meta.ts b/src/ps/games/snakesladders/meta.ts index 4cfdc11a..440f61e7 100644 --- a/src/ps/games/snakesladders/meta.ts +++ b/src/ps/games/snakesladders/meta.ts @@ -17,6 +17,20 @@ export const meta: Meta = { pokeTimer: fromHumanTime('30 sec'), timer: fromHumanTime('45 sec'), + htp: { + goal: 'Be the first to reach 100 to win.', + sections: [ + { + title: 'Gameplay', + lines: [ + '- Each player takes turns rolling a six-sided die to move their token along the board', + '- Players move their token forward by the number of spaces indicated by the die roll', + '- If a player lands on a snake, they move their token backward by the number of spaces indicated by the snake', + '- If a player lands on a ladder, they move their token forward by the number of spaces indicated by the ladder', + ], + }, + ], + }, // UGO-CODE ugo: { cap: 12, diff --git a/src/ps/games/splendor/meta.ts b/src/ps/games/splendor/meta.ts index d5c10fbb..338c4fa1 100644 --- a/src/ps/games/splendor/meta.ts +++ b/src/ps/games/splendor/meta.ts @@ -15,6 +15,66 @@ export const meta: Meta = { pokeTimer: fromHumanTime('1 min'), timer: fromHumanTime('2 min'), + htp: { + goal: 'Be the first to get 15 points', + sections: [ + { + title: 'Cards', + lines: [ + 'Each Card consists of 3 components:', + '- Cost (bottom-left): The amount of Gems needed to buy it.', + '- Type (top-right): Each Card acts as a permanent gem of the Type indicated.', + '- Point (top-left): The value each Card is worth.' + ] + }, + { + title: 'Gameplay', + lines: ['On your turn choose 1 action:'], + subsections: [ + { + title: 'Pick up Gems', + lines: [ + '- Take 3 different OR 2 of the same color (only if >=4 of that color available).', + '- Max 10 Gems can be held at a time.', + '- If >10, choose which to return.' + ] + }, + { + title: 'Buy a Card', + lines: [ + '- Pay Gems equal to the cost to make it yours.', + '- No game limit on number of Cards buyable.' + ] + }, + { + title: 'Reserve a Card', + lines: [ + '- Take a card so only you can buy it.', + '- Also gain a Dragon gem (if possible).', + '- Can reserve up to 3 cards only.', + '- Cannot win if you have an unbought reserved card.' + ] + } + ] + }, + { + title: 'Trainers', + lines: [ + 'Each Trainer card consists of 2 components:', + '- Point (top-right): The value each Trainer is worth', + '- Requirement (bottom-left): Unlike normal Cards, these requires having Cards (not Gems) of that specific type. Additionally, you do not have to pay anything to get them: they come when you fulfill the reqeuirements' + ] + }, + { + title: 'Win conditions', + lines: [ + '- Once a player reaches 15 points, everyone after them in turn order gets one last turn.', + '- If points are tied, the one with fewer Cards wins.', + '- If still tied, everyone wins!!!!' + ] + } + ] + }, // UGO-CODE ugo: { cap: 8, diff --git a/src/ps/index.ts b/src/ps/index.ts index 2db0ccca..219cdad4 100644 --- a/src/ps/index.ts +++ b/src/ps/index.ts @@ -8,7 +8,7 @@ import { transformHTML } from '@/ps/handlers/html'; import loadPS from '@/ps/loaders'; import { Logger } from '@/utils/logger'; -const PS = new Client({ username, password, rooms, transformHTML, avatar, server: 'localhost:8000', serverProtocol: 'ws' }); +const PS = new Client({ username, password, rooms, transformHTML, avatar }); PS.on('login', () => Logger.log(`Connected to PS! [${PS.status.username}]`)); if (IS_ENABLED.PS) loadPS(PS).then(() => PS.connect()); From 457e5c7293e1386e3810e109b42212d3b7c57fb3 Mon Sep 17 00:00:00 2001 From: PartMan Date: Fri, 7 Aug 2026 19:25:48 +0530 Subject: [PATCH 3/7] fix: resolve lint errors in how-to-play meta files Remove unused scrabble imports and shorten lines exceeding max-len. --- src/ps/games/othello/meta.ts | 17 +++++++---------- src/ps/games/scrabble/meta.ts | 20 +++++++++----------- src/ps/games/splendor/meta.ts | 34 ++++++++++++++++------------------ 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/ps/games/othello/meta.ts b/src/ps/games/othello/meta.ts index 8f13ab95..91ade3b5 100644 --- a/src/ps/games/othello/meta.ts +++ b/src/ps/games/othello/meta.ts @@ -19,30 +19,27 @@ export const meta: Meta = { timer: fromHumanTime('1 min'), htp: { - goal: 'Place discs on the board to surround and flip your opponent\'s discs.', + goal: "Place discs on the board to surround and flip your opponent's discs.", sections: [ { title: 'Setup', lines: [ 'Game begins with 4 discs placed in the center of the board with the same colored disks on opposite corners of the 2x2 central area.', - ] + ], }, { title: 'Gameplay', lines: [ '- Players take turns placing discs on the board.', - '- A valid move must outflank at least one of your opponent\'s discs vertically, horizontally, or diagonally.', - '- Outflank means to place a disc on the board so that your opponent\'s row (or rows) of disc(s) is bordered at each end by a disc of your colour. ', - '- When a disc is placed, it flips all of the opponent\'s discs that are in a straight line with the new disc.', + "- A valid move must outflank at least one of your opponent's discs vertically, horizontally, or diagonally.", + "- Outflank means to place a disc so your opponent's row (or rows) of disc(s) is bordered at each end by your colour.", + "- When a disc is placed, it flips all of the opponent's discs that are in a straight line with the new disc.", '- If a player does not have a valid move, then the turn passes to the other player.', - ] + ], }, { title: 'Win conditions', - lines: [ - '- The game ends when no more valid moves can be made.', - '- The player with the most discs at the end wins.', - ], + lines: ['- The game ends when no more valid moves can be made.', '- The player with the most discs at the end wins.'], }, ], }, diff --git a/src/ps/games/scrabble/meta.ts b/src/ps/games/scrabble/meta.ts index 9df88729..fbfe3965 100644 --- a/src/ps/games/scrabble/meta.ts +++ b/src/ps/games/scrabble/meta.ts @@ -4,8 +4,6 @@ import { GamesList } from '@/ps/games/types'; import { fromHumanTime } from '@/utils/humanTime'; import type { Meta } from '@/ps/games/types'; -import { setUGOPlayed } from '@/cache/ugo'; -import { SocketAddress } from 'net'; export const meta: Meta = { name: 'Scrabble', @@ -33,8 +31,8 @@ export const meta: Meta = { title: 'Setup', lines: [ '- Each player gets 7 letter from A-Z and blanks (represent any letter) to use', - '- First word must be at least 2 letters, covering center star square' - ] + '- First word must be at least 2 letters, covering center star square', + ], }, { title: 'Gameplay', @@ -56,8 +54,8 @@ export const meta: Meta = { { title: 'Letter values:', lines: [ - 'A=1, B=3, C=3, D=2, E=1, F=4, G=2, H=4, I=1, J=8, K=5, L=1, M=3, N=1, O=1, P=3, Q=10, R=1, S=1, T=1, U=1, V=4, W=4, X=8, Y=4, Z=10' - ] + 'A=1, B=3, C=3, D=2, E=1, F=4, G=2, H=4, I=1, J=8, K=5, L=1, M=3, N=1, O=1, P=3, Q=10, R=1, S=1, T=1, U=1, V=4, W=4, X=8, Y=4, Z=10', + ], }, { title: 'Board multipliers:', @@ -66,17 +64,17 @@ export const meta: Meta = { '- Triple letter score (TLS): Triple the value of one letter', '- Double word score (DWS): Double the score of the entire word', '- Triple word score (TWS): Triple the score of the entire word', - ] - } - ] + ], + }, + ], }, { title: 'Win conditions', lines: [ '- Game ends when all tiles are drawn and one player empties their rack, or when no player can make a move', '- Highest score wins', - ] - } + ], + }, ], }, // UGO-CODE diff --git a/src/ps/games/splendor/meta.ts b/src/ps/games/splendor/meta.ts index 338c4fa1..f5a0a1ab 100644 --- a/src/ps/games/splendor/meta.ts +++ b/src/ps/games/splendor/meta.ts @@ -24,8 +24,8 @@ export const meta: Meta = { 'Each Card consists of 3 components:', '- Cost (bottom-left): The amount of Gems needed to buy it.', '- Type (top-right): Each Card acts as a permanent gem of the Type indicated.', - '- Point (top-left): The value each Card is worth.' - ] + '- Point (top-left): The value each Card is worth.', + ], }, { title: 'Gameplay', @@ -36,15 +36,12 @@ export const meta: Meta = { lines: [ '- Take 3 different OR 2 of the same color (only if >=4 of that color available).', '- Max 10 Gems can be held at a time.', - '- If >10, choose which to return.' - ] + '- If >10, choose which to return.', + ], }, { title: 'Buy a Card', - lines: [ - '- Pay Gems equal to the cost to make it yours.', - '- No game limit on number of Cards buyable.' - ] + lines: ['- Pay Gems equal to the cost to make it yours.', '- No game limit on number of Cards buyable.'], }, { title: 'Reserve a Card', @@ -52,28 +49,29 @@ export const meta: Meta = { '- Take a card so only you can buy it.', '- Also gain a Dragon gem (if possible).', '- Can reserve up to 3 cards only.', - '- Cannot win if you have an unbought reserved card.' - ] - } - ] + '- Cannot win if you have an unbought reserved card.', + ], + }, + ], }, { title: 'Trainers', lines: [ 'Each Trainer card consists of 2 components:', '- Point (top-right): The value each Trainer is worth', - '- Requirement (bottom-left): Unlike normal Cards, these requires having Cards (not Gems) of that specific type. Additionally, you do not have to pay anything to get them: they come when you fulfill the reqeuirements' - ] + '- Requirement (bottom-left): Requires having Cards (not Gems) of that specific type.', + '- No payment needed: Trainers are gained automatically when requirements are fulfilled.', + ], }, { title: 'Win conditions', lines: [ '- Once a player reaches 15 points, everyone after them in turn order gets one last turn.', '- If points are tied, the one with fewer Cards wins.', - '- If still tied, everyone wins!!!!' - ] - } - ] + '- If still tied, everyone wins!!!!', + ], + }, + ], }, // UGO-CODE ugo: { From cc4195e867dd091d035d7d78f2065bacc8fd2127 Mon Sep 17 00:00:00 2001 From: PartMan Date: Sat, 8 Aug 2026 11:13:28 +0530 Subject: [PATCH 4/7] chore: Corrections set 1 --- src/ps/games/azul/meta.ts | 16 ++++++++-------- src/ps/games/battleship/meta.ts | 10 +++++----- src/ps/games/lightsout/meta.ts | 10 +++++----- src/ps/games/mastermind/meta.ts | 16 ++++++++-------- src/ps/games/othello/meta.ts | 11 ++--------- src/ps/games/scrabble/meta.ts | 20 +++++++------------- src/ps/games/snakesladders/meta.ts | 8 ++++---- 7 files changed, 39 insertions(+), 52 deletions(-) diff --git a/src/ps/games/azul/meta.ts b/src/ps/games/azul/meta.ts index 7be922c4..5af7c52e 100644 --- a/src/ps/games/azul/meta.ts +++ b/src/ps/games/azul/meta.ts @@ -32,7 +32,7 @@ export const meta: Meta = { lines: [ '- The game is played over several rounds, each consisting of two phases: drafting tiles and then scoring walls.', '- Game begins with 4 tiles placed in each factory.', - ] + ], }, { title: 'Drafting Phase', @@ -43,7 +43,7 @@ export const meta: Meta = { lines: [ '- Choose a tile from a factory to take all tiles of the same type from it.', '- The remaining tiles from that factory are moved to the waste.', - '- The first to pick from the waste must also take the start marker, giving them the first turn in the next round.' + '- The first to pick from the waste must also take the waste marker, giving them a penalty but also the first turn in the next round.', ], }, { @@ -52,19 +52,19 @@ export const meta: Meta = { '- You can only place the tiles in one of the rows on your player board.', '- You must place all tiles of the same color in a row.', '- Any overflow of tiles will be moved to the penalty line.', - ] - } + ], + }, ], }, { title: 'Scoring', lines: [ - '- Happens when there are no tiles left in factories.', + '- Happens when there are no tiles left in factories/waste.', '- For each filled row, place one tile on the wall in the corresponding row and type.', '- Every row/column of tiles gives you points equal to the number of tiles in the row/column.', - '- Every filled row gives 2 points, every column 7, every type 10.', - '- The game ends when a row is filled.' - ] + '- After the game, each filled row gives 2 points, every column 7, every type 10.', + '- The game ends when a row is filled.', + ], }, ], }, diff --git a/src/ps/games/battleship/meta.ts b/src/ps/games/battleship/meta.ts index 1d7c656b..e78337ae 100644 --- a/src/ps/games/battleship/meta.ts +++ b/src/ps/games/battleship/meta.ts @@ -28,17 +28,17 @@ export const meta: Meta = { '- 5 ships each to be placed on a 10x10 grid.', '- They are: (C)arrier (5), (B)attleship (4), (D)estroyer (3), (S)ubmarine (3), and (P)atrol (2).', '- Ships can be placed horizontally or vertically, but not diagonally.', - '- Ships cannot overlap, touch each other, or go out of bounds.', - ] + '- Ships cannot overlap or go out of bounds.', + ], }, { title: 'Gameplay', lines: [ - '- Players take turns shooting at coordinates on their opponent\'s board.', + "- Players take turns shooting at coordinates on their opponent's board.", '- If a ship is hit, it is marked as hit.', '- If all ships on one side are sunk, that player loses, and the game ends.', - ] - } + ], + }, ], }, diff --git a/src/ps/games/lightsout/meta.ts b/src/ps/games/lightsout/meta.ts index b5ddb5b5..09597a7f 100644 --- a/src/ps/games/lightsout/meta.ts +++ b/src/ps/games/lightsout/meta.ts @@ -10,17 +10,17 @@ export const meta: Meta = { players: 'single', htp: { - goal: 'Turn all the lights off and see if you can beat PartBot or not :p', + goal: 'Turn all the lights off! Bonus points if you can beat PartBot :p', sections: [ { title: 'How to Play', lines: [ '- The game is played on a 5×5 grid of lights.', - '- When you click on a light, it toggles (turns on or off), including the adjacent lights (up, down, left, right).', - '- A helpful guide: https://www.logicgamesonline.com/lightsout/tutorial.html' - ] + '- When you click on a light, it toggles (turns on or off), including adjacent lights (up, down, left, right).', + '- A helpful guide: https://www.logicgamesonline.com/lightsout/tutorial.html', + ], }, - ] + ], }, // UGO-CODE diff --git a/src/ps/games/mastermind/meta.ts b/src/ps/games/mastermind/meta.ts index 28204588..4a77a2ad 100644 --- a/src/ps/games/mastermind/meta.ts +++ b/src/ps/games/mastermind/meta.ts @@ -10,7 +10,7 @@ export const meta: Meta = { players: 'single', htp: { - goal: 'Guess the 4 digit code!', + goal: 'Guess the 4 digit code before your guesses run out!', sections: [ { title: 'How to Play', @@ -19,21 +19,21 @@ export const meta: Meta = { '- Red pin - correct number correct position', '- White pin - correct number wrong position', '- No pin - incorrect number', - '- Reds/Whites don\'t specify that the number in the corresponding position is correct.', - ] + "- Reds/Whites don't specify that the number in the corresponding position is correct.", + ], }, { title: 'Example', lines: [ - 'Let\'s assume the code is 0167...', + "Let's assume the code is 0167...", '- Guess: 1234. Result: White. (1 is the only correct number but in the wrong position.)', '- Guess: 2567. Result: Red-Red. (6,7 are correct numbers and correct positions.)', '- Guess: 6723. Result: White-White. (6,7 are correct numbers but in the wrong positions.)', '- Guess: 2345. Result: Nothing. (All numbers are incorrect.)', - ] - } - ] + ], + }, + ], }, // UGO-CODE - ugo: null + ugo: null, }; diff --git a/src/ps/games/othello/meta.ts b/src/ps/games/othello/meta.ts index 91ade3b5..128ca717 100644 --- a/src/ps/games/othello/meta.ts +++ b/src/ps/games/othello/meta.ts @@ -21,19 +21,12 @@ export const meta: Meta = { htp: { goal: "Place discs on the board to surround and flip your opponent's discs.", sections: [ - { - title: 'Setup', - lines: [ - 'Game begins with 4 discs placed in the center of the board with the same colored disks on opposite corners of the 2x2 central area.', - ], - }, { title: 'Gameplay', lines: [ '- Players take turns placing discs on the board.', - "- A valid move must outflank at least one of your opponent's discs vertically, horizontally, or diagonally.", - "- Outflank means to place a disc so your opponent's row (or rows) of disc(s) is bordered at each end by your colour.", - "- When a disc is placed, it flips all of the opponent's discs that are in a straight line with the new disc.", + "- A valid move must outflank (or 'sandwich') at least one of your opponent's discs vertically, horizontally, or diagonally.", + "- When a disc is placed, it flips all of the opponent's discs that are newly outflanked.", '- If a player does not have a valid move, then the turn passes to the other player.', ], }, diff --git a/src/ps/games/scrabble/meta.ts b/src/ps/games/scrabble/meta.ts index fbfe3965..6021e9c8 100644 --- a/src/ps/games/scrabble/meta.ts +++ b/src/ps/games/scrabble/meta.ts @@ -30,8 +30,8 @@ export const meta: Meta = { { title: 'Setup', lines: [ - '- Each player gets 7 letter from A-Z and blanks (represent any letter) to use', - '- First word must be at least 2 letters, covering center star square', + '- Each player gets 7 letters (A-Z and blanks, which can represent any letter)', + '- The first word must cover the center (star) square', ], }, { @@ -41,7 +41,7 @@ export const meta: Meta = { '- Every new word must connect to at least one letter already on the board', '- All words formed by your play must be valid Scrabble words', '- After turn end, draw new tiles to bring your hand back up to 7 tiles', - '- You may also choose to swap at least 2 letters instead of making a word, or pass the turn', + '- You may also choose to swap letters with the bag instead of making a word, or pass the turn', ], }, { @@ -51,19 +51,13 @@ export const meta: Meta = { '- Playing all 7 letters in one turn gives bonus 50 points', ], subsections: [ - { - title: 'Letter values:', - lines: [ - 'A=1, B=3, C=3, D=2, E=1, F=4, G=2, H=4, I=1, J=8, K=5, L=1, M=3, N=1, O=1, P=3, Q=10, R=1, S=1, T=1, U=1, V=4, W=4, X=8, Y=4, Z=10', - ], - }, { title: 'Board multipliers:', lines: [ - '- Double letter score (DLS): Double the value of one letter', - '- Triple letter score (TLS): Triple the value of one letter', - '- Double word score (DWS): Double the score of the entire word', - '- Triple word score (TWS): Triple the score of the entire word', + '- Double letter score (light blue): Double the value of one letter', + '- Triple letter score (dark blue): Triple the value of one letter', + '- Double word score (pink): Double the score of the entire word', + '- Triple word score (red): Triple the score of the entire word', ], }, ], diff --git a/src/ps/games/snakesladders/meta.ts b/src/ps/games/snakesladders/meta.ts index 440f61e7..7a0da47f 100644 --- a/src/ps/games/snakesladders/meta.ts +++ b/src/ps/games/snakesladders/meta.ts @@ -23,10 +23,10 @@ export const meta: Meta = { { title: 'Gameplay', lines: [ - '- Each player takes turns rolling a six-sided die to move their token along the board', - '- Players move their token forward by the number of spaces indicated by the die roll', - '- If a player lands on a snake, they move their token backward by the number of spaces indicated by the snake', - '- If a player lands on a ladder, they move their token forward by the number of spaces indicated by the ladder', + '- Each player takes turns rolling a six-sided die to move their token along the board.', + '- Players move their token forward by the number of spaces indicated by the die roll.', + '- If a player lands on a snake, they move their token to the tail of the snake.', + '- If a player lands on a ladder, they move their token to the top of the ladder.', ], }, ], From 0aa721c80125c134641ce4da930ad9574cbccc24 Mon Sep 17 00:00:00 2001 From: PartMan Date: Sat, 8 Aug 2026 11:29:28 +0530 Subject: [PATCH 5/7] chore: HTP updates --- src/ps/commands/games/core.tsx | 64 +++++++++++-------- src/ps/games/azul/meta.ts | 4 +- src/ps/games/connectfour/meta.ts | 4 +- src/ps/games/othello/meta.ts | 23 +++++-- src/ps/games/splendor/meta.ts | 5 +- src/ps/games/types.ts | 10 ++- src/web/static/guides/othello/setup.png | Bin 0 -> 25132 bytes src/web/static/guides/othello/valid-move.gif | Bin 0 -> 26454 bytes 8 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 src/web/static/guides/othello/setup.png create mode 100644 src/web/static/guides/othello/valid-move.gif diff --git a/src/ps/commands/games/core.tsx b/src/ps/commands/games/core.tsx index 9555ee4a..0c262de5 100644 --- a/src/ps/commands/games/core.tsx +++ b/src/ps/commands/games/core.tsx @@ -1,55 +1,64 @@ import { PSGames } from '@/cache'; import { gameCache } from '@/cache/games'; +import { prefix } from '@/config/ps'; import { Games } from '@/ps/games'; import { renderBackups, renderMenu } from '@/ps/games/menus'; import { generateId } from '@/ps/games/utils'; import { ChatError } from '@/utils/chatError'; +import { Button } from '@/utils/components/ps'; import { fromHumanTime, toHumanTime } from '@/utils/humanTime'; import { Timer } from '@/utils/timer'; import { toId } from '@/utils/toId'; -import type { NoTranslate, TranslationFn, ToTranslate } from '@/i18n/types'; +import type { NoTranslate, ToTranslate, TranslationFn } from '@/i18n/types'; import type { CommonGame } from '@/ps/games/game'; import type { BaseModEntry } from '@/ps/games/mods'; -import type { HTPDropdown, GameHTPData } from '@/ps/games/types'; +import type { GameHTPData, HTPDropdown, HTPImage } from '@/ps/games/types'; import type { PSCommand, PSCommandChild } from '@/types/chat'; import type { Room } from 'ps-client'; import type { HTMLopts } from 'ps-client/classes/common'; - - -import { Button, Username } from '@/utils/components/ps'; -import { prefix } from '@/config/ps'; import type { ReactElement } from 'react'; -import { groupSub } from '@/utils/groupSub'; + +const guideImageUrl = (path: HTPImage['path']) => `${process.env.WEB_URL}/static/guides/${path}`; const HTPSection = ({ section }: { section: HTPDropdown }) => { return (
- - {section.title} - + {section.title}
- {section.lines.map((line, idx) => ( -
{line}
+ {section.lines?.map((line, idx) => ( +
+ {line} +
))} - {section.subsections?.map((sub, idx) => ( - + {section.images?.map((image, idx) => ( + {image.alt ))} + {section.subsections?.map((sub, idx) => )}
); }; -const HowToPlayBox = ({ gameName, data }: { gameName: string; data: GameHTPData }) => { +const HowToPlayBox = ({ data }: { data: GameHTPData }) => { return ( -
+
Goal: {data.goal}
@@ -198,7 +207,7 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): }, help: `Game module for ${Game.meta.name}. See subcommands.`, syntax: 'CMD', - async run({ run, command, calledFrom, broadcastHTML, $T, message }) { + async run({ calledFrom, broadcastHTML, message }) { const sendHTML = (HTML: ReactElement) => { if (calledFrom && calledFrom.at(-1) !== 'help') return message.replyHTML(HTML); else return broadcastHTML(HTML); @@ -207,7 +216,8 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): return sendHTML(

- {`Hi, to make a game of ${gameId}, you ${Game.meta.players === 'single' ? `can use ,${gameId} create! ` : 'need to ask a staff member!'}`} + Hi, to make a game of {gameId}, you  + {Game.meta.players === 'single' ? `can use ,${gameId} create!` : 'need to ask a staff member!'}

If you want to learn how to play, check{' '} @@ -293,10 +303,10 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): help: 'Shows the how-to-play for this game.', flags: { allowPMs: true }, syntax: 'CMD', - async run({ message, arg }) { + async run({ message }) { const htpData = Game.meta.htp; if (!htpData) throw new ChatError('GAME.NO_DATA' as ToTranslate); - return message.replyHTML(); + return message.replyHTML(); }, }, ...conditionalCommand( diff --git a/src/ps/games/azul/meta.ts b/src/ps/games/azul/meta.ts index 5af7c52e..905b2f78 100644 --- a/src/ps/games/azul/meta.ts +++ b/src/ps/games/azul/meta.ts @@ -25,7 +25,7 @@ export const meta: Meta = { timer: fromHumanTime('45 sec'), htp: { - goal: 'Be the first to get 7 points.', + goal: 'Have the highest score when any player completes a wall row.', sections: [ { title: 'Setup', @@ -61,7 +61,7 @@ export const meta: Meta = { lines: [ '- Happens when there are no tiles left in factories/waste.', '- For each filled row, place one tile on the wall in the corresponding row and type.', - '- Every row/column of tiles gives you points equal to the number of tiles in the row/column.', + '- Each wall tile scores contiguous horizontal and vertical lines through it.', '- After the game, each filled row gives 2 points, every column 7, every type 10.', '- The game ends when a row is filled.', ], diff --git a/src/ps/games/connectfour/meta.ts b/src/ps/games/connectfour/meta.ts index a84ae06e..5a9e70fd 100644 --- a/src/ps/games/connectfour/meta.ts +++ b/src/ps/games/connectfour/meta.ts @@ -20,13 +20,13 @@ export const meta: Meta = { timer: fromHumanTime('1 min'), htp: { - goal: 'Get four of their colored discs in a row—horizontally, vertically, or diagonally.', + goal: 'Get four of your colored discs in a row—horizontally, vertically, or diagonally.', sections: [ { title: 'Gameplay', lines: [ '- Players take turns dropping one disc into one of the seven columns.', - '- Discs stack on top of each other in the selected column.' + '- Discs stack on top of each other in the selected column.', ], }, ], diff --git a/src/ps/games/othello/meta.ts b/src/ps/games/othello/meta.ts index 128ca717..501da172 100644 --- a/src/ps/games/othello/meta.ts +++ b/src/ps/games/othello/meta.ts @@ -19,20 +19,31 @@ export const meta: Meta = { timer: fromHumanTime('1 min'), htp: { - goal: "Place discs on the board to surround and flip your opponent's discs.", + goal: 'Have more discs of your color than your opponent when the game ends.', sections: [ + { + title: 'Initial board setup', + lines: ['- 2 white and 2 black discs start in the center.'], + images: [{ path: 'othello/setup.png', alt: 'Initial Othello board setup', width: 170 }], + }, { title: 'Gameplay', lines: [ - '- Players take turns placing discs on the board.', - "- A valid move must outflank (or 'sandwich') at least one of your opponent's discs vertically, horizontally, or diagonally.", - "- When a disc is placed, it flips all of the opponent's discs that are newly outflanked.", - '- If a player does not have a valid move, then the turn passes to the other player.', + '- Black moves first, then white.', + '- A valid move must outflank at least one opponent disc vertically, horizontally, or diagonally.', + '- Outflank: your disc borders opponent row(s) at both ends.', + '- If you have no valid move, your turn is skipped.', + '- Game ends when neither player can move or the board is full.', ], }, + { + title: 'Valid move example', + lines: ['- Black outflanks White vertically, horizontally, and diagonally (not all three required).'], + images: [{ path: 'othello/valid-move.gif', alt: 'Valid Othello move example', width: 170 }], + }, { title: 'Win conditions', - lines: ['- The game ends when no more valid moves can be made.', '- The player with the most discs at the end wins.'], + lines: ['- Most discs wins.'], }, ], }, diff --git a/src/ps/games/splendor/meta.ts b/src/ps/games/splendor/meta.ts index f5a0a1ab..29ca39b2 100644 --- a/src/ps/games/splendor/meta.ts +++ b/src/ps/games/splendor/meta.ts @@ -16,7 +16,7 @@ export const meta: Meta = { timer: fromHumanTime('2 min'), htp: { - goal: 'Be the first to get 15 points', + goal: 'Be the first to get 15 points.', sections: [ { title: 'Cards', @@ -49,7 +49,6 @@ export const meta: Meta = { '- Take a card so only you can buy it.', '- Also gain a Dragon gem (if possible).', '- Can reserve up to 3 cards only.', - '- Cannot win if you have an unbought reserved card.', ], }, ], @@ -68,7 +67,7 @@ export const meta: Meta = { lines: [ '- Once a player reaches 15 points, everyone after them in turn order gets one last turn.', '- If points are tied, the one with fewer Cards wins.', - '- If still tied, everyone wins!!!!', + '- If still tied, Audiino comes and bullies all winners.', ], }, ], diff --git a/src/ps/games/types.ts b/src/ps/games/types.ts index 545ecaff..c789ac26 100644 --- a/src/ps/games/types.ts +++ b/src/ps/games/types.ts @@ -2,9 +2,17 @@ import type { TranslatedText } from '@/i18n/types'; import type { ModData, ModEnum } from '@/ps/games/mods'; import type { Satisfies } from '@/types/common'; +export interface HTPImage { + /** Path under `/static/guides/`, e.g. `othello/setup.png`. */ + path: string; + alt?: string; + width?: number; +} + export interface HTPDropdown { title: string; - lines: string[]; + lines?: string[]; + images?: HTPImage[]; subsections?: HTPDropdown[]; } diff --git a/src/web/static/guides/othello/setup.png b/src/web/static/guides/othello/setup.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd51967c0d9f57ea04b5be870d839447d325ae8 GIT binary patch literal 25132 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 literal 0 HcmV?d00001 diff --git a/src/web/static/guides/othello/valid-move.gif b/src/web/static/guides/othello/valid-move.gif new file mode 100644 index 0000000000000000000000000000000000000000..e12b24521e87340aea33da2a812699990d997af5 GIT binary patch literal 26454 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 BCj$Tg literal 0 HcmV?d00001 From 49d0db23fb1b7a3bcbb5549922733155a7ae4b15 Mon Sep 17 00:00:00 2001 From: PartMan Date: Sat, 8 Aug 2026 12:07:29 +0530 Subject: [PATCH 6/7] chore: Links and JSX --- package-lock.json | 15 ------ package.json | 3 +- src/i18n/languages/english.ts | 1 + src/i18n/languages/french.ts | 1 + src/i18n/languages/hindi.ts | 1 + src/i18n/languages/portuguese.ts | 1 + src/ps/commands/games/core.tsx | 79 ++++++++++++++++++---------- src/ps/games/othello/meta.ts | 2 +- src/ps/games/splendor/htpContent.tsx | 9 ++++ src/ps/games/splendor/meta.ts | 6 ++- src/ps/games/types.ts | 3 ++ 11 files changed, 75 insertions(+), 46 deletions(-) create mode 100644 src/ps/games/splendor/htpContent.tsx diff --git a/package-lock.json b/package-lock.json index 308bdd3c..0cbb9dfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -630,9 +630,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -653,9 +650,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -676,9 +670,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -699,9 +690,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -742,9 +730,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index 43c04d8f..0aecdf0c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "secrets-check": "sh scripts/secrets_check.sh", "prettify": "prettier -w", "prepare": "sh ./scripts/setup.sh", + "prestart": "bash ./scripts/prestart.sh", "start": "ts-node --swc src/index.ts", "tailwind": "npx @tailwindcss/cli -i ./src/web/styles/tailwind.css -o ./src/web/react/compiled/styles.css", "test": "npm run lint && npm run tsc", @@ -71,4 +72,4 @@ "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", "lightningcss-linux-x64-gnu": "^1.29.1" } -} \ No newline at end of file +} diff --git a/src/i18n/languages/english.ts b/src/i18n/languages/english.ts index 11115806..20c2ff99 100644 --- a/src/i18n/languages/english.ts +++ b/src/i18n/languages/english.ts @@ -73,6 +73,7 @@ export default { IN_PROGRESS: 'This game is already in progress. Weeb.', IS_FULL: 'The game has no more space for players.', NOT_FOUND: "Couldn't find the game you meant...", + NO_DATA: 'This game has no how-to-play guide.', NOT_PLAYING: [ "You're not a player!", "You're not playing, weeb.", diff --git a/src/i18n/languages/french.ts b/src/i18n/languages/french.ts index b67027d9..97d4ba5f 100644 --- a/src/i18n/languages/french.ts +++ b/src/i18n/languages/french.ts @@ -76,6 +76,7 @@ export default { IN_PROGRESS: 'Cette partie est déjà en cours. Otaku.', IS_FULL: "Il n'y a plus de place pour de nouveaux joueurs.", NOT_FOUND: 'Impossible de trouver la partie en question...', + NO_DATA: "Ce jeu n'a pas de guide de jeu.", NOT_PLAYING: [ "Vous n'êtes pas un joueur !", 'Vous ne jouez pas, otaku.', diff --git a/src/i18n/languages/hindi.ts b/src/i18n/languages/hindi.ts index 55d1077d..fb590992 100644 --- a/src/i18n/languages/hindi.ts +++ b/src/i18n/languages/hindi.ts @@ -72,6 +72,7 @@ export default { IN_PROGRESS: 'Yeh game pehle hi chal raha hai. Weeb.', IS_FULL: 'Game mein ab aur khiladi nahi liye ja sakte.', NOT_FOUND: 'Aap jo game chahte the, woh nahi mila...', + NO_DATA: 'Is game ka how-to-play guide nahi hai.', NOT_PLAYING: 'Aap nahi khel rahe ho, weeb.', NOT_STARTED: 'Game abhi tak shuru nahi hua hai.', CANNOT_START: 'Game shuru nahi kiya ja sakta! Kripya khiladiyon ko dekhein.', diff --git a/src/i18n/languages/portuguese.ts b/src/i18n/languages/portuguese.ts index ad01f168..17ecec9b 100644 --- a/src/i18n/languages/portuguese.ts +++ b/src/i18n/languages/portuguese.ts @@ -75,6 +75,7 @@ export default { IN_PROGRESS: 'Este jogo já está em andamento. Weeb.', IS_FULL: 'Não há mais espaço para jogadores.', NOT_FOUND: 'Não foi possível encontrar o jogo que você quis dizer...', + NO_DATA: 'Este jogo não tem um guia de como jogar.', NOT_PLAYING: [ 'Você não é um jogador!', 'Você não está jogando, weeb.', diff --git a/src/ps/commands/games/core.tsx b/src/ps/commands/games/core.tsx index 0c262de5..a5b71b7d 100644 --- a/src/ps/commands/games/core.tsx +++ b/src/ps/commands/games/core.tsx @@ -10,25 +10,43 @@ import { fromHumanTime, toHumanTime } from '@/utils/humanTime'; import { Timer } from '@/utils/timer'; import { toId } from '@/utils/toId'; -import type { NoTranslate, ToTranslate, TranslationFn } from '@/i18n/types'; +import type { NoTranslate, TranslationFn } from '@/i18n/types'; import type { CommonGame } from '@/ps/games/game'; import type { BaseModEntry } from '@/ps/games/mods'; import type { GameHTPData, HTPDropdown, HTPImage } from '@/ps/games/types'; import type { PSCommand, PSCommandChild } from '@/types/chat'; import type { Room } from 'ps-client'; import type { HTMLopts } from 'ps-client/classes/common'; -import type { ReactElement } from 'react'; -const guideImageUrl = (path: HTPImage['path']) => `${process.env.WEB_URL}/static/guides/${path}`; +const guideImageUrl = (path: HTPImage['path']) => (/^https?:\/\//.test(path) ? path : `${process.env.WEB_URL}/static/guides/${path}`); + +const HTPLine = ({ line }: { line: string }) => { + const parts = line.split(/(https?:\/\/[^\s]+)/g); + if (parts.length === 1) return <>{line}; + + return ( + <> + {parts.map((part, idx) => + /^https?:\/\//.test(part) ? ( + + {part} + + ) : ( + part + ) + )} + + ); +}; const HTPSection = ({ section }: { section: HTPDropdown }) => { return (

- {section.title} -
+ {section.title} +
{section.lines?.map((line, idx) => (
- {line} +
))} {section.images?.map((image, idx) => ( @@ -36,9 +54,12 @@ const HTPSection = ({ section }: { section: HTPDropdown }) => { key={idx} src={guideImageUrl(image.path)} alt={image.alt ?? ''} - style={{ display: 'block', width: image.width ?? 170, marginTop: 4 }} + width={image.width ?? 170} + height={image.height ?? 178} + style={{ display: 'block', marginTop: 4 }} /> ))} + {section.content} {section.subsections?.map((sub, idx) => )}
@@ -50,17 +71,16 @@ const HowToPlayBox = ({ data }: { data: GameHTPData }) => {
- Goal: {data.goal} + Goal:{' '} + + +
{data.sections.map((sec, idx) => ( @@ -207,21 +227,16 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): }, help: `Game module for ${Game.meta.name}. See subcommands.`, syntax: 'CMD', - async run({ calledFrom, broadcastHTML, message }) { - const sendHTML = (HTML: ReactElement) => { - if (calledFrom && calledFrom.at(-1) !== 'help') return message.replyHTML(HTML); - else return broadcastHTML(HTML); - }; - const Bot = message.parent; - return sendHTML( -
+ async run({ broadcastHTML, message }) { + return broadcastHTML( +

Hi, to make a game of {gameId}, you  {Game.meta.players === 'single' ? `can use ,${gameId} create!` : 'need to ask a staff member!'}

If you want to learn how to play, check{' '} - {' '} out! @@ -231,6 +246,16 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): }, categories: ['game'], children: { + help: { + name: 'help', + aliases: ['h'], + help: 'Shows this help message.', + syntax: 'CMD', + flags: { allowPMs: true, noDisplay: true }, + async run({ command, run }) { + return run(`help ${command[0]}`); + }, + }, create: { name: 'create', aliases: ['new', 'n'], @@ -297,15 +322,15 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): } }, }, - help: { - name: 'help', - aliases: ['htp', 'howtoplay'], + htp: { + name: 'htp', + aliases: ['howtoplay'], help: 'Shows the how-to-play for this game.', flags: { allowPMs: true }, syntax: 'CMD', - async run({ message }) { + async run({ message, $T }) { const htpData = Game.meta.htp; - if (!htpData) throw new ChatError('GAME.NO_DATA' as ToTranslate); + if (!htpData) throw new ChatError($T('GAME.NO_DATA')); return message.replyHTML(); }, }, diff --git a/src/ps/games/othello/meta.ts b/src/ps/games/othello/meta.ts index 501da172..0228d8a9 100644 --- a/src/ps/games/othello/meta.ts +++ b/src/ps/games/othello/meta.ts @@ -24,7 +24,7 @@ export const meta: Meta = { { title: 'Initial board setup', lines: ['- 2 white and 2 black discs start in the center.'], - images: [{ path: 'othello/setup.png', alt: 'Initial Othello board setup', width: 170 }], + images: [{ path: 'othello/setup.png', alt: 'Initial Othello board setup', width: 170, height: 178 }], }, { title: 'Gameplay', diff --git a/src/ps/games/splendor/htpContent.tsx b/src/ps/games/splendor/htpContent.tsx new file mode 100644 index 00000000..37fb3fe3 --- /dev/null +++ b/src/ps/games/splendor/htpContent.tsx @@ -0,0 +1,9 @@ +import { Small } from '@/ps/games/render'; +import metadata from '@/ps/games/splendor/metadata.json'; +import { PokemonCard } from '@/ps/games/splendor/render'; + +export const swadloonExample = ( + + + +); diff --git a/src/ps/games/splendor/meta.ts b/src/ps/games/splendor/meta.ts index 29ca39b2..8ffe3e2c 100644 --- a/src/ps/games/splendor/meta.ts +++ b/src/ps/games/splendor/meta.ts @@ -1,3 +1,4 @@ +import { swadloonExample } from '@/ps/games/splendor/htpContent'; import { GamesList } from '@/ps/games/types'; import { fromHumanTime } from '@/utils/humanTime'; @@ -23,9 +24,10 @@ export const meta: Meta = { lines: [ 'Each Card consists of 3 components:', '- Cost (bottom-left): The amount of Gems needed to buy it.', - '- Type (top-right): Each Card acts as a permanent gem of the Type indicated.', + '- Type (top-right): Each Card acts as a permanent token of that type.', '- Point (top-left): The value each Card is worth.', ], + content: swadloonExample, }, { title: 'Gameplay', @@ -34,7 +36,7 @@ export const meta: Meta = { { title: 'Pick up Gems', lines: [ - '- Take 3 different OR 2 of the same color (only if >=4 of that color available).', + '- Take 3 different OR 2 of the same type (only if >=4 of that type available).', '- Max 10 Gems can be held at a time.', '- If >10, choose which to return.', ], diff --git a/src/ps/games/types.ts b/src/ps/games/types.ts index c789ac26..cbc06d09 100644 --- a/src/ps/games/types.ts +++ b/src/ps/games/types.ts @@ -1,18 +1,21 @@ import type { TranslatedText } from '@/i18n/types'; import type { ModData, ModEnum } from '@/ps/games/mods'; import type { Satisfies } from '@/types/common'; +import type { ReactElement } from 'react'; export interface HTPImage { /** Path under `/static/guides/`, e.g. `othello/setup.png`. */ path: string; alt?: string; width?: number; + height?: number; } export interface HTPDropdown { title: string; lines?: string[]; images?: HTPImage[]; + content?: ReactElement; subsections?: HTPDropdown[]; } From e4f4e27181ad716e864dfffbde82851d2d10e54e Mon Sep 17 00:00:00 2001 From: PartMan Date: Sat, 8 Aug 2026 12:09:50 +0530 Subject: [PATCH 7/7] chore: Yeet the NO_DATA --- src/i18n/languages/english.ts | 1 - src/i18n/languages/french.ts | 1 - src/i18n/languages/hindi.ts | 1 - src/i18n/languages/portuguese.ts | 1 - src/ps/commands/games/core.tsx | 3 +-- src/ps/games/types.ts | 2 +- 6 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/i18n/languages/english.ts b/src/i18n/languages/english.ts index 20c2ff99..11115806 100644 --- a/src/i18n/languages/english.ts +++ b/src/i18n/languages/english.ts @@ -73,7 +73,6 @@ export default { IN_PROGRESS: 'This game is already in progress. Weeb.', IS_FULL: 'The game has no more space for players.', NOT_FOUND: "Couldn't find the game you meant...", - NO_DATA: 'This game has no how-to-play guide.', NOT_PLAYING: [ "You're not a player!", "You're not playing, weeb.", diff --git a/src/i18n/languages/french.ts b/src/i18n/languages/french.ts index 97d4ba5f..b67027d9 100644 --- a/src/i18n/languages/french.ts +++ b/src/i18n/languages/french.ts @@ -76,7 +76,6 @@ export default { IN_PROGRESS: 'Cette partie est déjà en cours. Otaku.', IS_FULL: "Il n'y a plus de place pour de nouveaux joueurs.", NOT_FOUND: 'Impossible de trouver la partie en question...', - NO_DATA: "Ce jeu n'a pas de guide de jeu.", NOT_PLAYING: [ "Vous n'êtes pas un joueur !", 'Vous ne jouez pas, otaku.', diff --git a/src/i18n/languages/hindi.ts b/src/i18n/languages/hindi.ts index fb590992..55d1077d 100644 --- a/src/i18n/languages/hindi.ts +++ b/src/i18n/languages/hindi.ts @@ -72,7 +72,6 @@ export default { IN_PROGRESS: 'Yeh game pehle hi chal raha hai. Weeb.', IS_FULL: 'Game mein ab aur khiladi nahi liye ja sakte.', NOT_FOUND: 'Aap jo game chahte the, woh nahi mila...', - NO_DATA: 'Is game ka how-to-play guide nahi hai.', NOT_PLAYING: 'Aap nahi khel rahe ho, weeb.', NOT_STARTED: 'Game abhi tak shuru nahi hua hai.', CANNOT_START: 'Game shuru nahi kiya ja sakta! Kripya khiladiyon ko dekhein.', diff --git a/src/i18n/languages/portuguese.ts b/src/i18n/languages/portuguese.ts index 17ecec9b..ad01f168 100644 --- a/src/i18n/languages/portuguese.ts +++ b/src/i18n/languages/portuguese.ts @@ -75,7 +75,6 @@ export default { IN_PROGRESS: 'Este jogo já está em andamento. Weeb.', IS_FULL: 'Não há mais espaço para jogadores.', NOT_FOUND: 'Não foi possível encontrar o jogo que você quis dizer...', - NO_DATA: 'Este jogo não tem um guia de como jogar.', NOT_PLAYING: [ 'Você não é um jogador!', 'Você não está jogando, weeb.', diff --git a/src/ps/commands/games/core.tsx b/src/ps/commands/games/core.tsx index a5b71b7d..388a1cc7 100644 --- a/src/ps/commands/games/core.tsx +++ b/src/ps/commands/games/core.tsx @@ -328,9 +328,8 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]): help: 'Shows the how-to-play for this game.', flags: { allowPMs: true }, syntax: 'CMD', - async run({ message, $T }) { + async run({ message }) { const htpData = Game.meta.htp; - if (!htpData) throw new ChatError($T('GAME.NO_DATA')); return message.replyHTML(); }, }, diff --git a/src/ps/games/types.ts b/src/ps/games/types.ts index cbc06d09..2f3afe25 100644 --- a/src/ps/games/types.ts +++ b/src/ps/games/types.ts @@ -56,7 +56,7 @@ export type Meta = Readonly< canOfferDraws?: boolean; /** How to play instructions. */ - htp?: GameHTPData; + htp: GameHTPData; // UGO-CODE /**