From ea95464008414e591b8a7bb61ca7563ddff1f7e5 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Mon, 18 Aug 2025 17:33:19 +0000 Subject: [PATCH] Preact: Support backing up teams (Restoring backups not yet implemented) --- .../src/battle-team-editor.tsx | 14 +- play.pokemonshowdown.com/src/battle-teams.ts | 215 ++++++++++++--- .../src/panel-teambuilder.tsx | 255 ++++++++++++------ .../src/panel-teamdropdown.tsx | 165 +----------- 4 files changed, 361 insertions(+), 288 deletions(-) diff --git a/play.pokemonshowdown.com/src/battle-team-editor.tsx b/play.pokemonshowdown.com/src/battle-team-editor.tsx index f8e1cab1d..eebb69a1e 100644 --- a/play.pokemonshowdown.com/src/battle-team-editor.tsx +++ b/play.pokemonshowdown.com/src/battle-team-editor.tsx @@ -8,7 +8,6 @@ import preact from "../js/lib/preact"; import { type Team, Config } from "./client-main"; -import { PSTeambuilder } from "./panel-teamdropdown"; import { Dex, type ModdedDex, toID, type ID, PSUtils } from "./battle-dex"; import { Teams } from './battle-teams'; import { DexSearch, type SearchRow, type SearchType } from "./battle-dex-search"; @@ -603,7 +602,7 @@ class TeamEditorState extends PSModel { return Teams.export(this.sets, this.dex, !compat); } import(value: string) { - this.sets = PSTeambuilder.importTeam(value); + this.sets = Teams.import(value); this.save(); } getTypeWeakness(type: Dex.TypeName, attackType: Dex.TypeName): 0 | 0.5 | 1 | 2 { @@ -2681,16 +2680,7 @@ class StatForm extends preact.Component<{ }; updateNatureFromPlusMinus = () => { const { set } = this.props; - if (!this.plus || !this.minus) { - delete set.nature; - } else { - for (const i in BattleNatures) { - if (BattleNatures[i as Dex.NatureName].plus === this.plus && BattleNatures[i as Dex.NatureName].minus === this.minus) { - set.nature = i as Dex.NatureName; - break; - } - } - } + set.nature = Teams.getNatureFromPlusMinus(this.plus, this.minus) || undefined; }; /** Converts DV/IV in a textbox to the value in set. */ dvToIv(dvOrIvString?: string): number | null { diff --git a/play.pokemonshowdown.com/src/battle-teams.ts b/play.pokemonshowdown.com/src/battle-teams.ts index 1f82e7893..643803a3f 100644 --- a/play.pokemonshowdown.com/src/battle-teams.ts +++ b/play.pokemonshowdown.com/src/battle-teams.ts @@ -1,5 +1,5 @@ -import { Dex, type ModdedDex } from "./battle-dex"; -import { BattleNatures, BattleStatNames } from "./battle-dex-data"; +import { Dex, toID, type ModdedDex } from "./battle-dex"; +import { BattleNatures, BattleStatNames, BattleStatIDs, type StatNameExceptHP, type ID } from "./battle-dex-data"; export declare namespace Teams { /** @@ -46,6 +46,14 @@ export declare namespace Teams { species: string; moves: string[]; } + export interface Team { + name: string; + format: ID; + folder: string; + /** Note that this can be wrong if `.uploaded?.notLoaded` */ + packedTeam: string; + isBox: boolean; + } } export const Teams = new class { @@ -85,18 +93,10 @@ export const Teams = new class { evs = `|${set.evs['hp'] || ''},${set.evs['atk'] || ''},${set.evs['def'] || ''},` + `${set.evs['spa'] || ''},${set.evs['spd'] || ''},${set.evs['spe'] || ''}`; } - if (evs === '|,,,,,') { - buf += '|'; - } else { - buf += evs; - } + buf += evs === '|,,,,,' ? '|' : evs; // gender - if (set.gender) { - buf += `|${set.gender}`; - } else { - buf += '|'; - } + buf += `|${set.gender || ''}`; // ivs let ivs = '|'; @@ -104,32 +104,16 @@ export const Teams = new class { ivs = `|${getIv(set.ivs, 'hp')},${getIv(set.ivs, 'atk')},${getIv(set.ivs, 'def')},` + `${getIv(set.ivs, 'spa')},${getIv(set.ivs, 'spd')},${getIv(set.ivs, 'spe')}`; } - if (ivs === '|,,,,,') { - buf += '|'; - } else { - buf += ivs; - } + buf += ivs === '|,,,,,' ? '|' : ivs; // shiny - if (set.shiny) { - buf += '|S'; - } else { - buf += '|'; - } + buf += `|${set.shiny ? 'S' : ''}`; // level - if (set.level && set.level !== 100) { - buf += `|${set.level}`; - } else { - buf += '|'; - } + buf += `|${set.level && set.level !== 100 ? set.level : ''}`; // happiness - if (set.happiness !== undefined && set.happiness !== 255) { - buf += `|${set.happiness}`; - } else { - buf += '|'; - } + buf += `|${set.happiness !== undefined && set.happiness !== 255 ? set.happiness : ''}`; if (set.pokeball || set.hpType || set.gigantamax || (set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10) || set.teraType) { @@ -152,6 +136,16 @@ export const Teams = new class { unpack(buf: string): Teams.PokemonSet[] { if (!buf) return []; + // first, detect if this has team metadata + const endIndex = buf.indexOf(']'); + if (endIndex > 0) { + const firstPart = buf.slice(0, endIndex); + const pipeCount = firstPart.split('|').length - 1; + if (pipeCount === 12 || pipeCount === 1) { + buf = buf.slice(buf.indexOf('|') + 1); + } + } + const team = []; let i = 0; let j = 0; @@ -427,4 +421,161 @@ export const Teams = new class { } return text; } + + parseExportedTeamLine(line: string, isFirstLine: boolean, set: Dex.PokemonSet) { + if (isFirstLine || line.startsWith('[')) { + let item; + [line, item] = line.split('@'); + line = line.trim(); + item = item?.trim(); + if (item) { + set.item = item; + if (toID(set.item) === 'noitem') set.item = ''; + } + if (line.endsWith(' (M)')) { + set.gender = 'M'; + line = line.slice(0, -4); + } + if (line.endsWith(' (F)')) { + set.gender = 'F'; + line = line.slice(0, -4); + } + if (line.startsWith('[') && line.endsWith(']')) { + // the ending `]` is necessary to establish this as ability + // (rather than nickname starting with `[`) + set.ability = line.slice(1, -1); + if (toID(set.ability) === 'selectability') { + set.ability = ''; + } + } else if (line) { + const parenIndex = line.lastIndexOf(' ('); + if (line.endsWith(')') && parenIndex !== -1) { + set.species = Dex.species.get(line.slice(parenIndex + 2, -1)).name; + set.name = line.slice(0, parenIndex); + } else { + set.species = Dex.species.get(line).name; + set.name = ''; + } + } + } else if (line.startsWith('Trait: ')) { + set.ability = line.slice(7); + } else if (line.startsWith('Ability: ')) { + set.ability = line.slice(9); + } else if (line.startsWith('Item: ')) { + set.item = line.slice(6); + } else if (line.startsWith('Nickname: ')) { + set.name = line.slice(10); + } else if (line.startsWith('Species: ')) { + set.species = line.slice(9); + } else if (line === 'Shiny: Yes' || line === 'Shiny') { + set.shiny = true; + } else if (line.startsWith('Level: ')) { + set.level = +line.slice(7); + } else if (line.startsWith('Happiness: ')) { + set.happiness = +line.slice(11); + } else if (line.startsWith('Pokeball: ')) { + set.pokeball = line.slice(10); + } else if (line.startsWith('Hidden Power: ')) { + set.hpType = line.slice(14); + } else if (line.startsWith('Dynamax Level: ')) { + set.dynamaxLevel = +line.slice(15); + } else if (line === 'Gigantamax: Yes' || line === 'Gigantamax') { + set.gigantamax = true; + } else if (line.startsWith('Tera Type: ')) { + set.teraType = line.slice(11); + } else if (line.startsWith('EVs: ')) { + const evLines = line.slice(5).split('(')[0].split('/'); + set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }; + let plus = '', minus = ''; + for (let evLine of evLines) { + evLine = evLine.trim(); + const spaceIndex = evLine.indexOf(' '); + if (spaceIndex === -1) continue; + const statid = BattleStatIDs[evLine.slice(spaceIndex + 1)]; + if (!statid) continue; + if (evLine.charAt(spaceIndex - 1) === '+') plus = statid; + if (evLine.charAt(spaceIndex - 1) === '-') minus = statid; + set.evs[statid] = parseInt(evLine.slice(0, spaceIndex), 10) || 0; + } + const nature = this.getNatureFromPlusMinus(plus as StatNameExceptHP, minus as StatNameExceptHP); + if (nature) set.nature = nature; + } else if (line.startsWith('IVs: ')) { + const ivLines = line.slice(5).split(' / '); + set.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 }; + for (let ivLine of ivLines) { + ivLine = ivLine.trim(); + const spaceIndex = ivLine.indexOf(' '); + if (spaceIndex === -1) continue; + const statid = BattleStatIDs[ivLine.slice(spaceIndex + 1)]; + if (!statid) continue; + let statval = parseInt(ivLine.slice(0, spaceIndex), 10); + if (isNaN(statval)) statval = 31; + set.ivs[statid] = statval; + } + } else if (/^[A-Za-z]+ (N|n)ature/.exec(line)) { + let natureIndex = line.indexOf(' Nature'); + if (natureIndex === -1) natureIndex = line.indexOf(' nature'); + if (natureIndex === -1) return; + line = line.slice(0, natureIndex); + if (line !== 'undefined') set.nature = line as Dex.NatureName; + } else if (line.startsWith('-') || line.startsWith('~') || line.startsWith('Move:')) { + if (line.startsWith('Move:')) line = line.slice(4); + line = line.slice(line.charAt(1) === ' ' ? 2 : 1); + if (line.startsWith('Hidden Power [')) { + let hpType = line.slice(14, line.indexOf(']')) as Dex.TypeName; + if (hpType.includes(']') || hpType.includes('[')) hpType = '' as any; + line = 'Hidden Power ' + hpType; + set.hpType = hpType; + } + if (line === 'Frustration' && set.happiness === undefined) { + set.happiness = 0; + } + set.moves.push(line); + } + } + getNatureFromPlusMinus( + plus: StatNameExceptHP | '' | null, minus: StatNameExceptHP | '' | null + ): Dex.NatureName | null { + if (!plus || !minus) return null; + for (const i in BattleNatures) { + if (BattleNatures[i as 'Serious'].plus === plus && BattleNatures[i as 'Serious'].minus === minus) { + return i as Dex.NatureName; + } + } + return null; + } + import(buffer: string): Dex.PokemonSet[] { + const lines = buffer.split("\n"); + + const sets: Dex.PokemonSet[] = []; + let curSet: Dex.PokemonSet | null = null; + + while (lines.length && !lines[0]) lines.shift(); + while (lines.length && !lines[lines.length - 1]) lines.pop(); + + if (lines.length === 1 && lines[0].includes('|')) { + return Teams.unpack(lines[0]); + } + for (let line of lines) { + line = line.trim(); + if (line === '' || line === '---') { + curSet = null; + } else if (line.startsWith('===')) { + // team backup format; ignore + } else if (line.includes('|')) { + // packed format + return Teams.unpack(line); + } else if (!curSet) { + curSet = { + name: '', species: '', gender: '', + moves: [], + }; + sets.push(curSet); + this.parseExportedTeamLine(line, true, curSet); + } else { + this.parseExportedTeamLine(line, false, curSet); + } + } + return sets; + } }; diff --git a/play.pokemonshowdown.com/src/panel-teambuilder.tsx b/play.pokemonshowdown.com/src/panel-teambuilder.tsx index 52dc3657f..3bc37e413 100644 --- a/play.pokemonshowdown.com/src/panel-teambuilder.tsx +++ b/play.pokemonshowdown.com/src/panel-teambuilder.tsx @@ -7,10 +7,46 @@ import { PS, PSRoom, type RoomID, type Team } from "./client-main"; import { PSPanelWrapper, PSRoomPanel } from "./panels"; -import { PSTeambuilder, TeamBox } from "./panel-teamdropdown"; +import { TeamBox } from "./panel-teamdropdown"; import { Dex, PSUtils, toID, type ID } from "./battle-dex"; import { Teams } from "./battle-teams"; import { BattleLog } from "./battle-log"; +import preact from "../js/lib/preact"; + +class PSTextarea extends preact.Component<{ initialValue?: string, name?: string }> { + updateSize = () => { + const textbox = this.base!.querySelector('textarea')!; + const textboxTest = this.base!.querySelector('textarea.heighttester')!; + textboxTest.style.width = `${textbox.offsetWidth}px`; + textboxTest.value = textbox.value; + textbox.setAttribute('data-changed', textbox.value === this.props.initialValue ? '' : '1'); + const newHeight = Math.max(textboxTest.scrollHeight + 40, 50); + textbox.style.height = `${newHeight}px`; + }; + override componentDidMount(): void { + const textbox = this.base!.querySelector('textarea')!; + if (this.props.initialValue) { + textbox.value = this.props.initialValue; + } + this.updateSize(); + window.addEventListener('resize', this.updateSize); + } + override componentWillUnmount(): void { + window.removeEventListener('resize', this.updateSize); + } + override render() { + return
+