Preact: Support backing up teams

(Restoring backups not yet implemented)
This commit is contained in:
Guangcong Luo
2025-08-18 17:33:19 +00:00
parent 1aa237de65
commit ea95464008
4 changed files with 361 additions and 288 deletions

View File

@@ -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 {

View File

@@ -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;
}
};

View File

@@ -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<HTMLTextAreaElement>('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 <div style="position:relative">
<textarea
name={this.props.name} class="textbox" onInput={this.updateSize} onKeyUp={this.updateSize}
style="box-sizing:border-box;width:100%;resize:none"
/>
<div><textarea
class="textbox heighttester"
style="box-sizing:border-box;resize:none;height:50px;visibility:hidden;position:absolute;left:-200px"
/></div>
</div>;
}
}
class TeambuilderRoom extends PSRoom {
readonly DEFAULT_FORMAT = Dex.modid;
@@ -25,6 +61,8 @@ class TeambuilderRoom extends PSRoom {
curFolder = '';
curFolderKeep = '';
searchTerms: string[] = [];
exportMode = false;
exportCode: string | null = null;
override clientCommands = this.parseClientCommands({
'newteam'(target) {
@@ -45,11 +83,20 @@ class TeambuilderRoom extends PSRoom {
PS.teams.undelete();
this.update(null);
},
'backup'() {
this.setExportMode(!this.exportMode);
this.update(null);
},
});
override sendDirect(msg: string): void {
PS.alert(`Unrecognized command: ${msg}`);
}
setExportMode(exportMode: boolean) {
if (exportMode === this.exportMode) return;
this.exportMode = exportMode;
this.exportCode = null;
}
createTeam(copyFrom?: Team | null, isBox = false): Team {
if (copyFrom) {
return {
@@ -82,8 +129,7 @@ class TeambuilderRoom extends PSRoom {
this.searchTerms = value.split(",").map(q => q.trim().toLowerCase());
}
};
matchesSearch = (team: Team | null) => {
if (!team) return false;
matchesSearch = (team: Team) => {
if (this.searchTerms.length === 0) return true;
const normalized = team.packedTeam.toLowerCase();
return this.searchTerms.every(term => normalized.includes(term));
@@ -215,7 +261,7 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
return file.text?.()?.then(result => {
let sets;
try {
sets = PSTeambuilder.importTeam(result);
sets = Teams.import(result);
} catch {
PS.alert(`Your file "${file.name}" is not a valid team.`);
return null;
@@ -349,6 +395,9 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
<button class={`selectFolder${active}`} data-value={value}>{children}</button>
</div>;
}
saveExport = (e: MouseEvent) => {
alert("Unimplemented");
};
renderFolderList() {
const room = this.props.room;
// The folder list isn't actually saved anywhere:
@@ -438,11 +487,30 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
<div class="folderlistafter"></div>
</div>;
}
visibleTeams(teams?: Team[]): Team[];
visibleTeams(teams: (Team | null)[]): (Team | null)[];
visibleTeams(teams: (Team | null)[] = PS.teams.list): (Team | null)[] {
const { room } = this.props;
override render() {
if (room.curFolder) {
if (room.curFolder.endsWith('/')) {
const filterFolder = room.curFolder.slice(0, -1);
teams = teams.filter(team => !team || team.folder === filterFolder);
} else {
const filterFormat = room.curFolder;
teams = teams.filter(team => !team || team.format === filterFormat);
}
}
if (!room.searchTerms.length) return teams;
const filteredTeams = teams.filter(team => !team || room.matchesSearch(team));
return filteredTeams;
}
renderTeamPane() {
const room = this.props.room;
let teams: (Team | null)[] = PS.teams.list.slice();
let teams: (Team | null)[] = PS.teams.list.slice();
let isDragging = false;
if (PS.dragging?.type === 'team' && typeof PS.dragging.team === 'number') {
teams.splice(PS.dragging.team, 0, null);
@@ -458,93 +526,118 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
if (room.curFolder) {
if (room.curFolder.endsWith('/')) {
filterFolder = room.curFolder.slice(0, -1);
teams = teams.filter(team => !team || team.folder === filterFolder);
teamTerm = 'team in folder';
} else {
filterFormat = room.curFolder;
teams = teams.filter(team => !team || team.format === filterFormat);
if (filterFormat !== Dex.modid) teamTerm = BattleLog.formatName(filterFormat) + ' team';
}
}
const filteredTeams = teams.filter(room.matchesSearch);
const filteredTeams = this.visibleTeams(teams);
if (room.exportMode) {
return <div class="teampane">
<p>
<button data-cmd="/backup" class="button">
<i class="fa fa-caret-left" aria-hidden></i> Back
</button> {}
<button onClick={this.saveExport} class="button" disabled>
<i class="fa fa-save" aria-hidden></i> Save (unimplemented)
</button>
</p>
<PSTextarea
name="import" initialValue={(room.exportCode ??= PS.teams.packAll(filteredTeams.filter(Boolean) as Team[]))}
/>
</div>;
}
return <div class="teampane">
{filterFolder ? (
<h2>
<i class="fa fa-folder-open" aria-hidden></i> {filterFolder} {}
<button class="button small" style="margin-left:5px" name="renameFolder">
<i class="fa fa-pencil" aria-hidden></i> Rename
</button> {}
<button class="button small" style="margin-left:5px" name="promptDeleteFolder">
<i class="fa fa-times" aria-hidden></i> Remove
</button>
</h2>
) : filterFolder === '' ? (
<h2><i class="fa fa-folder-open-o" aria-hidden></i> Teams not in any folders</h2>
) : filterFormat ? (
<h2><i class="fa fa-folder-open-o" aria-hidden></i> {filterFormat} <small>({teams.length})</small></h2>
) : (
<h2>All Teams <small>({teams.length})</small></h2>
)}
<p>
<button data-cmd="/newteam" class="button big">
<i class="fa fa-plus-circle" aria-hidden></i> New {teamTerm}
</button> {}
<button data-cmd="/newteam box" class="button">
<i class="fa fa-archive" aria-hidden></i> New box
</button>
<input
type="search" class="textbox" placeholder="Search teams"
style="margin-left:5px;" onKeyUp={this.updateSearch}
></input>
</p>
<ul class="teamlist">
{!teams.length ? (
<li><em>you have no teams lol</em></li>
) : !filteredTeams.length ? (
<li><em>you have no teams matching <code>{room.searchTerms.join(", ")}</code></em></li>
) : filteredTeams.map(team => team ? (
<li key={team.key} onDragEnter={this.dragEnterTeam} data-teamkey={team.key}>
<TeamBox team={team} onClick={this.clearSearch} /> {}
{!team.uploaded && <button data-cmd={`/deleteteam ${team.key}`} class="option">
<i class="fa fa-trash" aria-hidden></i> Delete
</button>} {}
{team.uploaded?.private ? (
<i class="fa fa-cloud gray"></i>
) : team.uploaded ? (
<i class="fa fa-globe gray"></i>
) : team.teamid ? (
<i class="fa fa-plug gray"></i>
) : (
null
)}
</li>
) : isDragging ? (
<li key="dragging">
<div class="team"></div>
</li>
) : (
<li key="undelete">
<button data-cmd="/undeleteteam" class="option">
<i class="fa fa-undo" aria-hidden></i> Undo delete
</button>
</li>
))}
</ul>
<p>
<button data-cmd="/newteam bottom" class="button">
<i class="fa fa-plus-circle" aria-hidden></i> New {teamTerm}
</button> {}
<button data-cmd="/newteam box bottom" class="button">
<i class="fa fa-archive" aria-hidden></i> New box
</button>
</p>
<p>
<button data-cmd="/backup" class="button">
<i class="fa fa-file-code-o" aria-hidden></i> Backup
{room.searchTerms.length ? ' search results' : room.curFolder ? ' folder' : ''}
</button>
</p>
</div>;
}
override render() {
const room = this.props.room;
return <PSPanelWrapper room={room}>
<div class="folderpane">
{this.renderFolderList()}
</div>
<div class="teampane">
{filterFolder ? (
<h2>
<i class="fa fa-folder-open" aria-hidden></i> {filterFolder} {}
<button class="button small" style="margin-left:5px" name="renameFolder">
<i class="fa fa-pencil" aria-hidden></i> Rename
</button> {}
<button class="button small" style="margin-left:5px" name="promptDeleteFolder">
<i class="fa fa-times" aria-hidden></i> Remove
</button>
</h2>
) : filterFolder === '' ? (
<h2><i class="fa fa-folder-open-o" aria-hidden></i> Teams not in any folders</h2>
) : filterFormat ? (
<h2><i class="fa fa-folder-open-o" aria-hidden></i> {filterFormat} <small>({teams.length})</small></h2>
) : (
<h2>All Teams <small>({teams.length})</small></h2>
)}
<p>
<button data-cmd="/newteam" class="button big">
<i class="fa fa-plus-circle" aria-hidden></i> New {teamTerm}
</button> {}
<button data-cmd="/newteam box" class="button">
<i class="fa fa-archive" aria-hidden></i> New box
</button>
<input
type="search" class="textbox" placeholder="Search teams"
style="margin-left:5px;" onKeyUp={this.updateSearch}
></input>
</p>
<ul class="teamlist">
{!teams.length ? (
<li><em>you have no teams lol</em></li>
) : !filteredTeams.length ? (
<li><em>you have no teams matching <code>{room.searchTerms.join(", ")}</code></em></li>
) : filteredTeams.map(team => team ? (
<li key={team.key} onDragEnter={this.dragEnterTeam} data-teamkey={team.key}>
<TeamBox team={team} onClick={this.clearSearch} /> {}
{!team.uploaded && <button data-cmd={`/deleteteam ${team.key}`} class="option">
<i class="fa fa-trash" aria-hidden></i> Delete
</button>} {}
{team.uploaded?.private ? (
<i class="fa fa-cloud gray"></i>
) : team.uploaded ? (
<i class="fa fa-globe gray"></i>
) : team.teamid ? (
<i class="fa fa-plug gray"></i>
) : (
null
)}
</li>
) : isDragging ? (
<li key="dragging">
<div class="team"></div>
</li>
) : (
<li key="undelete">
<button data-cmd="/undeleteteam" class="option">
<i class="fa fa-undo" aria-hidden></i> Undo delete
</button>
</li>
))}
</ul>
<p>
<button data-cmd="/newteam bottom" class="button">
<i class="fa fa-plus-circle" aria-hidden></i> New {teamTerm}
</button> {}
<button data-cmd="/newteam box bottom" class="button">
<i class="fa fa-archive" aria-hidden></i> New box
</button>
</p>
</div>
{this.renderTeamPane()}
</PSPanelWrapper>;
}
}

View File

@@ -8,7 +8,6 @@
import { PS, type Team } from "./client-main";
import { PSIcon, PSPanelWrapper, PSRoomPanel } from "./panels";
import { Dex, toID, type ID } from "./battle-dex";
import { BattleNatures, BattleStatIDs, type StatNameExceptHP } from "./battle-dex-data";
import { Teams } from "./battle-teams";
export class PSTeambuilder {
@@ -27,166 +26,6 @@ export class PSTeambuilder {
if (delimIndex < 0) return [buffer, ''];
return [buffer.slice(0, delimIndex), buffer.slice(delimIndex + delimiter.length)];
}
static 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.getNature(plus as StatNameExceptHP, minus as StatNameExceptHP);
if (nature !== 'Serious') {
set.nature = nature as Dex.NatureName;
}
} 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);
}
}
static getNature(plus: StatNameExceptHP | '', minus: StatNameExceptHP | '') {
if (!plus || !minus) {
return 'Serious';
}
for (const i in BattleNatures) {
if (BattleNatures[i as 'Serious'].plus === plus && BattleNatures[i as 'Serious'].minus === minus) {
return i;
}
}
return 'Serious';
}
static importTeam(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
const team = PS.teams.unpackLine(line);
if (!team) continue;
return Teams.unpack(team.packedTeam);
} else if (!curSet) {
curSet = {
name: '', species: '', gender: '',
moves: [],
};
sets.push(curSet);
this.parseExportedTeamLine(line, true, curSet);
} else {
this.parseExportedTeamLine(line, false, curSet);
}
}
return sets;
}
static importTeamBackup(buffer: string): Team[] {
const teams: Team[] = [];
const lines = buffer.split("\n");
@@ -244,9 +83,9 @@ export class PSTeambuilder {
moves: [],
};
sets.push(curSet);
this.parseExportedTeamLine(line, true, curSet);
Teams.parseExportedTeamLine(line, true, curSet);
} else {
this.parseExportedTeamLine(line, false, curSet);
Teams.parseExportedTeamLine(line, false, curSet);
}
}
if (curTeam) {