From 1c558af3b8323f776f7d9a246162709c0cd5e26d Mon Sep 17 00:00:00 2001 From: Kris Johnson <11083252+KrisXV@users.noreply.github.com> Date: Sat, 4 May 2024 09:52:12 -0600 Subject: [PATCH] Move SSB chat stuff into its own plugin --- server/chat-plugins/randombattles/index.ts | 391 +------------- server/chat-plugins/randombattles/ssb.ts | 390 ++++++++++++++ server/chat-plugins/ssb.ts | 559 --------------------- 3 files changed, 393 insertions(+), 947 deletions(-) create mode 100644 server/chat-plugins/randombattles/ssb.ts delete mode 100644 server/chat-plugins/ssb.ts diff --git a/server/chat-plugins/randombattles/index.ts b/server/chat-plugins/randombattles/index.ts index cb339f1dc..4efe797c0 100644 --- a/server/chat-plugins/randombattles/index.ts +++ b/server/chat-plugins/randombattles/index.ts @@ -6,7 +6,6 @@ */ import {FS, Utils} from '../../../lib'; -import {SSBSet, ssbSets} from '../../../data/mods/gen9ssb/random-teams'; interface SetCriteria { @@ -110,7 +109,7 @@ const GEN_NAMES: {[k: string]: string} = { gen8: '[Gen 8]', gen9: '[Gen 9]', }; -const STAT_NAMES: {[k: string]: string} = { +export const STAT_NAMES: {[k: string]: string} = { hp: "HP", atk: "Atk", def: "Def", spa: "SpA", spd: "SpD", spe: "Spe", }; @@ -124,7 +123,8 @@ function formatAbility(ability: Ability | string) { ability = Dex.abilities.get(ability); return `${ability.name}`; } -function formatNature(n: string) { + +export function formatNature(n: string) { const nature = Dex.natures.get(n); return nature.name; } @@ -411,378 +411,6 @@ function CAP1v1Sets(species: string | Species) { return buf; } -function generateSSBSet(set: SSBSet, dex: ModdedDex, baseDex: ModdedDex) { - if (set.skip) { - const baseSet = toID(Object.values(ssbSets[set.skip]).join()); - const skipSet = toID(Object.values(set).join()).slice(0, -toID(set.skip).length); - if (baseSet === skipSet) return ``; - } - let buf = ``; - buf += `
Set`; - buf += ``; - 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) || 'CAP', - }; - - 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 = { randbats: 'randombattles', randomdoublesbattle: 'randombattles', @@ -983,19 +611,6 @@ export const commands: Chat.ChatCommands = { `/cap1v1 [pokemon] - Displays a Pok\u00e9mon's CAP 1v1 sets.`, ], - 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.`, - ], - setodds: 'randombattlesetprobabilities', randbatsodds: 'randombattlesetprobabilities', randbatsprobabilities: 'randombattlesetprobabilities', diff --git a/server/chat-plugins/randombattles/ssb.ts b/server/chat-plugins/randombattles/ssb.ts new file mode 100644 index 000000000..77e31808f --- /dev/null +++ b/server/chat-plugins/randombattles/ssb.ts @@ -0,0 +1,390 @@ +import {SSBSet, ssbSets} from "../../../data/mods/gen9ssb/random-teams"; +import {Utils} from "../../../lib"; +import {formatNature, STAT_NAMES} from "."; + +function generateSSBSet(set: SSBSet, dex: ModdedDex, baseDex: ModdedDex) { + if (set.skip) { + const baseSet = toID(Object.values(ssbSets[set.skip]).join()); + const skipSet = toID(Object.values(set).join()).slice(0, -toID(set.skip).length); + if (baseSet === skipSet) return ``; + } + let buf = ``; + buf += `
Set`; + buf += ``; + 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 += `${i} `; - } - 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`); -} -function itemDesc(item: Item): [string, Item | null] { - let html = ``; - let customZCrystal: Item | null = null; - const type = item.megaStone ? `Mega Stone` : (item.zMove ? `Z Crystal` : `Item`); - if (!item.desc && !item.shortDesc) console.log(item.name + " needs desc/shortDesc. (Item)"); - html += `\t
\n\t\t
Custom ${type}
\n\t\t
${item.desc || item.shortDesc || `No description yet!`}
\n\t
\n`; - if (item.zMove && typeof item.zMove === 'string') customZCrystal = item; - return [html, customZCrystal]; -} -function abilityDesc(ability: Ability): string { - let html = `\t
\n\t\t
Custom Ability - ${ability.name}
\n`; - if (!ability.desc && !ability.shortDesc) { - console.log(ability.name + " needs desc/shortDesc. (Ability)"); - } - html += `\t\t
${ability.desc || ability.shortDesc || `No description yet!`}
\n\t
\n`; - return html; -} -function sigMoveDesc(move: Move, header?: string): string { - if (!header) header = `Signature Move`; - let html = `\t
\n\t\t
${header} - ${move.name}
\n`; - const type = move.type === '???' ? 'question' : toID(move.type); - html += `\t\t
${move.type.toUpperCase()} ${move.category.toUpperCase()}\n`; - html += `\t\t`; - if (move.category !== 'Status') html += `Base Power: ${move.basePower} `; - html += `Accuracy: ${move.accuracy === true ? '-' : move.accuracy + '%'} `; - html += `PP: ${move.pp}`; - if (!move.noPPBoosts) html += ` (max: ${move.pp * 8 / 5})`; - html += `
\n`; - if (!move.desc && !move.shortDesc) { - console.log(move.name + " needs desc/shortDesc. (Move)"); - } - html += `\t\t
${move.desc || move.shortDesc || 'No description provided.'}
\n\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/*'); - }, -};