`;
+ buf += ``;
+ return buf;
+}
+
+function generateSSBMoveInfo(sigMove: Move, dex: ModdedDex) {
+ let buf = ``;
+ if (sigMove.shortDesc || sigMove.desc) {
+ buf += ``;
+ buf += Chat.getDataMoveHTML(sigMove);
+ const details: {[k: string]: string} = {
+ Priority: String(sigMove.priority),
+ Gen: String(sigMove.gen || 9),
+ };
+
+ if (sigMove.isNonstandard === "Past" && dex.gen >= 8) details["✗ Past Gens Only"] = "";
+ if (sigMove.secondary || sigMove.secondaries || sigMove.hasSheerForce) details["✓ Boosted by Sheer Force"] = "";
+ if (sigMove.flags['contact'] && dex.gen >= 3) details["✓ Contact"] = "";
+ if (sigMove.flags['sound'] && dex.gen >= 3) details["✓ Sound"] = "";
+ if (sigMove.flags['bullet'] && dex.gen >= 6) details["✓ Bullet"] = "";
+ if (sigMove.flags['pulse'] && dex.gen >= 6) details["✓ Pulse"] = "";
+ if (!sigMove.flags['protect'] && sigMove.target !== 'self') details["✓ Bypasses Protect"] = "";
+ if (sigMove.flags['bypasssub']) details["✓ Bypasses Substitutes"] = "";
+ if (sigMove.flags['defrost']) details["✓ Thaws user"] = "";
+ if (sigMove.flags['bite'] && dex.gen >= 6) details["✓ Bite"] = "";
+ if (sigMove.flags['punch'] && dex.gen >= 4) details["✓ Punch"] = "";
+ if (sigMove.flags['powder'] && dex.gen >= 6) details["✓ Powder"] = "";
+ if (sigMove.flags['reflectable'] && dex.gen >= 3) details["✓ Bounceable"] = "";
+ if (sigMove.flags['charge']) details["✓ Two-turn move"] = "";
+ if (sigMove.flags['recharge']) details["✓ Has recharge turn"] = "";
+ if (sigMove.flags['gravity'] && dex.gen >= 4) details["✗ Suppressed by Gravity"] = "";
+ if (sigMove.flags['dance'] && dex.gen >= 7) details["✓ Dance move"] = "";
+ if (sigMove.flags['slicing'] && dex.gen >= 9) details["✓ Slicing move"] = "";
+ if (sigMove.flags['wind'] && dex.gen >= 9) details["✓ Wind move"] = "";
+
+ if (sigMove.zMove?.basePower) {
+ details["Z-Power"] = String(sigMove.zMove.basePower);
+ } else if (sigMove.zMove?.effect) {
+ const zEffects: {[k: string]: string} = {
+ clearnegativeboost: "Restores negative stat stages to 0",
+ crit2: "Crit ratio +2",
+ heal: "Restores HP 100%",
+ curse: "Restores HP 100% if user is Ghost type, otherwise Attack +1",
+ redirect: "Redirects opposing attacks to user",
+ healreplacement: "Restores replacement's HP 100%",
+ };
+ details["Z-Effect"] = zEffects[sigMove.zMove.effect];
+ } else if (sigMove.zMove?.boost) {
+ details["Z-Effect"] = "";
+ const boost = sigMove.zMove.boost;
+ for (const h in boost) {
+ details["Z-Effect"] += ` ${Dex.stats.mediumNames[h as 'atk']} +${boost[h as 'atk']}`;
+ }
+ } else if (sigMove.isZ && typeof sigMove.isZ === 'string') {
+ details["✓ Z-Move"] = "";
+ const zCrystal = dex.items.get(sigMove.isZ);
+ details["Z-Crystal"] = zCrystal.name;
+ if (zCrystal.itemUser) {
+ details["User"] = zCrystal.itemUser.join(", ");
+ details["Required Move"] = dex.items.get(sigMove.isZ).zMoveFrom!;
+ }
+ } else {
+ details["Z-Effect"] = "None";
+ }
+
+ const targetTypes: {[k: string]: string} = {
+ normal: "One Adjacent Pok\u00e9mon",
+ self: "User",
+ adjacentAlly: "One Ally",
+ adjacentAllyOrSelf: "User or Ally",
+ adjacentFoe: "One Adjacent Opposing Pok\u00e9mon",
+ allAdjacentFoes: "All Adjacent Opponents",
+ foeSide: "Opposing Side",
+ allySide: "User's Side",
+ allyTeam: "User's Side",
+ allAdjacent: "All Adjacent Pok\u00e9mon",
+ any: "Any Pok\u00e9mon",
+ all: "All Pok\u00e9mon",
+ scripted: "Chosen Automatically",
+ randomNormal: "Random Adjacent Opposing Pok\u00e9mon",
+ allies: "User and Allies",
+ };
+ details["Target"] = targetTypes[sigMove.target] || "Unknown";
+ if (sigMove.isNonstandard === 'Unobtainable') {
+ details[`Unobtainable in Gen ${dex.gen}`] = "";
+ }
+ buf += `${Object.entries(details).map(([detail, value]) => (
+ value === '' ? detail : `${detail}: ${value}`
+ )).join(" |  ")}`;
+ if (sigMove.desc && sigMove.desc !== sigMove.shortDesc) {
+ buf += `In-Depth Description${sigMove.desc}`;
+ }
+ }
+ return buf;
+}
+
+function generateSSBItemInfo(set: SSBSet, dex: ModdedDex, baseDex: ModdedDex) {
+ let buf = ``;
+ if (!Array.isArray(set.item)) {
+ const baseItem = baseDex.items.get(set.item);
+ const sigItem = dex.items.get(set.item);
+ if (!baseItem.exists || (baseItem.desc || baseItem.shortDesc) !== (sigItem.desc || sigItem.shortDesc)) {
+ buf += ``;
+ buf += Chat.getDataItemHTML(sigItem);
+ const details: {[k: string]: string} = {
+ Gen: String(sigItem.gen),
+ };
+
+ if (dex.gen >= 4) {
+ if (sigItem.fling) {
+ details["Fling Base Power"] = String(sigItem.fling.basePower);
+ if (sigItem.fling.status) details["Fling Effect"] = sigItem.fling.status;
+ if (sigItem.fling.volatileStatus) details["Fling Effect"] = sigItem.fling.volatileStatus;
+ if (sigItem.isBerry) details["Fling Effect"] = "Activates the Berry's effect on the target.";
+ if (sigItem.id === 'whiteherb') details["Fling Effect"] = "Restores the target's negative stat stages to 0.";
+ if (sigItem.id === 'mentalherb') {
+ const flingEffect = "Removes the effects of Attract, Disable, Encore, Heal Block, Taunt, and Torment from the target.";
+ details["Fling Effect"] = flingEffect;
+ }
+ } else {
+ details["Fling"] = "This item cannot be used with Fling.";
+ }
+ }
+ if (sigItem.naturalGift && dex.gen >= 3) {
+ details["Natural Gift Type"] = sigItem.naturalGift.type;
+ details["Natural Gift Base Power"] = String(sigItem.naturalGift.basePower);
+ }
+ if (sigItem.isNonstandard && sigItem.isNonstandard !== "Custom") {
+ details[`Unobtainable in Gen ${dex.gen}`] = "";
+ }
+ buf += `${Object.entries(details).map(([detail, value]) => (
+ value === '' ? detail : `${detail}: ${value}`
+ )).join(" |  ")}`;
+ }
+ }
+ return buf;
+}
+
+function generateSSBAbilityInfo(set: SSBSet, dex: ModdedDex, baseDex: ModdedDex) {
+ let buf = ``;
+ if (!Array.isArray(set.ability) && !baseDex.abilities.get(set.ability).exists) {
+ const sigAbil = Dex.deepClone(dex.abilities.get(set.ability));
+ if (!sigAbil.desc && !sigAbil.shortDesc) {
+ sigAbil.desc = `This ability doesn't have a description. Try contacting the SSB dev team.`;
+ }
+ buf += ``;
+ buf += Chat.getDataAbilityHTML(sigAbil);
+ const details: {[k: string]: string} = {
+ Gen: String(sigAbil.gen || 9) || 'CAP',
+ };
+ if (sigAbil.flags['cantsuppress']) details["✓ Not affected by Gastro Acid"] = "";
+ if (sigAbil.flags['breakable']) details["✓ Ignored by Mold Breaker"] = "";
+ buf += `${Object.entries(details).map(([detail, value]) => (
+ value === '' ? detail : `${detail}: ${value}`
+ )).join(" |  ")}`;
+ if (sigAbil.desc && sigAbil.shortDesc && sigAbil.desc !== sigAbil.shortDesc) {
+ buf += `In-Depth Description${sigAbil.desc}`;
+ }
+ }
+ return buf;
+}
+
+function generateSSBPokemonInfo(species: string, dex: ModdedDex, baseDex: ModdedDex) {
+ let buf = ``;
+ const origSpecies = baseDex.species.get(species);
+ const newSpecies = dex.species.get(species);
+ if (
+ newSpecies.types.join('/') !== origSpecies.types.join('/') ||
+ Object.values(newSpecies.abilities).join('/') !== Object.values(origSpecies.abilities).join('/') ||
+ Object.values(newSpecies.baseStats).join('/') !== Object.values(origSpecies.baseStats).join('/')
+ ) {
+ buf += ``;
+ buf += Chat.getDataPokemonHTML(newSpecies, dex.gen, 'SSB');
+ let weighthit = 20;
+ if (newSpecies.weighthg >= 2000) {
+ weighthit = 120;
+ } else if (newSpecies.weighthg >= 1000) {
+ weighthit = 100;
+ } else if (newSpecies.weighthg >= 500) {
+ weighthit = 80;
+ } else if (newSpecies.weighthg >= 250) {
+ weighthit = 60;
+ } else if (newSpecies.weighthg >= 100) {
+ weighthit = 40;
+ }
+ const details: {[k: string]: string} = {
+ "Dex#": String(newSpecies.num),
+ Gen: String(newSpecies.gen) || 'CAP',
+ Height: `${newSpecies.heightm} m`,
+ };
+ details["Weight"] = `${newSpecies.weighthg / 10} kg (${weighthit} BP)`;
+ if (newSpecies.color && dex.gen >= 5) details["Dex Colour"] = newSpecies.color;
+ if (newSpecies.eggGroups && dex.gen >= 2) details["Egg Group(s)"] = newSpecies.eggGroups.join(", ");
+ const evos: string[] = [];
+ for (const evoName of newSpecies.evos) {
+ const evo = dex.species.get(evoName);
+ if (evo.gen <= dex.gen) {
+ const condition = evo.evoCondition ? ` ${evo.evoCondition}` : ``;
+ switch (evo.evoType) {
+ case 'levelExtra':
+ evos.push(`${evo.name} (level-up${condition})`);
+ break;
+ case 'levelFriendship':
+ evos.push(`${evo.name} (level-up with high Friendship${condition})`);
+ break;
+ case 'levelHold':
+ evos.push(`${evo.name} (level-up holding ${evo.evoItem}${condition})`);
+ break;
+ case 'useItem':
+ evos.push(`${evo.name} (${evo.evoItem})`);
+ break;
+ case 'levelMove':
+ evos.push(`${evo.name} (level-up with ${evo.evoMove}${condition})`);
+ break;
+ case 'other':
+ evos.push(`${evo.name} (${evo.evoCondition})`);
+ break;
+ case 'trade':
+ evos.push(`${evo.name} (trade${evo.evoItem ? ` holding ${evo.evoItem}` : condition})`);
+ break;
+ default:
+ evos.push(`${evo.name} (${evo.evoLevel}${condition})`);
+ }
+ }
+ }
+ if (!evos.length) {
+ details[`Does Not Evolve`] = "";
+ } else {
+ details["Evolution"] = evos.join(", ");
+ }
+ buf += `${Object.entries(details).map(([detail, value]) => (
+ value === '' ? detail : `${detail}: ${value}`
+ )).join(" |  ")}`;
+ }
+ return buf;
+}
+
+function generateSSBInnateInfo(name: string, dex: ModdedDex, baseDex: ModdedDex) {
+ let buf = ``;
+ // Special casing for users whose usernames are already existing, i.e. dhelmise
+ let effect = dex.conditions.get(name + 'user');
+ let longDesc = ``;
+ const baseAbility = baseDex.deepClone(baseDex.abilities.get('noability'));
+ if (effect.exists && (effect as any).innateName && (effect.desc || effect.shortDesc)) {
+ baseAbility.name = (effect as any).innateName;
+ if (!effect.desc && !effect.shortDesc) {
+ baseAbility.desc = baseAbility.shortDesc = "This innate does not have a description.";
+ }
+ if (effect.desc) baseAbility.desc = effect.desc;
+ if (effect.shortDesc) baseAbility.shortDesc = effect.shortDesc;
+ buf += `Innate Ability: ${Chat.getDataAbilityHTML(baseAbility)}`;
+ if (effect.desc && effect.shortDesc && effect.desc !== effect.shortDesc) {
+ longDesc = effect.desc;
+ }
+ } else {
+ effect = dex.deepClone(dex.conditions.get(name));
+ if (!effect.desc && !effect.shortDesc) {
+ effect.desc = effect.shortDesc = "This innate does not have a description.";
+ }
+ if (effect.exists && (effect as any).innateName) {
+ baseAbility.name = (effect as any).innateName;
+ if (effect.desc) baseAbility.desc = effect.desc;
+ if (effect.shortDesc) baseAbility.shortDesc = effect.shortDesc;
+ buf += `Innate Ability: ${Chat.getDataAbilityHTML(baseAbility)}`;
+ if (effect.desc && effect.shortDesc && effect.desc !== effect.shortDesc) {
+ longDesc = effect.desc;
+ }
+ }
+ }
+ if (buf) {
+ const details: {[k: string]: string} = {Gen: '9'};
+ buf += `${Object.entries(details).map(([detail, value]) => (
+ value === '' ? detail : `${detail}: ${value}`
+ )).join(" |  ")}`;
+ }
+ if (longDesc) {
+ buf += `In-Depth Description${longDesc}`;
+ }
+ return buf;
+}
+
+function SSBSets(target: string) {
+ const baseDex = Dex;
+ const dex = Dex.forFormat('gen9superstaffbrosultimate');
+ if (!Object.keys(ssbSets).map(toID).includes(toID(target))) {
+ return {e: `Error: ${target.trim()} doesn't have a [Gen 9] Super Staff Bros Ultimate set.`};
+ }
+ let name = '';
+ for (const member in ssbSets) {
+ if (toID(member) === toID(target)) name = member;
+ }
+ let buf = '';
+ const sets: string[] = [];
+ for (const set in ssbSets) {
+ if (!set.startsWith(name)) continue;
+ if (!ssbSets[set].skip && set !== name) continue;
+ sets.push(set);
+ }
+ for (const setName of sets) {
+ const set = ssbSets[setName];
+ const mutatedSpecies = dex.species.get(set.species);
+ if (!set.skip) {
+ buf += Utils.html`
${setName}
`;
+ } else {
+ buf += `${setName.split('-').slice(1).join('-') + ' forme'}`;
+ }
+ buf += generateSSBSet(set, dex, baseDex);
+ const item = dex.items.get(set.item as string);
+ if (!set.skip || set.signatureMove !== ssbSets[set.skip].signatureMove) {
+ const sigMove = baseDex.moves.get(set.signatureMove).exists && !Array.isArray(set.item) &&
+ typeof item.zMove === 'string' ?
+ dex.moves.get(item.zMove) : dex.moves.get(set.signatureMove);
+ buf += generateSSBMoveInfo(sigMove, dex);
+ }
+ buf += generateSSBItemInfo(set, dex, baseDex);
+ buf += generateSSBAbilityInfo(set, dex, baseDex);
+ buf += generateSSBInnateInfo(setName, dex, baseDex);
+ buf += generateSSBPokemonInfo(set.species, dex, baseDex);
+ if (!Array.isArray(set.item) && item.megaStone) {
+ buf += generateSSBPokemonInfo(item.megaStone, dex, baseDex);
+ // keys and Kennedy have an itemless forme change
+ } else if (['Rayquaza'].includes(set.species)) {
+ buf += generateSSBPokemonInfo(`${set.species}-Mega`, dex, baseDex);
+ } else if (['Cinderace'].includes(set.species)) {
+ buf += generateSSBPokemonInfo(`${set.species}-Gmax`, dex, baseDex);
+ }
+ if (set.skip) buf += ``;
+ }
+ return buf;
+}
+
+export const commands: Chat.ChatCommands = {
+ ssb(target, room, user) {
+ if (!this.runBroadcast()) return;
+ if (!target) return this.parse(`/help ssb`);
+ const set = SSBSets(target);
+ if (typeof set !== 'string') {
+ throw new Chat.ErrorMessage(set.e);
+ }
+ return this.sendReplyBox(set);
+ },
+ ssbhelp: [
+ `/ssb [staff member] - Displays a staff member's Super Staff Bros. set and custom features.`,
+ ],
+};
diff --git a/server/chat-plugins/ssb.ts b/server/chat-plugins/ssb.ts
deleted file mode 100644
index 43cb6de66..000000000
--- a/server/chat-plugins/ssb.ts
+++ /dev/null
@@ -1,559 +0,0 @@
-// Thank goodness for this, no need to manually update each time anymore.
-import {ssbSets} from '../../data/mods/gen9ssb/random-teams';
-import {Utils} from '../../lib';
-import {FS} from '../../lib/fs';
-import {RoomSections} from '../chat-commands/room-settings';
-const dex = Dex.mod('gen9ssb');
-const STAT_FORMAT = {hp: 'HP', atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'};
-// Similar to User.usergroups, but is a custom group from a custom csv file
-// You will need to get a copy from the main server when you update, or introduce some sort of hard coded change.
-const usergroups: {[userid: string]: string} = {};
-const usergroupData = FS('config/usergroups-main.csv').readIfExistsSync().split('\n');
-for (const row of usergroupData) {
- if (!toID(row)) continue;
- const cells = row.split(',');
- usergroups[toID(cells[0])] = cells[1].trim() || ' ';
-}
-function getPosition(name: string): string {
- // Special checks first
- const id = toID(name);
- const ssbContribs: string[] = ['ausma', 'blitz', 'hizo', 'violet'];
- if (id === 'artemis') {
- return `Server Automatic Moderation`;
- }
- if (ssbContribs.includes(id)) return `Super Staff Bros Contributor`;
- // Override + -> public RO for retired staff.
- const retiredStaff: string[] = [
- 'kennedy', 'billo', 'breadstycks', 'chloe', 'clerica', 'coolcodename', 'corthius',
- 'dawnofartemis', 'frostyicelad', 'ganjafin', 'inthehills', 'j0rdy004', 'kolohe',
- 'peary', 'phoopes', 'pissog', 'returntomonkey', 'skies', 'swiffix'];
- if (retiredStaff.includes(id)) return `Retired Staff`;
- let group = usergroups[id] || ' ';
- if (id === 'artemis') group = '@';
- const sectionid = Users.globalAuth.sectionLeaders.get(id);
- let section = '';
- if (sectionid && group === '§') {
- section = RoomSections.sectionNames[sectionid];
- return `${Utils.escapeHTML(section.split(' ').map((x: string) => x[0].toUpperCase() + x.substr(1)).join(' '))} Section Leader`;
- }
- const symbolToGroup: {[group: string]: string} = {
- ' ': `Retired Staff`,
- '+': `Public Room Owner`,
- '%': `Global Driver`,
- '@': 'Global Moderator',
- '&': 'Global Administrator',
- };
- if (!symbolToGroup[group]) throw new Error(`Unable to find group ${group} in supported groups for user ${name}.`);
- return symbolToGroup[group];
-}
-function canMegaEvo(species: string, item: string | string[]): string {
- // Meanwhile, these pokemon can mega evolve though strange conditions not easily detected.
- // Its best to hardcode this, even though id rather avoid that.
- const forceMega: {[species: string]: string} = {
- 'rayquaza': 'rayquaza-mega',
- 'cinderace': 'cinderace-gmax',
- };
- if (toID(species) in forceMega) {
- return forceMega[toID(species)];
- } else {
- if (!Array.isArray(item)) item = [item];
- for (const i of item) {
- const itemObj = dex.items.get(i);
- if (toID(species) === toID(itemObj.megaEvolves) && itemObj.megaStone) {
- return itemObj.megaStone.toLowerCase();
- }
- }
- }
- return ``;
-}
-function speciesToImageUrl(
- species: string, item: string | string[] | undefined,
- shiny: boolean, anim: boolean
-): string {
- // Just the URL
- // These don't have models yet, use the BW sprites in all cases
- const forceBW = [
- 'fluttermane', 'sinistchamasterpiece', 'chiyu', 'dachsbun', 'ironjugulis', 'ceruledge', 'qwilfishhisui', 'pichuspikyeared', 'flamigo', 'cinderace', 'archaludon', 'tatsugiri', 'kingambit', 'hemogoblin', 'ogerpon', 'clodsire',
- ];
- // Hardcoded speical cases, avoid using usually.
- const override: {[species: string]: string} = {
- 'Alcremie-Lemon-Cream': 'alcremie-lemoncream',
- 'Alcremie-Ruby-Swirl': 'alcremie-rubyswirl',
- 'Alcremie-Mint-Cream': 'alcremie-mintcream',
- 'Flygon': 'flygon',
- 'Ralts': 'ralts',
- };
- const s = shiny ? `-shiny` : '';
- const base = anim && !forceBW.includes(toID(species)) ? `ani` : `bw`;
- const type = anim && !forceBW.includes(toID(species)) ? `.gif` : `.png`;
- const imageBase = `//play.pokemonshowdown.com/sprites/${base}${s}/`;
- let isMega = item ? canMegaEvo(species, item) : '';
- if (isMega) isMega = isMega.slice(isMega.indexOf('-'));
- let spriteId = '';
- if (override[species]) {
- spriteId = override[species];
- } else {
- spriteId = dex.species.get(species + isMega).spriteid;
- }
- return `${imageBase}${spriteId}${type}`;
-}
-function genItemHTML(item: string | string[]): string {
- // All of the HTML because a set can have mutliple possible items
- const zTable: {[type: string]: string} = {
- bug: 'buginium_z',
- dark: 'darkinium_z',
- dragon: 'dragonium_z',
- electric: 'electrium_z',
- fairy: 'fairium_z',
- fighting: 'fightinium_z',
- fire: 'firium_z',
- flying: 'flyinium_z',
- ghost: 'ghostium_z',
- grass: 'grassium_z',
- ground: 'groundium_z',
- ice: 'icium_z',
- normal: 'normalium_z',
- poison: 'poisonium_z',
- psychic: 'psychium_z',
- rock: 'rockium_z',
- steel: 'steelium_z',
- water: 'waterium_z',
- '': 'normalium_z',
- };
- if (!Array.isArray(item)) {
- item = [item];
- }
- item = item.slice();
- let html = ``;
- for (const i of item) {
- if (!i) {
- html = ``;
- continue;
- }
- let sprite = i;
- const spriteid = toID(sprite);
- if (spriteid === 'leek') sprite = 'stick';
- if (spriteid === 'lilligantiumz') sprite = 'waterium_z';
- if (spriteid === 'irpatuziniumz') sprite = 'fairium_z';
- if (spriteid === 'pearyumz') sprite = 'steelium_z';
- if (spriteid === 'rainiumz') sprite = 'primarium_z';
- if (!Dex.items.get(sprite).exists && dex.items.get(sprite).zMove) {
- const move = dex.items.get(sprite).zMove;
- if (move === true) throw new Error(`Unexpected true for ${sprite}.`);
- sprite = zTable[toID(dex.moves.get(move).type)];
- }
- let url = `/dex/media/sprites/xyitems/${sprite = sprite.toLowerCase().replace(/ /g, '_').replace(/'/g, '')}.png`;
- let width;
- let height;
- if (spriteid === 'boosterenergy') {
- url = `/articles/images/sprites/booster_energy.png`;
- width = height = 32;
- }
- if (spriteid === 'covertcloak') {
- url = `/articles/images/sprites/covert_cloak.png`;
- width = height = 32;
- }
- if (spriteid === 'clearamulet') {
- url = `/articles/images/sprites/clear_amulet.png`;
- width = height = 32;
- }
- if (spriteid === 'griseouscore') {
- url = `/articles/images/sprites/griseous_core.png`;
- width = height = 32;
- }
- if (spriteid === 'mirrorherb') {
- url = `/articles/images/sprites/mirror_herb.png`;
- width = height = 32;
- }
- if (spriteid === 'loadeddice') {
- url = `/articles/images/sprites/loaded_dice.png`;
- width = height = 32;
- }
- if (spriteid === 'flygonite') {
- url = `//play.pokemonshowdown.com/sprites/itemicons/dream-ball.png`;
- }
- html += ` `;
- }
- return html;
-}
-function parseNature(nature: string | string[] | undefined) {
- // too many options to cleanly parse, do it here.
- if (!nature) return `Serious Nature`;
- if (Array.isArray(nature)) return nature.map(n => `${n} Nature`).join(' / ');
- return `${nature} Nature`;
-}
-// innates are stored in each condition if applicable
-function getClassName(name: string): string {
- name = toID(name);
- while (!isNaN(parseInt(name.charAt(0)))) {
- name = name.slice(1);
- }
- return name;
-}
-function genSetHeader(name: string): string {
- // Generates the header for each set
- return `
\n\t
${Utils.escapeHTML(name)} - ${getPosition(name)}
\n`;
-}
-function genSetBody(name: string): string {
- // Generates the body of a set and all of its alts
- const setAlts: string[] = [];
- const userid = toID(name);
- for (const key in ssbSets) {
- if (toID(key.split('-')[0]) === userid) setAlts.push(key);
- }
- let html = ``;
- for (const setID of setAlts) {
- const set = ssbSets[setID];
- html += `\t
\n`;
- html += `\t\t ${genItemHTML(set.item)}\n`;
- html += `\t\t
\n\t\t\t
${set.species}`;
- const itemsList = (Array.isArray(set.item) ? set.item : [set.item]).slice()
- .filter(i => !!i).map(i => {
- if (!Dex.items.get(i).exists) return `${i}`;
- return i;
- });
- if (itemsList.length) html += ` @ ${itemsList.join(' / ')}`;
- html += `
Ability: ${abilities.join(' / ')}`;
- const canMega = canMegaEvo(set.species, set.item);
- let mega: Species | null = null;
- if (canMega) {
- mega = dex.species.get(canMega);
- if (!mega) throw new Error(`Unable to get mega for pokemon that can mega evolve (set: ${name}, mega: ${canMega})`);
- if (toID(mega.abilities[0]) !== toID(set.ability)) {
- if (!Dex.abilities.get(mega.abilities[0]).exists) {
- html += ` >> ${mega.abilities[0]}`;
- } else {
- html += ` >> ${mega.abilities[0]}`;
- }
- }
- }
- html += `
\n`;
- }
- if (set.species !== 'Fennekin') {
- html += `\t\t\t
EVs: `;
- const DEFAULT_EVS: {[ev in StatID]: number} = {hp: 82, atk: 82, def: 82, spa: 82, spd: 82, spe: 82};
- const evs = set.evs || DEFAULT_EVS;
- let ev: StatID;
- for (ev in evs) {
- html += `${evs[ev]} ${STAT_FORMAT[ev]} / `;
- }
- // cut off last /
- html = html.slice(0, html.length - 3);
- html += `
\n`;
- html += `\t\t\t
${parseNature(set.nature)}
\n`;
- if (set.ivs) {
- html += `\t\t\t
IVs: `;
- let iv: StatID;
- for (iv in set.ivs) {
- html += `${set.ivs[iv]} ${STAT_FORMAT[iv]} / `;
- }
- // cut off last /
- html = html.slice(0, html.length - 3);
- html += `
\n`;
- }
- }
- // Moves
- const movepool = set.moves.slice().concat(set.signatureMove);
- // Z move check
- let zMoveBase: Item | null = null;
- const itemArr = (Array.isArray(set.item) ? set.item : [set.item]).slice();
- for (const i of itemArr) {
- const itemObj = dex.items.get(i);
- if (!Dex.items.get(i).exists && itemObj.zMoveFrom) {
- zMoveBase = itemObj;
- break;
- }
- }
- for (const m of movepool) {
- let move;
- if (typeof m === 'string') {
- if (!Dex.moves.get(m).exists) {
- move = `${m}`;
- } else {
- move = m;
- }
- if (zMoveBase && m === zMoveBase.zMoveFrom) {
- move += ` >> ${zMoveBase.zMove}`;
- }
- } else {
- move = m.map(v => {
- if (!Dex.moves.get(v).exists) return `${v}`;
- return v;
- }).join(' / ');
- }
- html += `\t\t\t
- ${move}
\n`;
- }
- html += `\t\t
\n\t
\n`;
- }
- return html;
-}
-function genSetDescriptions(name: string): string {
- // Generates the set's descriptions which appear under the set body(s).
- const setAlts: string[] = [];
- const userid = toID(name);
- for (const key in ssbSets) {
- if (toID(key.split('-')[0]) === userid) setAlts.push(key);
- }
- let baseStatHtml = ``;
- let itemHtml = ``;
- let abilityHtml = ``;
- let moveHtml = ``;
- let innateHtml = ``;
- let customZ: Item | null = null;
- const cache: {[key: string]: string[]} = {
- items: [],
- moves: [],
- abilities: [],
- };
- for (const alt of setAlts) {
- const set = ssbSets[alt];
-
- // base stats
- let speciesid = set.species;
- if (toID(alt) === 'kennedy') speciesid = 'cinderacegmax';
- if (toID(alt) === 'arya') speciesid = 'trapinch';
- const species = dex.species.get(speciesid);
- const baseFormatSpecies = Dex.species.get(species.name);
- if (Object.values(species.baseStats).join() !== Object.values(baseFormatSpecies.baseStats).join()) {
- baseStatHtml += `\t
\n`;
- return html;
-}
-function genDetails(name: string): string {
- let move: Move | null = null;
- let header = ``;
- switch (toID(name)) {
- case 'kennedy':
- move = dex.moves.get('anfieldatmosphere');
- header = `Custom Terrain`;
- break;
- }
- if (!move) return ``;
- return sigMoveDesc(move, header);
-}
-function genSetCSS(name: string): string {
- // Generates the appropriate CSS statement for this set.
- const set = ssbSets[name];
- if (!set) throw new Error(`Set not found for ${name}.`);
- // .xthetap {background-image: url(//play.pokemonshowdown.com/sprites/bw-shiny/arcanine.png);}
- return `\t.${getClassName(name)} {background-image: url(${speciesToImageUrl(set.species, set.item, set.shiny === true, false)});}\n`;
-}
-function genRosterHTML(): string {
- // Generates all the buttons at once.
- // 14 x whatever rows
- // top row, 12
- // center the bottom row
- // image position override
- const imagePositions: {[pos: string]: string[]} = {
- supertall: ['kalalokki', 'kennedy'],
- tall: ['aelita', 'arcueid', 'arya', 'breadstycks', 'elly', 'lasen',
- 'maroon', 'mex', 'notater517', 'penquin', 'rainshaft', 'siegfried', 'solaroslunaris',
- 'struchni', 'thejesucristoosama', 'violet', 'warriorgallade', 'xprienzo', 'yveltalnl'],
- short: ['frostyicelad', 'partman', 'rumia', 'spoo', 'ut', 'venous', 'waves', 'hasteinky'],
- supershort: ['appletunalamode', 'arsenal'],
- shiftLeft: ['arsenal', 'coolcodename', 'blitz', 'easyonthehills', 'eva', 'goroyagami',
- 'havi', 'ken', 'kry', 'pokemonvortex', 'r8', 'rainshaft', 'solaroslunaris', 'spoo'],
- shiftRight: ['aethernum', 'alexander489', 'breadstycks', 'hasteinky', 'keys', 'lasen',
- 'mex', 'scotteh', 'steorra', 'waves', 'xprienzo', 'thejesucristoosama', 'appletunalamode'],
- };
- let html = `
Choose your Fighter!
\n\t
\n`;
- // Get an array of all set names we need a button for, alts NOT included.
- const pool = Object.keys(ssbSets).filter(s => !ssbSets[s].skip);
- let slotsInRow = 0;
- // Counter controlled so we can see the index
- for (let i = 0; i < pool.length; i++) {
- if (slotsInRow === 0) {
- // Start of a row
- html += `\t
\n\t\t`;
- if (i === 0) {
- // First row
- // html += `\t\t\n`;
- slotsInRow++;
- } else if (i + 13 > pool.length) {
- // Last row, and there aren't 13 sets left, fill in blanks.
- // For an odd remianing amount, have extra blanks to the right (end)
- let difference = Math.floor((13 - ((pool.length - 1) - i)) / 2);
- for (difference; difference > 0; difference--) {
- // html += `\t\t\n`;
- slotsInRow++;
- }
- }
- }
- // Add next set
- html += ``;
- slotsInRow++;
- // End of set pool, fill EVERYTHING else in this row with blanks
- if (i + 1 === pool.length) {
- while (slotsInRow < 13) {
- // html += `\t\t\n`;
- slotsInRow++;
- }
- }
- if (slotsInRow === 12 && i < 13) {
- // first row ends
- // html += `\t\t\n`;
- slotsInRow++;
- }
- // End of row, cap it off for the next row/end of roster
- if (slotsInRow === 13) {
- // Next row
- html += `\n\t
\n`;
- slotsInRow = 0;
- }
- }
- html += `
`;
- return html;
-}
-
-
-export const commands: Chat.ChatCommands = {
- genarticle(target, room, user) {
- this.checkCan('lockdown');
- let css = ``;
- const roster = genRosterHTML();
- let data = ``;
- // Skip alts, they are added as appropriate
- const pool = Object.keys(ssbSets).filter(s => !ssbSets[s].skip);
- for (const set of pool) {
- css += genSetCSS(set);
- data += genSetHeader(set);
- data += genSetBody(set);
- data += genSetDescriptions(set);
- }
- data = data.replace(/\u00ED/g, 'í');
- data = data.replace(/\u00F6/g, 'ö');
- data = data.replace(/Pok[eé]/g, 'Poké');
- FS('logs/ssb_article').mkdirIfNonexistentSync();
- FS('logs/ssb_article/css.txt').writeSync(css);
- FS('logs/ssb_article/roster.txt').writeSync(roster);
- FS('logs/ssb_article/roster-details.txt').writeSync(data);
- return this.sendReply('Done, check logs/ssb_article/*');
- },
-};