diff --git a/build b/build index b82f892dd..b9a05fdb2 100755 --- a/build +++ b/build @@ -49,6 +49,9 @@ case 'minidex': case 'sprites': execSync(`node ./build-tools/build-minidex`, options); break; +case 'commands': + execSync(`node ./build-tools/build-commands`, options); + break; case 'replays': execSync(`node ./build-tools/build-replays`, options); process.exit(); @@ -68,6 +71,9 @@ execSync(`node ./build-tools/update` + full, options); if (full) { try { + execSync(`node ./build-tools/build-commands`, options); execSync(`node ./build-tools/build-replays`, options); - } catch (e) {} + } catch (e) { + console.error(e); + } } diff --git a/build-tools/build-commands b/build-tools/build-commands new file mode 100755 index 000000000..787f2f254 --- /dev/null +++ b/build-tools/build-commands @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * Builds out a cache of commands from the server & their help commands + */ + +const fs = require('fs'); + +console.log('Building `data/commands.json` index...'); + +const noop = () => null; +Object.assign(globalThis, { + Config: { routes: { root: '' } }, + Monitor: { crashlog: noop }, + Punishments: { addRoomPunishmentType: noop }, + Rooms: { + rooms: new Map(), + search: noop, + get: noop, + RoomGame: class {}, + RoomGamePlayer: class {}, + SimpleRoomGame: class {}, + }, + toID: str => ("" + str).toLowerCase().replace(/[^a-z0-9]/ig, ''), +}); + +const { Chat } = require('../caches/pokemon-showdown/dist/server/chat'); + +// yes, these have to be done separately, yes it sucks +Object.assign(globalThis, { + Tournaments: require('../caches/pokemon-showdown/dist/server/tournaments'), + Chat, + Dex: { includeData: noop }, +}); + +Chat.commands = Object.create(null); +Chat.pages = Object.create(null); +Chat.loadPluginDirectory("dist/server/chat-commands"); +Chat.loadPlugin(globalThis.Tournaments, "tournaments"); +Chat.loadPluginDirectory("dist/server/chat-plugins"); + +const recurseCommands = (table = Chat.commands, namespace = '', results = {}) => { + for (const cmd in table) { + const handler = table[cmd]; + if (!handler || ['string', 'boolean'].includes(typeof handler) || cmd === 'aliases') { + continue; + } + const fullCmd = (namespace + ' ' + cmd).trim(); + if (Array.isArray(handler)) { + /** handler = Chat.parseCommand(`/${fullCmd.replace('help', '')}`).handler; + if (handler.requiredPermission) continue; + results.push(fullCmd); */ + continue; + } + if (typeof handler === 'object') { + recurseCommands(handler, (namespace + " " + cmd).trim(), results); + continue; + } + const pKey = handler.plugin.split('/server/')[1]; + if (!results[pKey]) { + results[pKey] = []; + } + if (!results[pKey].includes(fullCmd) && !handler.requiredPermission) { + results[pKey].push(fullCmd); + } + } + return results; +}; + +fs.writeFileSync( + 'play.pokemonshowdown.com/data/commands.js', + `exports.BattleChatCommands = ${JSON.stringify(recurseCommands())}` +); + +console.log('DONE'); +process.exit(0); diff --git a/eslint.config.mjs b/eslint.config.mjs index a9c496393..8562e00f2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -49,7 +49,7 @@ export default configure([ "BattleOtherAnims": false, "BattlePokedex": false, "BattlePokemonSprites": false, "BattlePokemonSpritesBW": false, "BattleSearchCountIndex": false, "BattleSearchIndex": false, "BattleArticleTitles": false, "BattleSearchIndexOffset": false, "BattleSearchIndexType": false, "BattleStatIDs": false, "BattleStatNames": false, "BattleStatusAnims": false, "BattleStatuses": false, "BattleTeambuilderTable": false, "ModifiableValue": false, "BattleStatGuesser": false, "BattleStatOptimizer": false, "BattleText": true, "BattleTextAFD": false, "BattleTextNotAFD": false, - "BattleTextParser": false, + "BattleTextParser": false, "BattleChatCommands": false, // Generic global variables "Config": false, "BattleSearch": false, "Storage": false, "Dex": false, "DexSearch": false, @@ -57,7 +57,7 @@ export default configure([ "ChatHistory": false, "Topbar": false, "UserList": false, // Rooms - "Room": false, "BattleRoom": false, "ChatRoom": false, "ConsoleRoom": false, "HTMLRoom": false, "LadderRoom": false, "MainMenuRoom": false, "RoomsRoom": false, "BattlesRoom": false, "TeambuilderRoom": false, + "Room": false, "BattleRoom": false, "ChatRoom": false, "ConsoleRoom": false, "HTMLRoom": false, "LadderRoom": false, "MainMenuRoom": false, "RoomsRoom": false, "BattlesRoom": false, "TeambuilderRoom": false, "ResourceRoom": false, // Tons of popups "Popup": false, "ForfeitPopup": false, "BracketPopup": false, "LoginPasswordPopup": false, "UserPopup": false, "UserOptionsPopup": false, "UserOptions": false, "TeamPopup": false, diff --git a/play.pokemonshowdown.com/index.template.html b/play.pokemonshowdown.com/index.template.html index efa2e4dd6..8dc43862b 100644 --- a/play.pokemonshowdown.com/index.template.html +++ b/play.pokemonshowdown.com/index.template.html @@ -6,7 +6,7 @@ +#################+,//////, +###################,/////, ########+ +########//// - ////, PokemonShowdown.com + ////, PokemonShowdown.com ########+ +########/// +###################+// +#################+ @@ -130,5 +130,7 @@ https://psim.us/dev + + diff --git a/play.pokemonshowdown.com/js/client-chat.js b/play.pokemonshowdown.com/js/client-chat.js index 498ac88e1..f00794d50 100644 --- a/play.pokemonshowdown.com/js/client-chat.js +++ b/play.pokemonshowdown.com/js/client-chat.js @@ -2068,6 +2068,115 @@ } }); + this.ResourceRoom = HTMLRoom.extend({ + events: { 'keydown [name=search]': 'searchCommands' }, + type: 'resources', + title: 'Help', + search: '', + searchCommands: function (e) { + this.search = toID($(e.currentTarget).val()); + this.update(); + }, + + initialize: function () { + this.$el.addClass('ps-room-light').addClass('scrollable'); + var buf = '
'; + buf += '

