Spaces:
Running
Running
/** | |
* Teams | |
* Pokemon Showdown - http://pokemonshowdown.com/ | |
* | |
* Functions for converting and generating teams. | |
* | |
* @license MIT | |
*/ | |
import { Dex, toID } from './dex'; | |
import type { PRNG, PRNGSeed } from './prng'; | |
export interface PokemonSet { | |
/** | |
* Nickname. Should be identical to its base species if not specified | |
* by the player, e.g. "Minior". | |
*/ | |
name: string; | |
/** | |
* Species name (including forme if applicable), e.g. "Minior-Red". | |
* This should always be converted to an id before use. | |
*/ | |
species: string; | |
/** | |
* This can be an id, e.g. "whiteherb" or a full name, e.g. "White Herb". | |
* This should always be converted to an id before use. | |
*/ | |
item: string; | |
/** | |
* This can be an id, e.g. "shieldsdown" or a full name, | |
* e.g. "Shields Down". | |
* This should always be converted to an id before use. | |
*/ | |
ability: string; | |
/** | |
* Each move can be an id, e.g. "shellsmash" or a full name, | |
* e.g. "Shell Smash" | |
* These should always be converted to ids before use. | |
*/ | |
moves: string[]; | |
/** | |
* This can be an id, e.g. "adamant" or a full name, e.g. "Adamant". | |
* This should always be converted to an id before use. | |
*/ | |
nature: string; | |
gender: string; | |
/** | |
* Effort Values, used in stat calculation. | |
* These must be between 0 and 255, inclusive. | |
* | |
* Also used to store AVs for Let's Go | |
*/ | |
evs: StatsTable; | |
/** | |
* Individual Values, used in stat calculation. | |
* These must be between 0 and 31, inclusive. | |
* | |
* These are also used as DVs, or determinant values, in Gens | |
* 1 and 2, which are represented as even numbers from 0 to 30. | |
* | |
* In Gen 2-6, these must match the Hidden Power type. | |
* | |
* In Gen 7+, Bottle Caps means these can either match the | |
* Hidden Power type or 31. | |
*/ | |
ivs: StatsTable; | |
/** | |
* This is usually between 1 and 100, inclusive, | |
* but the simulator supports levels up to 9999 for testing purposes. | |
*/ | |
level: number; | |
/** | |
* While having no direct competitive effect, certain Pokemon cannot | |
* be legally obtained as shiny, either as a whole or with certain | |
* event-only abilities or moves. | |
*/ | |
shiny?: boolean; | |
/** | |
* This is technically "Friendship", but the community calls this | |
* "Happiness". | |
* | |
* It's used to calculate the power of the moves Return and Frustration. | |
* This value must be between 0 and 255, inclusive. | |
*/ | |
happiness?: number; | |
/** | |
* The pokeball this Pokemon is in. Like shininess, this property | |
* has no direct competitive effects, but has implications for | |
* event legality. For example, any Rayquaza that knows V-Create | |
* must be sent out from a Cherish Ball. | |
* | |
* TODO: actually support this in the validator, switching animations, | |
* and the teambuilder. | |
*/ | |
pokeball?: string; | |
/** | |
* Hidden Power type. Optional in older gens, but used in Gen 7+ | |
* because `ivs` contain post-Battle-Cap values. | |
*/ | |
hpType?: string; | |
/** | |
* Dynamax Level. Affects the amount of HP gained when Dynamaxed. | |
* This value must be between 0 and 10, inclusive. | |
*/ | |
dynamaxLevel?: number; | |
gigantamax?: boolean; | |
/** | |
* Tera Type | |
*/ | |
teraType?: string; | |
} | |
export const Teams = new class Teams { | |
pack(team: PokemonSet[] | null): string { | |
if (!team) return ''; | |
function getIv(ivs: StatsTable, s: keyof StatsTable): string { | |
return ivs[s] === 31 || ivs[s] === undefined ? '' : ivs[s].toString(); | |
} | |
let buf = ''; | |
for (const set of team) { | |
if (buf) buf += ']'; | |
// name | |
buf += (set.name || set.species); | |
// species | |
const id = this.packName(set.species || set.name); | |
buf += `|${this.packName(set.name || set.species) === id ? '' : id}`; | |
// item | |
buf += `|${this.packName(set.item)}`; | |
// ability | |
buf += `|${this.packName(set.ability)}`; | |
// moves | |
buf += '|' + set.moves.map(this.packName).join(','); | |
// nature | |
buf += `|${set.nature || ''}`; | |
// evs | |
let evs = '|'; | |
if (set.evs) { | |
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; | |
} | |
// gender | |
if (set.gender) { | |
buf += `|${set.gender}`; | |
} else { | |
buf += '|'; | |
} | |
// ivs | |
let ivs = '|'; | |
if (set.ivs) { | |
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; | |
} | |
// shiny | |
if (set.shiny) { | |
buf += '|S'; | |
} else { | |
buf += '|'; | |
} | |
// level | |
if (set.level && set.level !== 100) { | |
buf += `|${set.level}`; | |
} else { | |
buf += '|'; | |
} | |
// happiness | |
if (set.happiness !== undefined && set.happiness !== 255) { | |
buf += `|${set.happiness}`; | |
} else { | |
buf += '|'; | |
} | |
if (set.pokeball || set.hpType || set.gigantamax || | |
(set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10) || set.teraType) { | |
buf += `,${set.hpType || ''}`; | |
buf += `,${this.packName(set.pokeball || '')}`; | |
buf += `,${set.gigantamax ? 'G' : ''}`; | |
buf += `,${set.dynamaxLevel !== undefined && set.dynamaxLevel !== 10 ? set.dynamaxLevel : ''}`; | |
buf += `,${set.teraType || ''}`; | |
} | |
} | |
return buf; | |
} | |
unpack(buf: string): PokemonSet[] | null { | |
if (!buf) return null; | |
if (typeof buf !== 'string') return buf; | |
if (buf.startsWith('[') && buf.endsWith(']')) { | |
try { | |
buf = this.pack(JSON.parse(buf)); | |
} catch { | |
return null; | |
} | |
} | |
const team = []; | |
let i = 0; | |
let j = 0; | |
// limit to 24 | |
for (let count = 0; count < 24; count++) { | |
const set: PokemonSet = {} as PokemonSet; | |
team.push(set); | |
// name | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
set.name = buf.substring(i, j); | |
i = j + 1; | |
// species | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
set.species = this.unpackName(buf.substring(i, j), Dex.species) || set.name; | |
i = j + 1; | |
// item | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
set.item = this.unpackName(buf.substring(i, j), Dex.items); | |
i = j + 1; | |
// ability | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
const ability = buf.substring(i, j); | |
const species = Dex.species.get(set.species); | |
set.ability = ['', '0', '1', 'H', 'S'].includes(ability) ? | |
species.abilities[ability as '0' || '0'] || (ability === '' ? '' : '!!!ERROR!!!') : | |
this.unpackName(ability, Dex.abilities); | |
i = j + 1; | |
// moves | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
set.moves = buf.substring(i, j).split(',', 24).map(name => this.unpackName(name, Dex.moves)); | |
i = j + 1; | |
// nature | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
set.nature = this.unpackName(buf.substring(i, j), Dex.natures); | |
i = j + 1; | |
// evs | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
if (j !== i) { | |
const evs = buf.substring(i, j).split(',', 6); | |
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, | |
}; | |
} | |
i = j + 1; | |
// gender | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
if (i !== j) set.gender = buf.substring(i, j); | |
i = j + 1; | |
// ivs | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
if (j !== i) { | |
const ivs = buf.substring(i, j).split(',', 6); | |
set.ivs = { | |
hp: ivs[0] === '' ? 31 : Number(ivs[0]) || 0, | |
atk: ivs[1] === '' ? 31 : Number(ivs[1]) || 0, | |
def: ivs[2] === '' ? 31 : Number(ivs[2]) || 0, | |
spa: ivs[3] === '' ? 31 : Number(ivs[3]) || 0, | |
spd: ivs[4] === '' ? 31 : Number(ivs[4]) || 0, | |
spe: ivs[5] === '' ? 31 : Number(ivs[5]) || 0, | |
}; | |
} | |
i = j + 1; | |
// shiny | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
if (i !== j) set.shiny = true; | |
i = j + 1; | |
// level | |
j = buf.indexOf('|', i); | |
if (j < 0) return null; | |
if (i !== j) set.level = parseInt(buf.substring(i, j)); | |
i = j + 1; | |
// happiness | |
j = buf.indexOf(']', i); | |
let misc; | |
if (j < 0) { | |
if (i < buf.length) misc = buf.substring(i).split(',', 6); | |
} else { | |
if (i !== j) misc = buf.substring(i, j).split(',', 6); | |
} | |
if (misc) { | |
set.happiness = (misc[0] ? Number(misc[0]) : 255); | |
set.hpType = misc[1] || ''; | |
set.pokeball = this.unpackName(misc[2] || '', Dex.items); | |
set.gigantamax = !!misc[3]; | |
set.dynamaxLevel = (misc[4] ? Number(misc[4]) : 10); | |
set.teraType = misc[5]; | |
} | |
if (j < 0) break; | |
i = j + 1; | |
} | |
return team; | |
} | |
/** Very similar to toID but without the lowercase conversion */ | |
packName(this: void, name: string | undefined | null) { | |
if (!name) return ''; | |
return name.replace(/[^A-Za-z0-9]+/g, ''); | |
} | |
/** Will not entirely recover a packed name, but will be a pretty readable guess */ | |
unpackName(name: string, dexTable?: { get: (name: string) => AnyObject }) { | |
if (!name) return ''; | |
if (dexTable) { | |
const obj = dexTable.get(name); | |
if (obj.exists) return obj.name; | |
} | |
return name.replace(/([0-9]+)/g, ' $1 ').replace(/([A-Z])/g, ' $1').replace(/[ ][ ]/g, ' ').trim(); | |
} | |
/** | |
* Exports a team in human-readable PS export format | |
*/ | |
export(team: PokemonSet[], options?: { hideStats?: boolean }) { | |
let output = ''; | |
for (const set of team) { | |
output += this.exportSet(set, options) + `\n`; | |
} | |
return output; | |
} | |
exportSet(set: PokemonSet, { hideStats }: { hideStats?: boolean } = {}) { | |
let out = ``; | |
// core | |
if (set.name && set.name !== set.species) { | |
out += `${set.name} (${set.species})`; | |
} else { | |
out += set.species; | |
} | |
if (set.gender === 'M') out += ` (M)`; | |
if (set.gender === 'F') out += ` (F)`; | |
if (set.item) out += ` @ ${set.item}`; | |
out += ` \n`; | |
if (set.ability) { | |
out += `Ability: ${set.ability} \n`; | |
} | |
// details | |
if (set.level && set.level !== 100) { | |
out += `Level: ${set.level} \n`; | |
} | |
if (set.shiny) { | |
out += `Shiny: Yes \n`; | |
} | |
if (typeof set.happiness === 'number' && set.happiness !== 255 && !isNaN(set.happiness)) { | |
out += `Happiness: ${set.happiness} \n`; | |
} | |
if (set.pokeball) { | |
out += `Pokeball: ${set.pokeball} \n`; | |
} | |
if (set.hpType) { | |
out += `Hidden Power: ${set.hpType} \n`; | |
} | |
if (typeof set.dynamaxLevel === 'number' && set.dynamaxLevel !== 10 && !isNaN(set.dynamaxLevel)) { | |
out += `Dynamax Level: ${set.dynamaxLevel} \n`; | |
} | |
if (set.gigantamax) { | |
out += `Gigantamax: Yes \n`; | |
} | |
if (set.teraType) { | |
out += `Tera Type: ${set.teraType} \n`; | |
} | |
// stats | |
if (!hideStats) { | |
if (set.evs) { | |
const stats = Dex.stats.ids().map( | |
stat => set.evs[stat] ? | |
`${set.evs[stat]} ${Dex.stats.shortNames[stat]}` : `` | |
).filter(Boolean); | |
if (stats.length) { | |
out += `EVs: ${stats.join(" / ")} \n`; | |
} | |
} | |
if (set.nature) { | |
out += `${set.nature} Nature \n`; | |
} | |
if (set.ivs) { | |
const stats = Dex.stats.ids().map( | |
stat => (set.ivs[stat] !== 31 && set.ivs[stat] !== undefined) ? | |
`${set.ivs[stat] || 0} ${Dex.stats.shortNames[stat]}` : `` | |
).filter(Boolean); | |
if (stats.length) { | |
out += `IVs: ${stats.join(" / ")} \n`; | |
} | |
} | |
} | |
// moves | |
for (let move of set.moves) { | |
if (move.startsWith(`Hidden Power `) && move.charAt(13) !== '[') { | |
move = `Hidden Power [${move.slice(13)}]`; | |
} | |
out += `- ${move} \n`; | |
} | |
return out; | |
} | |
parseExportedTeamLine(line: string, isFirstLine: boolean, set: PokemonSet, aggressive?: boolean) { | |
if (isFirstLine) { | |
let item; | |
[line, item] = line.split(' @ '); | |
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.endsWith(')') && line.includes('(')) { | |
const [name, species] = line.slice(0, -1).split('('); | |
set.species = Dex.species.get(species).name; | |
set.name = name.trim(); | |
} else { | |
set.species = Dex.species.get(line).name; | |
set.name = ''; | |
} | |
} else if (line.startsWith('Trait: ')) { | |
line = line.slice(7); | |
set.ability = aggressive ? toID(line) : line; | |
} else if (line.startsWith('Ability: ')) { | |
line = line.slice(9); | |
set.ability = aggressive ? toID(line) : line; | |
} else if (line === 'Shiny: Yes') { | |
set.shiny = true; | |
} else if (line.startsWith('Level: ')) { | |
line = line.slice(7); | |
set.level = +line; | |
} else if (line.startsWith('Happiness: ')) { | |
line = line.slice(11); | |
set.happiness = +line; | |
} else if (line.startsWith('Pokeball: ')) { | |
line = line.slice(10); | |
set.pokeball = aggressive ? toID(line) : line; | |
} else if (line.startsWith('Hidden Power: ')) { | |
line = line.slice(14); | |
set.hpType = aggressive ? toID(line) : line; | |
} else if (line.startsWith('Tera Type: ')) { | |
line = line.slice(11); | |
set.teraType = aggressive ? line.replace(/[^a-zA-Z0-9]/g, '') : line; | |
} else if (line === 'Gigantamax: Yes') { | |
set.gigantamax = true; | |
} else if (line.startsWith('EVs: ')) { | |
line = line.slice(5); | |
const evLines = line.split('/'); | |
set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }; | |
for (const evLine of evLines) { | |
const [statValue, statName] = evLine.trim().split(' '); | |
const statid = Dex.stats.getID(statName); | |
if (!statid) continue; | |
const value = parseInt(statValue); | |
set.evs[statid] = value; | |
} | |
} else if (line.startsWith('IVs: ')) { | |
line = line.slice(5); | |
const ivLines = line.split('/'); | |
set.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 }; | |
for (const ivLine of ivLines) { | |
const [statValue, statName] = ivLine.trim().split(' '); | |
const statid = Dex.stats.getID(statName); | |
if (!statid) continue; | |
let value = parseInt(statValue); | |
if (isNaN(value)) value = 31; | |
set.ivs[statid] = value; | |
} | |
} else if (/^[A-Za-z]+ (N|n)ature/.test(line)) { | |
let natureIndex = line.indexOf(' Nature'); | |
if (natureIndex === -1) natureIndex = line.indexOf(' nature'); | |
if (natureIndex === -1) return; | |
line = line.substr(0, natureIndex); | |
if (line !== 'undefined') set.nature = aggressive ? toID(line) : line; | |
} else if (line.startsWith('-') || line.startsWith('~')) { | |
line = line.slice(line.charAt(1) === ' ' ? 2 : 1); | |
if (line.startsWith('Hidden Power [')) { | |
const hpType = line.slice(14, -1); | |
line = 'Hidden Power ' + hpType; | |
if (!set.ivs && Dex.types.isName(hpType)) { | |
set.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 }; | |
const hpIVs = Dex.types.get(hpType).HPivs || {}; | |
for (const statid in hpIVs) { | |
set.ivs[statid as StatID] = hpIVs[statid as StatID]!; | |
} | |
} | |
} | |
if (line === 'Frustration' && set.happiness === undefined) { | |
set.happiness = 0; | |
} | |
set.moves.push(line); | |
} | |
} | |
/** Accepts a team in any format (JSON, packed, or exported) */ | |
import(buffer: string, aggressive?: boolean): PokemonSet[] | null { | |
const sanitize = aggressive ? toID : Dex.getName; | |
if (buffer.startsWith('[')) { | |
try { | |
const team = JSON.parse(buffer); | |
if (!Array.isArray(team)) throw new Error(`Team should be an Array but isn't`); | |
for (const set of team) { | |
set.name = sanitize(set.name); | |
set.species = sanitize(set.species); | |
set.item = sanitize(set.item); | |
set.ability = sanitize(set.ability); | |
set.gender = sanitize(set.gender); | |
set.nature = sanitize(set.nature); | |
const evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }; | |
if (set.evs) { | |
for (const statid in evs) { | |
if (typeof set.evs[statid] === 'number') evs[statid as StatID] = set.evs[statid]; | |
} | |
} | |
set.evs = evs; | |
const ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 }; | |
if (set.ivs) { | |
for (const statid in ivs) { | |
if (typeof set.ivs[statid] === 'number') ivs[statid as StatID] = set.ivs[statid]; | |
} | |
} | |
set.ivs = ivs; | |
if (!Array.isArray(set.moves)) { | |
set.moves = []; | |
} else { | |
set.moves = set.moves.map(sanitize); | |
} | |
} | |
return team; | |
} catch {} | |
} | |
const lines = buffer.split("\n"); | |
const sets: PokemonSet[] = []; | |
let curSet: 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.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 (!curSet) { | |
curSet = { | |
name: '', species: '', item: '', ability: '', gender: '', | |
nature: '', | |
evs: { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }, | |
ivs: { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 }, | |
level: 100, | |
moves: [], | |
}; | |
sets.push(curSet); | |
this.parseExportedTeamLine(line, true, curSet, aggressive); | |
} else { | |
this.parseExportedTeamLine(line, false, curSet, aggressive); | |
} | |
} | |
return sets; | |
} | |
getGenerator(format: Format | string, seed: PRNG | PRNGSeed | null = null) { | |
let TeamGenerator; | |
format = Dex.formats.get(format); | |
const formatID = toID(format); | |
if (formatID.includes('gen9computergeneratedteams')) { | |
TeamGenerator = require(Dex.forFormat(format).dataDir + '/cg-teams').default; | |
} else if (formatID.includes('gen9superstaffbrosultimate')) { | |
TeamGenerator = require(`../data/mods/gen9ssb/random-teams`).default; | |
} else if (formatID.includes('gen9babyrandombattle')) { | |
TeamGenerator = require(`../data/random-battles/gen9baby/teams`).default; | |
} else if (formatID.includes('gen9randombattle') && format.ruleTable?.has('+pokemontag:cap')) { | |
TeamGenerator = require(`../data/random-battles/gen9cap/teams`).default; | |
} else { | |
TeamGenerator = require(`../data/random-battles/${format.mod}/teams`).default; | |
} | |
return new TeamGenerator(format, seed); | |
} | |
generate(format: Format | string, options: PlayerOptions | null = null): PokemonSet[] { | |
return this.getGenerator(format, options?.seed).getTeam(options); | |
} | |
}; | |
export default Teams; | |