diff --git a/data/rulesets.ts b/data/rulesets.ts index 7fe0954628..db32b946b3 100644 --- a/data/rulesets.ts +++ b/data/rulesets.ts @@ -3257,4 +3257,10 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = { if (!speciesMods.length) throw new Error('This format has no rules that modify base stats.'); }, }, + editbattle: { + effectType: "Rule", + name: "Edit Battle", + desc: "Allows players to edit battle details using `/editbattle`.", + // Implemented in chat-commands/admin.ts + }, }; diff --git a/server/chat-commands/admin.ts b/server/chat-commands/admin.ts index 9e5fdee50a..52ab3eed3d 100644 --- a/server/chat-commands/admin.ts +++ b/server/chat-commands/admin.ts @@ -1621,12 +1621,18 @@ export const commands: Chat.ChatCommands = { ebat: 'editbattle', editbattle(target, room, user) { room = this.requireRoom(); - this.checkCan('forcewin'); if (!target) return this.parse('/help editbattle'); if (!room.battle) { throw new Chat.ErrorMessage("/editbattle - This is not a battle room."); } const battle = room.battle; + const format = Dex.formats.get(battle.format, true); + const ruleTable = Dex.formats.getRuleTable(format); + if (ruleTable.has('editbattle')) { + this.checkCan('editprivacy', null, room); + } else { + this.checkCan('forcewin'); + } void battle.stream.write(`>editbattle user:${user.name}, ${target}`); }, editbattlehelp: [ @@ -1639,9 +1645,11 @@ export const commands: Chat.ChatCommands = { `/editbattle fieldcondition [fieldcondition]`, `/editbattle weather [weather]`, `/editbattle terrain [terrain]`, + `/editbattle basestats [player], [pokemon], [hp], [atk], [def], [spa], [spd], [spe]`, `/editbattle reseed [optional seed]`, - `Short forms: /ebat h OR s OR pp OR b OR v OR sc OR fc OR w OR t`, + `Short forms: /ebat h OR s OR pp OR b OR v OR sc OR fc OR w OR t OR bs`, `[player] must be a username or number, [pokemon] must be species name or party slot number (not nickname), [move] must be move name.`, + `Modified base stats must be integers within the stat limit (1-255).`, ], }; diff --git a/sim/battle-stream.ts b/sim/battle-stream.ts index fda9a5b4dc..ca98d41ffc 100644 --- a/sim/battle-stream.ts +++ b/sim/battle-stream.ts @@ -373,6 +373,48 @@ export class BattleStream extends Streams.ObjectReadWriteStream { if (targets.length) battle.add(`||Reseeded to ${targets.join(',')}`); break; } + case 'basestats': + case 'bs': { + if (targets.length !== 8) { + battle.add("||<<< Error: Format should be: basestats PLAYER, POKEMON, HP, ATK, DEF, SPA, SPD, SPE"); + return; + } + const [player, pokemon, hpStr, atkStr, defStr, spaStr, spdStr, speStr] = targets.map(t => t.trim()); + const p = getPokemon(player, pokemon); + const hp = Number(hpStr); + const atk = Number(atkStr); + const def = Number(defStr); + const spa = Number(spaStr); + const spd = Number(spdStr); + const spe = Number(speStr); + if (![hp, atk, def, spa, spd, spe].every(stat => Number.isInteger(stat))) { + battle.add("||<<< Error: All stats must be integers between 1 and 255."); + return; + } + if ([hp, atk, def, spa, spd, spe].some(stat => stat < 1 || stat > 255)) { + battle.add("||<<< Error: Stats must be integers between 1 and 255."); + return; + } + const newBaseStats: StatsTable = { hp, atk, def, spa, spd, spe }; + const oldMaxhp = p.maxhp; + const oldHp = p.hp; + const wasFainted = p.fainted; + const newStats = battle.spreadModify(newBaseStats, p.set); + p.baseStoredStats = newStats; + p.baseMaxhp = newStats.hp; + p.maxhp = newStats.hp; + if (wasFainted) { + p.hp = 0; + } else { + const hpRatio = oldMaxhp ? oldHp / oldMaxhp : 1; + p.hp = Math.max(1, Math.min(p.maxhp, Math.round(hpRatio * p.maxhp))); + } + for (const stat in p.storedStats) { + p.storedStats[stat as StatIDExceptHP] = newStats[stat as StatIDExceptHP]; + } + p.speed = p.storedStats.spe; + break; + } default: throw new Error(`Unknown editbattle command: ${cmd}`); }