PS! Informational Resources


'; + buf += 'PS! is a wide and varied site, with more facets than can be covered here easily.
'; + buf += 'While this page chiefly documents the ever-shifting set of commands available to PS! users, here are some useful resources for newcomers:
'; + buf += '- Beginner\'s Guide to Pokémon Showdown
'; + buf += '- An introduction to competitive Pokémon
'; + buf += '- What do \'OU\', \'UU\', etc mean?
'; + buf += '- What are the rules for each format?
'; + buf += '- What is \'Sleep Clause\' and other clauses?
'; + buf += '- Next Steps for Competitive Battling
'; + buf += '-
'; + buf += '- '; + buf += '
Commands:
'; + buf += 'Within any of the chats, and in private messages, it is possible to type in commands (messages beginning with /) in order to perform a particular action. '; + buf += 'A great number of these commands exist, with some only available to certain users. '; + buf += 'For instance, you can broadcast commands to others with the ! command prefix, but only when you\'re a player in a battle or a Voice (+) user. '; + buf += 'For more information on ranks, type /groups in any chat. You can also use the "chat self" button on your username in the top right if you need a place to send these commands '; + buf += 'without joining a room.

'; + + buf += '
Here\'s a list of the most useful commands for the average Pokemon Showdown experience:'; + buf += '

COMMANDS: /report, /msg, /reply, /logout, /challenge, /search, /rating, /whois, /user, /join, /leave, /userauth, /roomauth

'; + buf += '

BATTLE ROOM COMMANDS: /savereplay, /hideroom, /inviteonly, /invite, /timer, /forfeit

'; + buf += '

OPTION COMMANDS: /nick, /avatar, /ignore, /status, /away, /busy, /back, /timestamps, /highlight, /showjoins, /hidejoins, /blockchallenges, /blockpms

'; + buf += '

