pokemon-showdown-client/play.pokemonshowdown.com/src/panel-teamdropdown.tsx
Mia ba9f3a529e
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run
Preact: Support remote teams (#2390)
---------

Co-authored-by: Guangcong Luo <guangcongluo@gmail.com>
2025-05-07 20:34:08 -07:00

962 lines
29 KiB
TypeScript

/**
* Team Selector Panel
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license AGPLv3
*/
import { PS, type Team } from "./client-main";
import { PSIcon, PSPanelWrapper, PSRoomPanel } from "./panels";
import { Dex, type ModdedDex, toID, type ID } from "./battle-dex";
import { BattleNatures, BattleStatIDs, BattleStatNames, type StatNameExceptHP } from "./battle-dex-data";
export class PSTeambuilder {
static packTeam(team: Dex.PokemonSet[]) {
let buf = '';
if (!team) return '';
for (const set of team) {
let hasHP = '';
if (buf) buf += ']';
// name
buf += set.name || set.species;
// species
let id = toID(set.species);
buf += `|${toID(set.name || set.species) === id ? '' : id}`;
// item
buf += `|${toID(set.item)}`;
// ability
id = toID(set.ability);
buf += `|${id || '-'}`;
// moves
buf += '|';
if (set.moves) {
for (let j = 0; j < set.moves.length; j++) {
let moveid = toID(set.moves[j]);
if (j && !moveid) continue;
buf += `${j ? ',' : ''}${moveid}`;
if (moveid.substr(0, 11) === 'hiddenpower' && moveid.length > 11) {
hasHP = moveid.slice(11);
}
}
}
// nature
buf += `|${set.nature || ''}`;
// evs
if (set.evs) {
buf += `|${set.evs['hp'] || ''},${set.evs['atk'] || ''},${set.evs['def'] || ''},` +
`${set.evs['spa'] || ''},${set.evs['spd'] || ''},${set.evs['spe'] || ''}`;
} else {
buf += '|';
}
// gender
buf += `|${set.gender || ''}`;
// ivs
if (set.ivs) {
buf += `|${set.ivs['hp'] === 31 ? '' : set.ivs['hp']},${set.ivs['atk'] === 31 ? '' : set.ivs['atk']},` +
`${set.ivs['def'] === 31 ? '' : set.ivs['def']},${set.ivs['spa'] === 31 ? '' : set.ivs['spa']},` +
`${set.ivs['spd'] === 31 ? '' : set.ivs['spd']},${set.ivs['spe'] === 31 ? '' : set.ivs['spe']}`;
} else {
buf += '|';
}
// shiny
if (set.shiny) {
buf += '|S';
} else {
buf += '|';
}
// level
if (set.level) {
buf += `|${set.level}`;
} else {
buf += '|';
}
// happiness
if (set.happiness !== undefined) {
buf += `|${set.happiness}`;
} else {
buf += '|';
}
if (
set.pokeball || (set.hpType && toID(set.hpType) !== hasHP) || set.gigantamax ||
(set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10) || set.teraType
) {
buf += `,${set.hpType || ''}`;
buf += `,${toID(set.pokeball)}`;
buf += `,${set.gigantamax ? 'G' : ''}`;
buf += `,${set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10 ? set.dynamaxLevel : ''}`;
buf += `,${set.teraType || ''}`;
}
}
return buf;
}
static unpackTeam(buf: string) {
if (!buf) return [];
let team: Dex.PokemonSet[] = [];
for (const setBuf of buf.split(`]`)) {
const parts = setBuf.split(`|`);
if (parts.length < 11) continue;
let set: Dex.PokemonSet = { species: '', moves: [] };
team.push(set);
// name
const species = Dex.species.get(parts[1] || parts[0]);
set.name = parts[1] ? parts[0] : species.baseSpecies;
// species
set.species = species.name;
// item
set.item = Dex.items.get(parts[2]).name;
// ability
set.ability =
parts[3] === '-' ? '' :
(species.baseSpecies === 'Zygarde' && parts[3] === 'H') ? 'Power Construct' :
['', '0', '1', 'H', 'S'].includes(parts[3]) ?
species.abilities[parts[3] as '0' || '0'] || (parts[3] === '' ? '' : '!!!ERROR!!!') :
Dex.abilities.get(parts[3]).name;
// moves
set.moves = parts[4].split(',').map(moveid =>
Dex.moves.get(moveid).name
);
// nature
const natureid = toID(parts[5]);
set.nature = natureid.charAt(0).toUpperCase() + natureid.slice(1) as Dex.NatureName;
if (set.nature as any === 'Undefined') set.nature = undefined;
// evs
if (parts[6]) {
if (parts[6].length > 5) {
const evs = parts[6].split(',');
set.evs = {
hp: Number(evs[0]) || 0,
atk: Number(evs[1]) || 0,
def: Number(evs[2]) || 0,
spa: Number(evs[3]) || 0,
spd: Number(evs[4]) || 0,
spe: Number(evs[5]) || 0,
};
} else if (parts[6] === '0') {
set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
}
}
// gender
if (parts[7]) set.gender = parts[7];
// ivs
if (parts[8]) {
const ivs = parts[8].split(',');
set.ivs = {
hp: ivs[0] === '' ? 31 : Number(ivs[0]),
atk: ivs[1] === '' ? 31 : Number(ivs[1]),
def: ivs[2] === '' ? 31 : Number(ivs[2]),
spa: ivs[3] === '' ? 31 : Number(ivs[3]),
spd: ivs[4] === '' ? 31 : Number(ivs[4]),
spe: ivs[5] === '' ? 31 : Number(ivs[5]),
};
}
// shiny
if (parts[9]) set.shiny = true;
// level
if (parts[10]) set.level = parseInt(parts[9], 10);
// happiness
if (parts[11]) {
const misc = parts[11].split(',', 6);
set.happiness = (misc[0] ? Number(misc[0]) : undefined);
set.hpType = misc[1];
set.pokeball = misc[2];
set.gigantamax = !!misc[3];
set.dynamaxLevel = (misc[4] ? Number(misc[4]) : undefined);
set.teraType = misc[5];
}
}
return team;
}
/**
* (Exports end with two spaces so linebreaks are preserved in Markdown;
* I assume mostly for Reddit.)
*/
static exportSet(set: Dex.PokemonSet, dex: ModdedDex = Dex, compat?: boolean) {
let text = '';
// core
if (set.name && set.name !== set.species) {
text += `${set.name} (${set.species})`;
} else {
text += `${set.species}`;
}
if (set.gender === 'M') text += ` (M)`;
if (set.gender === 'F') text += ` (F)`;
if (compat && set.item) {
text += ` @ ${set.item}`;
}
text += `\n`;
if ((set.item || set.ability || dex.gen >= 2) && !compat) {
if (set.ability || dex.gen >= 3) text += `[${set.ability || '(select ability)'}]`;
if (set.item || dex.gen >= 2) text += ` @ ${set.item || "(no item)"}`;
text += `\n`;
} else if (set.ability && set.ability !== 'No Ability') {
text += `Ability: ${set.ability}\n`;
}
if (!compat) {
if (set.moves) {
for (let move of set.moves) {
if (move.startsWith('Hidden Power ')) {
const hpType = move.slice(13);
move = move.slice(0, 13);
move = `${move}[${hpType}]`;
}
text += `- ${move || ''}\n`;
}
}
for (let i = set.moves?.length || 0; i < 4; i++) {
text += `- \n`;
}
}
// stats
let first = true;
if (set.evs || set.nature) {
const nature = BattleNatures[set.nature as 'Serious'];
for (const stat of Dex.statNames) {
const plusMinus = compat ? '' : nature?.plus === stat ? '+' : nature?.minus === stat ? '-' : '';
const ev = set.evs?.[stat] || '';
if (ev === '' && !plusMinus) continue;
text += first ? `EVs: ` : ` / `;
first = false;
text += `${ev}${plusMinus} ${BattleStatNames[stat]}`;
}
}
if (!first) {
if (set.nature && !compat) text += ` (${set.nature})`;
text += `\n`;
}
if (set.nature && compat) {
text += `${set.nature} Nature\n`;
} else if (['Hardy', 'Docile', 'Serious', 'Bashful', 'Quirky'].includes(set.nature!)) {
text += `${set.nature!} Nature\n`;
}
first = true;
if (set.ivs) {
for (const stat of Dex.statNames) {
if (set.ivs[stat] === undefined || isNaN(set.ivs[stat]) || set.ivs[stat] === 31) continue;
if (first) {
text += `IVs: `;
first = false;
} else {
text += ` / `;
}
text += `${set.ivs[stat]} ${BattleStatNames[stat]}`;
}
}
if (!first) {
text += `\n`;
}
// details
if (set.level && set.level !== 100) {
text += `Level: ${set.level}\n`;
}
if (set.shiny) {
text += compat ? `Shiny: Yes\n` : `Shiny\n`;
}
if (typeof set.happiness === 'number' && set.happiness !== 255 && !isNaN(set.happiness)) {
text += `Happiness: ${set.happiness}\n`;
}
if (typeof set.dynamaxLevel === 'number' && set.dynamaxLevel !== 255 && !isNaN(set.dynamaxLevel)) {
text += `Dynamax Level: ${set.dynamaxLevel}\n`;
}
if (set.gigantamax) {
text += compat ? `Gigantamax: Yes\n` : `Gigantamax\n`;
}
if (set.teraType) {
text += `Tera Type: ${set.teraType}\n`;
}
if (compat) {
for (let move of set.moves || []) {
if (move.startsWith('Hidden Power ')) {
const hpType = move.slice(13);
move = move.slice(0, 13);
move = compat ? `${move}[${hpType}]` : `${move}${hpType}`;
}
text += `- ${move}\n`;
}
for (let i = set.moves?.length || 0; i < 4; i++) {
text += `- \n`;
}
}
text += `\n`;
return text;
}
static exportTeam(sets: Dex.PokemonSet[], dex?: ModdedDex, compat?: boolean) {
let text = '';
for (const set of sets) {
// core
text += PSTeambuilder.exportSet(set, dex, compat);
}
return text;
}
static splitPrefix(buffer: string, delimiter: string, prefixOffset = 0): [string, string] {
const delimIndex = buffer.indexOf(delimiter);
if (delimIndex < 0) return ['', buffer];
return [buffer.slice(prefixOffset, delimIndex), buffer.slice(delimIndex + delimiter.length)];
}
static splitLast(buffer: string, delimiter: string): [string, string] {
const delimIndex = buffer.lastIndexOf(delimiter);
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 [')) {
const hpType = line.slice(14, -1) as Dex.TypeName;
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 this.unpackTeam(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 this.unpackTeam(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");
let curTeam: Team | null = null;
let sets: Dex.PokemonSet[] | null = null;
let curSet: Dex.PokemonSet | null = null;
while (lines.length && !lines[0]) lines.shift();
while (lines.length && !lines[lines.length - 1]) lines.pop();
for (let line of lines) {
line = line.trim();
if (line === '' || line === '---') {
curSet = null;
} else if (line.startsWith('===')) {
if (curTeam) {
// not the first team, store the previous team
curTeam.packedTeam = this.packTeam(sets!);
teams.push(curTeam);
}
curTeam = {
name: '',
format: '' as ID,
packedTeam: '',
folder: '',
key: '',
iconCache: '',
};
sets = [];
line = line.slice(3, -3).trim();
[curTeam.format, line] = this.splitPrefix(line, ']', 1) as [ID, string];
if (!curTeam.format) curTeam.format = 'gen8' as ID;
else if (!curTeam.format.startsWith('gen')) curTeam.format = `gen6${curTeam.format}` as ID;
[curTeam.folder, curTeam.name] = this.splitPrefix(line, '/');
} else if (line.includes('|')) {
if (curTeam) {
// not the first team, store the previous team
curTeam.packedTeam = this.packTeam(sets!);
teams.push(curTeam);
}
curTeam = null;
curSet = null;
const team = PS.teams.unpackLine(line);
if (team) teams.push(team);
} else if (!curSet) {
if (!sets) continue; // corruption
curSet = {
name: '', species: '', gender: '',
moves: [],
};
sets.push(curSet);
this.parseExportedTeamLine(line, true, curSet);
} else {
this.parseExportedTeamLine(line, false, curSet);
}
}
if (curTeam) {
curTeam.packedTeam = this.packTeam(sets!);
teams.push(curTeam);
}
return teams;
}
static packedTeamSpecies(buf: string) {
if (!buf) return [];
const team = [];
let i = 0;
while (true) {
const name = buf.slice(i, buf.indexOf('|', i));
i = buf.indexOf('|', i) + 1;
team.push(buf.slice(i, buf.indexOf('|', i)) || name);
for (let k = 0; k < 9; k++) {
i = buf.indexOf('|', i) + 1;
}
i = buf.indexOf(']', i) + 1;
if (i < 1) break;
}
return team;
}
}
export function TeamFolder(props: { cur?: boolean, value: string, children: preact.ComponentChildren }) {
// folders are <div>s rather than <button>s because in theory it has
// less weird interactions with HTML5 drag-and-drop
if (props.cur) {
return <div class="folder cur"><div class="folderhack3">
<div class="folderhack1"></div><div class="folderhack2"></div>
<div class="selectFolder" data-value={props.value}>{props.children}</div>
</div></div>;
}
return <div class="folder">
<div class="selectFolder" data-value={props.value}>{props.children}</div>
</div>;
}
export function TeamBox(props: { team: Team | null, noLink?: boolean, button?: boolean }) {
const team = props.team;
let contents;
if (team) {
team.iconCache ||= team.packedTeam ? (
PSTeambuilder.packedTeamSpecies(team.packedTeam).map(
// can't use <PSIcon>, weird interaction with iconCache
// don't try this at home; I'm a trained professional
pokemon => PSIcon({ pokemon })
)
) : (
<em>(empty team)</em>
);
let format = team.format as string;
if (format.startsWith('gen8')) format = format.slice(4);
format = (format ? `[${format}] ` : ``) + (team.folder ? `${team.folder}/` : ``);
contents = [
<strong>{format && <span>{format}</span>}{team.name}</strong>,
<small>{team.iconCache}</small>,
];
} else {
contents = [
<em>Select a team</em>,
];
}
if (props.button) {
return <button class="team" value={team ? team.key : ''}>
{contents}
</button>;
}
if (props.noLink) {
return <div class="team">
{contents}
</div>;
}
return <a href={`team-${team ? team.key : ''}`} class="team" draggable>
{contents}
</a>;
}
/**
* Team selector popup
*/
class TeamDropdownPanel extends PSRoomPanel {
static readonly id = 'teamdropdown';
static readonly routes = ['teamdropdown'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
gen = '';
format: string | null = null;
getTeams() {
if (!this.format && !this.gen) return PS.teams.list;
return PS.teams.list.filter(team => {
if (this.gen && !team.format.startsWith(this.gen)) return false;
if (this.format && team.format !== this.format) return false;
return true;
});
}
setFormat = (e: MouseEvent) => {
const target = e.currentTarget as HTMLButtonElement;
this.format = (target.name === 'format' && target.value) || '';
this.gen = (target.name === 'gen' && target.value) || '';
this.forceUpdate();
};
click = (e: MouseEvent) => {
let curTarget = e.target as HTMLElement | null;
let target;
while (curTarget && curTarget !== e.currentTarget) {
if (curTarget.tagName === 'BUTTON') {
target = curTarget as HTMLButtonElement;
}
curTarget = curTarget.parentElement;
}
if (!target) return;
PS.teams.loadTeam(PS.teams.byKey[target.value], true);
this.chooseParentValue(target.value);
};
override render() {
const room = this.props.room;
if (!room.parentElem) {
return <PSPanelWrapper room={room}>
<p>Error: You tried to open a team selector, but you have nothing to select a team for.</p>
</PSPanelWrapper>;
}
const baseFormat = room.parentElem.getAttribute('data-format') || Dex.modid;
let isFirstLoad = this.format === null;
if (isFirstLoad) {
this.format = baseFormat;
}
let teams = this.getTeams();
if (!teams.length && this.format && isFirstLoad) {
this.gen = this.format.slice(0, 4);
this.format = '';
teams = this.getTeams();
}
if (!teams.length && this.gen && isFirstLoad) {
this.gen = '';
teams = this.getTeams();
}
let availableWidth = document.body.offsetWidth;
let width = 307;
if (availableWidth > 636) width = 613;
if (availableWidth > 945) width = 919;
let teamBuckets: { [folder: string]: Team[] } = {};
for (const team of teams) {
const list = teamBuckets[team.folder] || (teamBuckets[team.folder] = []);
list.push(team);
}
let teamList = [];
const baseGen = baseFormat.slice(0, 4);
let genList: string[] = [];
for (const team of PS.teams.list) {
const gen = team.format.slice(0, 4);
if (gen && !genList.includes(gen)) genList.push(gen);
}
const hasOtherGens = genList.length > 1 || genList[0] !== baseGen;
teamList.push(<p>
{baseFormat.length > 4 && (
<button
class={'button' + (baseFormat === this.format ? ' disabled' : '')}
onClick={this.setFormat} name="format" value={baseFormat}
>
<i class="fa fa-folder-o" aria-hidden></i> [{baseFormat.slice(0, 4)}] {baseFormat.slice(4)}
</button>
)} {}
<button
class={'button' + (baseGen === this.format ? ' disabled' : '')} onClick={this.setFormat} name="format" value={baseGen}
>
<i class="fa fa-folder-o" aria-hidden></i> [{baseGen}] <em>(uncategorized)</em>
</button> {}
<button
class={'button' + (baseGen === this.gen ? ' disabled' : '')} onClick={this.setFormat} name="gen" value={baseGen}
>
<i class="fa fa-folder-o" aria-hidden></i> [{baseGen}] <em>(all)</em>
</button> {}
{hasOtherGens && !this.gen && (
<button class="button" onClick={this.setFormat} name="gen" value={baseGen}>Other gens</button>
)}
</p>);
if (hasOtherGens && this.gen) {
teamList.push(<h2>Other gens</h2>);
teamList.push(<p>{genList.sort().map(gen => [
<button class={'button' + (gen === this.gen ? ' disabled' : '')} onClick={this.setFormat} name="gen" value={gen}>
<i class="fa fa-folder-o" aria-hidden></i> [{gen}] <em>(all)</em>
</button>,
" ",
])}</p>);
}
let isEmpty = true;
for (let folder in teamBuckets) {
if (folder && (this.gen || this.format)) {
teamList.push(<h2>
<i class="fa fa-folder-open" aria-hidden></i> {folder} + {}
<i class="fa fa-folder-open-o" aria-hidden></i> {this.format || this.gen}
</h2>);
} else if (folder) {
teamList.push(<h2>
<i class="fa fa-folder-open" aria-hidden></i> {folder}
</h2>);
} else if (this.gen || this.format) {
teamList.push(<h2>
<i class="fa fa-folder-open-o" aria-hidden></i> {this.format || this.gen}
</h2>);
} else {
teamList.push(<h2>
<i class="fa fa-folder-open-o" aria-hidden></i> Teams not in any folders
</h2>);
}
teamList.push(<ul class="teamdropdown" onClick={this.click}>
{teamBuckets[folder].map(team => <li key={team.key} style={{ display: 'inline-block' }}>
<TeamBox team={team} button />
</li>)}
</ul>);
isEmpty = false;
}
return <PSPanelWrapper room={room} width={width}><div class="pad">
{teamList}
{isEmpty && <p><em>No teams found</em></p>}
</div></PSPanelWrapper>;
}
}
export interface FormatData {
id: ID;
name: string;
team?: 'preset' | null;
section: string;
column: number;
searchShow?: boolean;
challengeShow?: boolean;
tournamentShow?: boolean;
rated: boolean;
teambuilderLevel?: number | null;
teambuilderFormat?: ID;
battleFormat?: string;
isTeambuilderFormat: boolean;
effectType: 'Format';
}
declare const BattleFormats: { [id: string]: FormatData };
export type SelectType = 'teambuilder' | 'challenge' | 'search';
class FormatDropdownPanel extends PSRoomPanel {
static readonly id = 'formatdropdown';
static readonly routes = ['formatdropdown'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
gen = '' as ID;
format: string | null = null;
search = '';
click = (e: MouseEvent) => {
let curTarget = e.target as HTMLElement | null;
let target;
while (curTarget && curTarget !== e.currentTarget) {
if (curTarget.tagName === 'BUTTON') {
target = curTarget as HTMLButtonElement;
}
curTarget = curTarget.parentElement;
}
if (!target) return;
this.chooseParentValue(target.value);
};
updateSearch = (ev: Event) => {
this.search = (ev.currentTarget as HTMLInputElement).value;
this.forceUpdate();
};
toggleGen = (ev: Event) => {
const target = ev.currentTarget as HTMLButtonElement;
this.gen = this.gen === target.value ? '' as ID : target.value as ID;
this.forceUpdate();
};
override render() {
const room = this.props.room;
if (!room.parentElem) {
return <PSPanelWrapper room={room}>
<p>Error: You tried to open a format selector, but you have nothing to select a format for.</p>
</PSPanelWrapper>;
}
let formatsLoaded = !!window.BattleFormats;
if (formatsLoaded) {
formatsLoaded = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (let i in window.BattleFormats) {
formatsLoaded = true;
break;
}
}
const curGen = (gen: string) => this.gen === gen ? ' cur' : '';
const searchBar = <div style="margin-bottom: 0.5em">
<input
type="search" name="search" placeholder="Search formats" class="textbox autofocus"
onInput={this.updateSearch} onChange={this.updateSearch}
/> {}
<button onClick={this.toggleGen} value="gen9" class={`button button-first${curGen('gen9')}`}>Gen 9</button>
<button onClick={this.toggleGen} value="gen8" class={`button button-middle${curGen('gen8')}`}>8</button>
<button onClick={this.toggleGen} value="gen7" class={`button button-middle${curGen('gen7')}`}>7</button>
<button onClick={this.toggleGen} value="gen6" class={`button button-middle${curGen('gen6')}`}>6</button>
<button onClick={this.toggleGen} value="gen5" class={`button button-middle${curGen('gen5')}`}>5</button>
<button onClick={this.toggleGen} value="gen4" class={`button button-middle${curGen('gen4')}`}>4</button>
<button onClick={this.toggleGen} value="gen3" class={`button button-middle${curGen('gen3')}`}>3</button>
<button onClick={this.toggleGen} value="gen2" class={`button button-middle${curGen('gen2')}`}>2</button>
<button onClick={this.toggleGen} value="gen1" class={`button button-last${curGen('gen1')}`}>1</button>
</div>;
if (!formatsLoaded) {
return <PSPanelWrapper room={room}><div class="pad">
{searchBar}
<p>Loading...</p>
</div></PSPanelWrapper>;
}
/**
* 'challenge' hides search-only formats, and 'search' hides challenge-only
* formats. 'teambuilder' shows teambuilder formats (removing parentheses
* from format names).
*/
const selectType: SelectType = (
room.parentElem.getAttribute('data-selecttype') as any || 'challenge'
);
const curFormat = toID((room.parentElem as HTMLButtonElement).value);
const formats = Object.values(BattleFormats).filter(format => {
if (selectType === 'challenge' && format.challengeShow === false) return false;
if (selectType === 'search' && format.searchShow === false) return false;
if (selectType === 'teambuilder' && format.team) return false;
return true;
});
let curSection = '';
let curColumnNum = 0;
let curColumn: ({ id: ID, name: string, section: string } | { id: null, section: string })[] = [];
const columns = [curColumn];
const searchID = toID(this.search);
for (const format of formats) {
if (searchID && !toID(format.name).includes(searchID)) {
continue;
}
if (this.gen && !format.id.startsWith(this.gen)) continue;
if (format.column !== curColumnNum) {
if (curColumn.length) {
curColumn = [];
columns.push(curColumn);
}
curColumnNum = format.column;
}
if (format.section !== curSection) {
curSection = format.section;
if (curSection) {
curColumn.push({ id: null, section: curSection });
}
}
curColumn.push(format);
}
if (this.gen && selectType === 'teambuilder') {
columns[0].unshift({
id: this.gen,
name: `[Gen ${this.gen.slice(3)}]`,
section: 'No Format',
});
}
const width = Math.max(columns.length, 2.1) * 225 + 30;
const noResults = curColumn.length === 0;
return <PSPanelWrapper room={room} width={width}><div class="pad">
{searchBar}
{columns.map(column => <ul class="options" onClick={this.click}>
{column.map(format => format.id ? (
<li><button value={format.name} class={`option${curFormat === format.id ? ' cur' : ''}`}>
{format.name.replace('[Gen 8 ', '[').replace('[Gen 9] ', '').replace('[Gen 7 ', '[')}
{format.section === 'No Format' && <em> (uncategorized)</em>}
</button></li>
) : (
<li><h3>
{format.section}
</h3></li>
))}
</ul>)}
{noResults && <p>
<em>No formats{!!searchID && ` matching "${searchID}"`} found</em>
</p>}
<div style="float: left"></div>
</div></PSPanelWrapper>;
}
}
PS.addRoomType(TeamDropdownPanel, FormatDropdownPanel);