mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-10-26 12:17:36 -05:00
Preact: Support backing up teams
(Restoring backups not yet implemented)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user