INFORMATIONAL/RESOURCE COMMANDS: /groups, /faq, /rules, /intro, /formatshelp, /othermetas, /analysis, /punishments, /calc, /git, /cap, /roomhelp, /roomfaq (replace / with ! to broadcast. Broadcasting requires: + % @ # ~)

'; + buf += '

DATA COMMANDS: /data, /dexsearch, /movesearch, /itemsearch, /learn, /statcalc, /effectiveness, /weakness, /coverage, /randommove, /randompokemon (replace / with ! to broadcast. Broadcasting requires: + % @ # ~)

'; + buf += '

For an overview of room commands, use /roomhelp

'; + buf += '

For details of a specific command, you can use the command /help [command], for example /help data.

'; + buf += '

'; + + buf += 'A complete list of commands available to regular users is provided below. Use /help [commandname] for more in-depth information on how to use them.'; + buf += '
'; + buf += ' '; + buf += '
'; + buf += '' + this.getCommandList() + ''; + this.$el.html(buf); + }, + update: function () { + this.$el.find('[name="cmdbuffer"]').html(this.getCommandList()); + }, + getCommandList: function () { + var buf = ''; + var search = this.search; + var keys = Object.keys(BattleChatCommands).sort(function (a, b) { + // pin info to the top ALWAYS + if (b.endsWith('info')) return 2; + // prefer default commands near the top, more generally useful + if (b.includes('chat-commands')) return 1; + var aCount = BattleChatCommands[a].filter(function (x) { + return toID(x).includes(search); + }).length; + var bCount = BattleChatCommands[b].filter(function (x) { + return toID(x).includes(search); + }).length; + + return (bCount - aCount) || a.localeCompare(b); + }).filter(function (plugin) { + return !(plugin.endsWith('admin')); + }); + + for (var i = 0; i < keys.length; i++) { + var pluginName = keys[i]; + var cmdTable = BattleChatCommands[pluginName]; + if (!cmdTable.length) continue; + if (pluginName.startsWith('chat-plugins')) { + pluginName = 'Chat plugin: ' + pluginName.split('/').slice(1).join('/'); + } else if (pluginName.startsWith('chat-commands')) { + pluginName = 'Core commands: ' + pluginName.split('/').slice(1).join('/'); ; + } + var matchedCmds = []; + for (var j = 0; j < cmdTable.length; j++) { + if (this.search.length && !toID(cmdTable[j]).includes(this.search)) { + continue; + } + matchedCmds.push('
  • ' + cmdTable[j] + '
  • '); + } + var open = (( + this.search.length && matchedCmds.length) || pluginName.includes('Core') + ) ? ' open' : ''; + var empty = this.search.length && !matchedCmds.length; + + if (!empty) buf += '
    '; + buf += '' + pluginName + ''; + if (!empty) { + buf += '
    '; + } else { + buf += '

    '; + } + buf += '
    '; + } + return buf; + }, + join: function () {}, + leave: function () {} + }); + }).call(this, jQuery); function ChatHistory() { diff --git a/play.pokemonshowdown.com/js/client-mainmenu.js b/play.pokemonshowdown.com/js/client-mainmenu.js index 992c5d65f..6850c57b0 100644 --- a/play.pokemonshowdown.com/js/client-mainmenu.js +++ b/play.pokemonshowdown.com/js/client-mainmenu.js @@ -60,7 +60,8 @@ buf += ''; + buf += '

    '; + buf += '

    '; this.$('.mainmenu').html(buf); diff --git a/play.pokemonshowdown.com/js/client-topbar.js b/play.pokemonshowdown.com/js/client-topbar.js index 3becfdd84..f80b27ad4 100644 --- a/play.pokemonshowdown.com/js/client-topbar.js +++ b/play.pokemonshowdown.com/js/client-topbar.js @@ -86,6 +86,8 @@ return buf + '> Teambuilder'; case 'ladder': return buf + '> Ladder'; + case 'resources': + return buf + '> Resources'; case 'battles': return buf + '> Battles'; case 'rooms': @@ -130,7 +132,12 @@ this.$('.logo').show(); this.$('.maintabbar').removeClass('minitabbar'); - var buf = ''; + var buf = ''; var sideBuf = ''; var notificationCount = app.rooms[''].notifications ? 1 : 0; diff --git a/play.pokemonshowdown.com/js/client.js b/play.pokemonshowdown.com/js/client.js index da25119df..9e9a5a260 100644 --- a/play.pokemonshowdown.com/js/client.js +++ b/play.pokemonshowdown.com/js/client.js @@ -1691,6 +1691,7 @@ function toId() { 'ladder': LadderRoom, 'lobby': ChatRoom, 'staff': ChatRoom, + 'resources': ResourceRoom, 'constructor': ChatRoom }; var typeTable = { @@ -1727,7 +1728,7 @@ function toId() { if (this.curSideRoom === oldRoom) this.curSideRoom = room; if (this.sideRoom === oldRoom) this.sideRoom = room; } - if (['', 'teambuilder', 'ladder', 'rooms'].indexOf(room.id) < 0) { + if (['', 'teambuilder', 'ladder', 'rooms', 'resources'].indexOf(room.id) < 0) { if (room.isSideRoom) { this.sideRoomList.push(room); } else { diff --git a/play.pokemonshowdown.com/js/page-resources.js b/play.pokemonshowdown.com/js/page-resources.js new file mode 100644 index 000000000..937b704b3 --- /dev/null +++ b/play.pokemonshowdown.com/js/page-resources.js @@ -0,0 +1,145 @@ +"use strict";function _inheritsLoose(t,o){t.prototype=Object.create(o.prototype),t.prototype.constructor=t,_setPrototypeOf(t,o);}function _setPrototypeOf(t,e){return _setPrototypeOf=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t;},_setPrototypeOf(t,e);}var + + + + + + + + +ResourceRoom=function(_PSRoom){ + + + +function ResourceRoom(options){var _this; +_this=_PSRoom.call(this,options)||this;_this.classType='resources';_this.canConnect=false; +_this.title='Resources';return _this; +}_inheritsLoose(ResourceRoom,_PSRoom);var _proto=ResourceRoom.prototype;_proto. +connect=function connect(){};return ResourceRoom;}(PSRoom);var + + +ResourcePanel=function(_PSRoomPanel){function ResourcePanel(){var _this2;for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key];}_this2=_PSRoomPanel.call.apply(_PSRoomPanel,[this].concat(args))||this;_this2. + + + + + + +state={search:''};return _this2;}_inheritsLoose(ResourcePanel,_PSRoomPanel);var _proto2=ResourcePanel.prototype;_proto2. +receiveLine=function receiveLine(){};_proto2. +render=function render(){var _this3=this; +var room=this.props.room; +return preact.h(PSPanelWrapper,{room:room,scrollable:true}, +preact.h("div",{className:"pad"}, +preact.h("h2",null,"PS! Informational Resources"), +preact.h("hr",null), +preact.h("p",null,"PS! is a wide and varied site, with more facets than can be covered here easily.", + +preact.h("br",null),"While this page chiefly documents the ever-shifting set of commands available to PS! users, here are some useful resources for newcomers:" + +), +preact.h("ul",null, +preact.h("li",null, +preact.h("a",{href:"https://www.smogon.com/forums/threads/3676132/"},"Beginner's Guide to Pok\xE9mon Showdown") +), +preact.h("li",null, +preact.h("a",{href:"https://www.smogon.com/dp/articles/intro_comp_pokemon"},"An introduction to competitive Pok\xE9mon") +), +preact.h("li",null, +preact.h("a",{href:"https://www.smogon.com/sm/articles/sm_tiers"},"What do 'OU', 'UU', etc mean?") +), +preact.h("li",null, +preact.h("a",{href:"https://www.smogon.com/dex/ss/formats/"},"What are the rules for each format?") +), +preact.h("li",null, +preact.h("a",{href:"https://www.smogon.com/ss/articles/clauses"},"What is 'Sleep Clause' and other clauses?") +), +preact.h("li",null, +preact.h("a",{href:"https://www.smogon.com/articles/getting-started"},"Next Steps for Competitive Battling") +), +preact.h("li",null, +preact.h("button",{className:"button","data-cmd":"/report"},"Report a user") +), +preact.h("li",null, +preact.h("button",{className:"button","data-cmd":"/join help"},"Join the Help room for live help") +) +), +preact.h("hr",null), +preact.h("strong",null,"Commands:"), +preact.h("p",null,"Within any of the chats, and in private messages, it is possible to type in commands (messages beginning with ", +preact.h("code",null,"/"),") to perform a particular action. A great number of these commands exist, with some only available to certain users. For instance, you can broadcast commands to others with the ",preact.h("code",null,"!")," prefix, but only when you're a player in a battle or a Voice (+) user.", +preact.h("br",null),"For more information on ranks, type ", +preact.h("code",null,"/groups")," in any chat. You can also use the \"chat self\" button on your username in the top right if you need a place to send these commands without joining a room." +), + +preact.h("details",{className:"readmore"}, +preact.h("summary",null,"Here's a list of the most useful commands for the average Pok\xE9mon Showdown experience:"), +preact.h("p",null,"COMMANDS: /report, /msg, /reply, /logout, /challenge, /search, /rating, /whois, /user, /join, /leave, /userauth, /roomauth"), +preact.h("p",null,"BATTLE ROOM COMMANDS: /savereplay, /hideroom, /inviteonly, /invite, /timer, /forfeit"), +preact.h("p",null,"OPTION COMMANDS: /nick, /avatar, /ignore, /status, /away, /busy, /back, /timestamps, /highlight, /showjoins, /hidejoins, /blockchallenges, /blockpms"), +preact.h("p",null,"INFORMATIONAL/RESOURCE COMMANDS: /groups, /faq, /rules, /intro, /formatshelp, /othermetas, /analysis, /punishments, /calc, /git, /cap, /roomhelp, /roomfaq (replace / with ! to broadcast. Broadcasting requires: + % @ # ~)"), +preact.h("p",null,"DATA COMMANDS: /data, /dexsearch, /movesearch, /itemsearch, /learn, /statcalc, /effectiveness, /weakness, /coverage, /randommove, /randompokemon (replace / with ! to broadcast. Broadcasting requires: + % @ # ~)"), +preact.h("p",null,"For an overview of room commands, use ",preact.h("code",null,"/roomhelp")), +preact.h("p",null,"For details of a specific command, you can use ",preact.h("code",null,"/help [command]"),", for example ",preact.h("code",null,"/help data"),".") +), + +preact.h("br",null), +preact.h("p",null,"A complete list of commands available to regular users is provided below. Use ",preact.h("code",null,"/help [commandname]")," for more in-depth information on how to use them."), +preact.h("hr",null), +preact.h("label",{"for":"search"},"Search/filter commands:"),' ', +preact.h("input",{ +name:"search", +placeholder:"search", +style:{width:'25%'}, +value:this.state.search, +onChange:function(e){var _e$target;return _this3.setState({search:toID((_e$target=e.target)==null?void 0:_e$target.value)});}} +), +preact.h("br",null), +preact.h("span",null,this.getCommandList()) +) +); +};_proto2. +getCommandList=function getCommandList(){ +var buf=[]; +var search=this.state.search; +var keys=Object.keys(BattleChatCommands).sort(function(a,b){ + +if(b.endsWith('info'))return 2; + +if(b.includes('chat-commands'))return 1; +var aCount=BattleChatCommands[a].filter(function(x){return toID(x).includes(search);}).length; +var bCount=BattleChatCommands[b].filter(function(x){return toID(x).includes(search);}).length; + +return bCount-aCount||a.localeCompare(b); +}).filter(function(plugin){return!plugin.endsWith('admin');});for(var _i2=0;_i2< + +keys.length;_i2++){var pluginName=keys[_i2]; +var cmdTable=BattleChatCommands[pluginName]; +if(!cmdTable.length)continue; +if(pluginName.startsWith('chat-plugins')){ +pluginName='Chat plugin: '+pluginName.split('/').slice(1).join('/'); +}else if(pluginName.startsWith('chat-commands')){ +pluginName='Core commands: '+pluginName.split('/').slice(1).join('/');; +} +var matchedCmds=[];for(var _i4=0;_i4< +cmdTable.length;_i4++){var cmd=cmdTable[_i4]; +if(search.length&&!toID(cmd).includes(search)){ +continue; +} +matchedCmds.push(preact.h("li",null,cmd)); +} + +buf.push( +preact.h("details",{"class":"readmore"}, +preact.h("summary",null,preact.h("strong",null,pluginName)), +preact.h("ul",null,matchedCmds) +) +); +buf.push(preact.h("br",null)); +} +return buf; +};return ResourcePanel;}(PSRoomPanel);ResourcePanel.id='resources';ResourcePanel.routes=['resources'];ResourcePanel.Model=ResourceRoom;ResourcePanel.icon=preact.h("i",{"class":"fa fa-question-circle","aria-hidden":true});ResourcePanel.title='Resources'; + + +PS.addRoomType(ResourcePanel); +//# sourceMappingURL=page-resources.js.map \ No newline at end of file diff --git a/play.pokemonshowdown.com/js/storage.js b/play.pokemonshowdown.com/js/storage.js index 482892a28..555e2c1f6 100644 --- a/play.pokemonshowdown.com/js/storage.js +++ b/play.pokemonshowdown.com/js/storage.js @@ -32,7 +32,7 @@ Storage.bg = { changeCount: 0, // futureproofing in case we ever add more? // because doing this once was annoying - MENU_BUTTONS: 6, + MENU_BUTTONS: 7, set: function (bgUrl, bgid, noSave) { if (!this.load(bgUrl, bgid)) { this.extractMenuColors(bgUrl, bgid, noSave); diff --git a/play.pokemonshowdown.com/preactalpha.template.html b/play.pokemonshowdown.com/preactalpha.template.html index 9191349ce..79f67b9b3 100644 --- a/play.pokemonshowdown.com/preactalpha.template.html +++ b/play.pokemonshowdown.com/preactalpha.template.html @@ -6,7 +6,7 @@ +#################+,//////, +###################,/////, ########+ +########//// - ////, PokemonShowdown.com + ////, PokemonShowdown.com ########+ +########/// +###################+// +#################+ @@ -106,6 +106,7 @@ https://psim.us/dev + @@ -136,6 +137,7 @@ https://psim.us/dev + diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index 5916dbbcd..51e284a2a 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -657,6 +657,7 @@ class MainMenuPanel extends PSRoomPanel {

    Watch a battle

    Find a user

    Friends

    +

    Info & Resources