diff --git a/play.pokemonshowdown.com/src/battle-team-editor.tsx b/play.pokemonshowdown.com/src/battle-team-editor.tsx index 3fecae8b2..f9ba7f20c 100644 --- a/play.pokemonshowdown.com/src/battle-team-editor.tsx +++ b/play.pokemonshowdown.com/src/battle-team-editor.tsx @@ -17,6 +17,7 @@ import { BattleNatures, BattleStatNames, type StatName } from "./battle-dex-data import { BattleStatGuesser, BattleStatOptimizer } from "./battle-tooltips"; import { PSModel } from "./client-core"; import { Net } from "./client-connection"; +import { PSIcon } from "./panels"; type SelectionType = 'pokemon' | 'ability' | 'item' | 'move' | 'stats' | 'details'; @@ -28,6 +29,18 @@ type SampleSets = { type SampleSetsTable = { dex?: SampleSets, stats?: SampleSets }; class TeamEditorState extends PSModel { + static clipboard: { + teams: { + [teamKey: string]: { + team: Team, + sets: { [index: number]: Dex.PokemonSet }, + /** whether to delete the team itself when moving it */ + entire: boolean, + }, + } | null, + otherSets: Dex.PokemonSet[] | null, + readonly: boolean, + } | null = null; team: Team; sets: Dex.PokemonSet[] = []; lastPackedTeam = ''; @@ -221,6 +234,79 @@ class TeamEditorState extends PSModel { this.sets.splice(this.deletedSet.index, 0, this.deletedSet.set); this.deletedSet = null; } + copySet(index: number) { + if (this.sets.length <= index) return; + + TeamEditorState.clipboard ||= { + teams: {}, + otherSets: null, + readonly: false, + }; + TeamEditorState.clipboard.teams ||= {}; + TeamEditorState.clipboard.teams[this.team.key] ||= { + team: this.team, sets: {}, entire: false, + }; + if (this.readonly) TeamEditorState.clipboard.readonly = true; + + if (TeamEditorState.clipboard.teams[this.team.key].sets[index] === this.sets[index]) { + // remove + delete TeamEditorState.clipboard.teams[this.team.key].sets[index]; + if (!Object.keys(TeamEditorState.clipboard.teams[this.team.key].sets).length) { + delete TeamEditorState.clipboard.teams[this.team.key]; + } + if (!Object.keys(TeamEditorState.clipboard.teams).length) { + TeamEditorState.clipboard.teams = null; + if (!TeamEditorState.clipboard.otherSets) { + TeamEditorState.clipboard = null; + } + } + return; + } + TeamEditorState.clipboard.teams[this.team.key].sets[index] = this.sets[index]; + } + pasteSet(index: number, isMove?: boolean) { + if (!TeamEditorState.clipboard) return; + if (this.readonly) return; + + if (isMove) { + if (TeamEditorState.clipboard.readonly) return; + + for (const key in TeamEditorState.clipboard.teams) { + const clipboardTeam = TeamEditorState.clipboard.teams[key]; + const sources = Object.keys(clipboardTeam.sets).map(Number); + // descending order, so splices won't affect future indices + sources.sort((a, b) => -(a - b)); + for (const source of sources) { + if (key === this.team.key) { + this.sets.splice(source, 1); + if (source < index) index--; + } else { + const team = clipboardTeam.team; + const sets = Teams.unpack(team.packedTeam); + sets.splice(source, 1); + team.packedTeam = Teams.pack(sets); + } + } + } + } + + const sets: Dex.PokemonSet[] = []; + for (const key in TeamEditorState.clipboard.teams) { + const clipboardTeam = TeamEditorState.clipboard.teams[key]; + for (const set of Object.values(clipboardTeam.sets)) { + sets.push(set); + } + } + sets.push(...TeamEditorState.clipboard.otherSets || []); + + for (const set of sets) { + // not the most efficient way to deepclone but we don't need efficiency here + const newSet = JSON.parse(JSON.stringify(set)) as Dex.PokemonSet; + this.sets.splice(index, 0, newSet); + index++; + } + TeamEditorState.clipboard = null; + } ignoreRows = ['header', 'sortpokemon', 'sortmove', 'html']; downSearchValue() { if (!this.search.results || this.searchIndex >= this.search.results.length - 1) return; @@ -743,9 +829,34 @@ export class TeamEditor extends preact.Component<{