Spaces:
Running
Running
; | |
var __defProp = Object.defineProperty; | |
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | |
var __getOwnPropNames = Object.getOwnPropertyNames; | |
var __hasOwnProp = Object.prototype.hasOwnProperty; | |
var __export = (target, all) => { | |
for (var name in all) | |
__defProp(target, name, { get: all[name], enumerable: true }); | |
}; | |
var __copyProps = (to, from, except, desc) => { | |
if (from && typeof from === "object" || typeof from === "function") { | |
for (let key of __getOwnPropNames(from)) | |
if (!__hasOwnProp.call(to, key) && key !== except) | |
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | |
} | |
return to; | |
}; | |
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | |
var cg_teams_exports = {}; | |
__export(cg_teams_exports, { | |
default: () => TeamGenerator, | |
levelUpdateInterval: () => levelUpdateInterval | |
}); | |
module.exports = __toCommonJS(cg_teams_exports); | |
var import_sim = require("../sim"); | |
var import_cg_team_data = require("./cg-team-data"); | |
const MAX_WEAK_TO_SAME_TYPE = 3; | |
const TOP_SPEED = 300; | |
const levelOverride = {}; | |
let levelUpdateInterval = null; | |
const useBaseSpecies = [ | |
"Pikachu", | |
"Gastrodon", | |
"Magearna", | |
"Dudunsparce", | |
"Maushold", | |
"Keldeo", | |
"Zarude", | |
"Polteageist", | |
"Sinistcha", | |
"Sawsbuck", | |
"Vivillon", | |
"Florges", | |
"Minior", | |
"Toxtricity", | |
"Tatsugiri", | |
"Alcremie" | |
]; | |
async function updateLevels(database) { | |
const updateSpecies = await database.prepare( | |
"UPDATE gen9computergeneratedteams SET wins = 0, losses = 0, level = ? WHERE species_id = ?" | |
); | |
const updateHistory = await database.prepare( | |
`INSERT INTO gen9_historical_levels (level, species_id, timestamp) VALUES (?, ?, ${Date.now()})` | |
); | |
const data = await database.all("SELECT species_id, wins, losses, level FROM gen9computergeneratedteams"); | |
for (let { species_id, wins, losses, level } of data) { | |
const total = wins + losses; | |
if (total > 10) { | |
if (wins / total >= 0.55) | |
level--; | |
if (wins / total <= 0.45) | |
level++; | |
level = Math.max(1, Math.min(100, level)); | |
await updateSpecies?.run([level, species_id]); | |
await updateHistory?.run([level, species_id]); | |
} | |
levelOverride[species_id] = level; | |
} | |
} | |
if (global.Config && Config.usesqlite && Config.usesqliteleveling) { | |
const database = (0, import_sim.SQL)(module, { file: "./databases/battlestats.db" }); | |
void updateLevels(database); | |
levelUpdateInterval = setInterval(() => void updateLevels(database), 1e3 * 60 * 60 * 2); | |
} | |
class TeamGenerator { | |
constructor(format, seed) { | |
this.dex = import_sim.Dex.forFormat(format); | |
this.format = import_sim.Dex.formats.get(format); | |
this.teamSize = this.format.ruleTable?.maxTeamSize || 6; | |
this.prng = import_sim.PRNG.get(seed); | |
this.itemPool = this.dex.items.all().filter((i) => i.exists && i.isNonstandard !== "Past" && !i.isPokeball); | |
this.specialItems = {}; | |
for (const i of this.itemPool) { | |
if (i.itemUser && !i.isNonstandard) { | |
for (const user of i.itemUser) { | |
if (import_sim.Dex.species.get(user).requiredItems?.[0] !== i.name) | |
this.specialItems[user] = i.id; | |
} | |
} | |
} | |
const rules = import_sim.Dex.formats.getRuleTable(this.format); | |
if (rules.adjustLevel) | |
this.forceLevel = rules.adjustLevel; | |
} | |
getTeam() { | |
let speciesPool = this.dex.species.all().filter((s) => { | |
if (!s.exists) | |
return false; | |
if (s.isNonstandard || s.isNonstandard === "Unobtainable") | |
return false; | |
if (s.nfe) | |
return false; | |
if (s.battleOnly && (!s.requiredItems?.length || s.name.endsWith("-Tera"))) | |
return false; | |
return true; | |
}); | |
const teamStats = { | |
hazardSetters: {}, | |
typeWeaknesses: {}, | |
hazardRemovers: 0 | |
}; | |
const team = []; | |
while (team.length < this.teamSize && speciesPool.length) { | |
const species = this.prng.sample(speciesPool); | |
const haveRoomToReject = speciesPool.length >= this.teamSize - team.length; | |
const isGoodFit = this.speciesIsGoodFit(species, teamStats); | |
if (haveRoomToReject && !isGoodFit) | |
continue; | |
speciesPool = speciesPool.filter((s) => s.baseSpecies !== species.baseSpecies); | |
team.push(this.makeSet(species, teamStats)); | |
} | |
return team; | |
} | |
makeSet(species, teamStats) { | |
const abilityPool = Object.values(species.abilities); | |
const abilityWeights = abilityPool.map((a) => this.getAbilityWeight(this.dex.abilities.get(a))); | |
const ability = this.weightedRandomPick(abilityPool, abilityWeights); | |
const level = this.forceLevel || TeamGenerator.getLevel(species); | |
const moves = []; | |
let movesStats = { | |
setup: { atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }, | |
attackTypes: {}, | |
noSleepTalk: 0, | |
hazards: 0, | |
stallingMoves: 0, | |
healing: 0, | |
nonStatusMoves: 0 | |
}; | |
let movePool = [...this.dex.species.getMovePool(species.id)]; | |
if (!movePool.length) | |
throw new Error(`No moves for ${species.id}`); | |
const numberOfMovesToConsider = Math.min(movePool.length, Math.max(15, Math.trunc(movePool.length * 0.3))); | |
let movePoolIsTrimmed = false; | |
let isRound2 = false; | |
const movePoolCopy = movePool; | |
let interimMovePool = []; | |
while (moves.length < 4 && movePool.length) { | |
let weights; | |
if (!movePoolIsTrimmed) { | |
if (!isRound2) { | |
for (const moveID2 of movePool) { | |
const move2 = this.dex.moves.get(moveID2); | |
const weight = this.getMoveWeight(move2, teamStats, species, moves, movesStats, ability, level); | |
interimMovePool.push({ move: moveID2, weight }); | |
} | |
interimMovePool.sort((a, b) => b.weight - a.weight); | |
} else { | |
const originalWeights = []; | |
for (const move2 of moves) { | |
originalWeights.push(interimMovePool.find((m) => m.move === move2.id)); | |
} | |
interimMovePool = originalWeights; | |
for (const moveID2 of movePoolCopy) { | |
const move2 = this.dex.moves.get(moveID2); | |
if (moves.includes(move2)) | |
continue; | |
const weight = this.getMoveWeight(move2, teamStats, species, moves, movesStats, ability, level); | |
interimMovePool.push({ move: moveID2, weight }); | |
} | |
interimMovePool.sort((a, b) => b.weight - a.weight); | |
moves.splice(0); | |
movesStats = { | |
setup: { atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }, | |
attackTypes: {}, | |
noSleepTalk: 0, | |
hazards: 0, | |
stallingMoves: 0, | |
healing: 0, | |
nonStatusMoves: 0 | |
}; | |
} | |
movePool = []; | |
weights = []; | |
for (let i = 0; i < numberOfMovesToConsider; i++) { | |
movePool.push(interimMovePool[i].move); | |
weights.push(interimMovePool[i].weight); | |
} | |
movePoolIsTrimmed = true; | |
} else { | |
weights = movePool.map( | |
(m) => this.getMoveWeight(this.dex.moves.get(m), teamStats, species, moves, movesStats, ability, level) | |
); | |
} | |
const moveID = this.weightedRandomPick(movePool, weights, { remove: true }); | |
const move = this.dex.moves.get(moveID); | |
moves.push(move); | |
if (TeamGenerator.moveIsHazard(moves[moves.length - 1])) { | |
teamStats.hazardSetters[moveID] = (teamStats.hazardSetters[moveID] || 0) + 1; | |
movesStats.hazards++; | |
} | |
if (["defog", "courtchange", "tidyup", "rapidspin", "mortalspin"].includes(moveID)) | |
teamStats.hazardRemovers++; | |
const boosts = move.boosts || move.self?.boosts || move.selfBoost?.boosts || ability !== "Sheer Force" && move.secondary?.self?.boosts; | |
if (move.category === "Status") { | |
if (boosts) { | |
for (const stat in boosts) { | |
const chance = Math.min(100, move.secondary?.chance || 100 * (ability === "Serene Grace" ? 2 : 1)); | |
const boost = (boosts[stat] || 0) * chance / 100; | |
if (boost) { | |
if (movesStats.setup[stat] < 0 && boost > 0) { | |
movesStats.setup[stat] = boost; | |
} else { | |
movesStats.setup[stat] += boost; | |
} | |
if (boost > 1) | |
movesStats.noSleepTalk++; | |
} | |
} | |
} else { | |
movesStats.noSleepTalk++; | |
} | |
if (move.heal) | |
movesStats.healing++; | |
if (move.stallingMove) | |
movesStats.stallingMoves++; | |
} else { | |
movesStats.nonStatusMoves++; | |
const bp = +move.basePower; | |
const moveType = TeamGenerator.moveType(move, species); | |
if (movesStats.attackTypes[moveType] < bp) | |
movesStats.attackTypes[moveType] = bp; | |
} | |
if (!isRound2 && moves.length === 3) { | |
isRound2 = true; | |
movePoolIsTrimmed = false; | |
continue; | |
} | |
const pairedMove = import_cg_team_data.MOVE_PAIRINGS[moveID]; | |
const alreadyHavePairedMove = moves.some((m) => m.id === pairedMove); | |
if (moves.length < 4 && pairedMove && !(pairedMove === "sleeptalk" && movesStats.noSleepTalk) && !alreadyHavePairedMove && // We don't check movePool because sometimes paired moves are bad. | |
this.dex.species.getLearnsetData(species.id).learnset?.[pairedMove]) { | |
moves.push(this.dex.moves.get(pairedMove)); | |
const pairedMoveIndex = movePool.indexOf(pairedMove); | |
if (pairedMoveIndex > -1) | |
movePool.splice(pairedMoveIndex, 1); | |
} | |
} | |
let item = ""; | |
const nonStatusMoves = moves.filter((m) => this.dex.moves.get(m).category !== "Status"); | |
if (species.requiredItem) { | |
item = species.requiredItem; | |
} else if (species.requiredItems) { | |
item = this.prng.sample(species.requiredItems.filter((i) => !this.dex.items.get(i).isNonstandard)); | |
} else if (this.specialItems[species.name] && nonStatusMoves.length) { | |
item = this.specialItems[species.name]; | |
} else if (moves.every((m) => m.id !== "acrobatics")) { | |
const weights = []; | |
const items = []; | |
for (const i of this.itemPool) { | |
const weight = this.getItemWeight(i, teamStats, species, moves, ability, level); | |
if (weight !== 0) { | |
weights.push(weight); | |
items.push(i.name); | |
} | |
} | |
if (!item) | |
item = this.weightedRandomPick(items, weights); | |
} else if (["Quark Drive", "Protosynthesis"].includes(ability)) { | |
item = "Booster Energy"; | |
} | |
const ivs = { | |
hp: 31, | |
atk: moves.some((move) => this.dex.moves.get(move).category === "Physical") ? 31 : 0, | |
def: 31, | |
spa: 31, | |
spd: 31, | |
spe: 31 | |
}; | |
const hasTeraBlast = moves.some((m) => m.id === "terablast"); | |
const hasRevelationDance = moves.some((m) => m.id === "revelationdance"); | |
let teraType; | |
if (species.forceTeraType) { | |
teraType = species.forceTeraType; | |
} else if (item === "blacksludge" && this.prng.randomChance(2, 3)) { | |
teraType = "Poison"; | |
} else if (hasTeraBlast && ability === "Contrary" && this.prng.randomChance(2, 3)) { | |
teraType = "Stellar"; | |
} else { | |
let types = nonStatusMoves.map((m) => TeamGenerator.moveType(this.dex.moves.get(m), species)); | |
const noStellar = ability === "Adaptability" || new Set(types).size < 3; | |
if (hasTeraBlast || hasRevelationDance || !nonStatusMoves.length) { | |
types = [...this.dex.types.names()]; | |
if (noStellar) | |
types.splice(types.indexOf("Stellar")); | |
} else { | |
if (!noStellar) | |
types.push("Stellar"); | |
} | |
teraType = this.prng.sample(types); | |
} | |
return { | |
name: species.name, | |
species: species.name, | |
item, | |
ability, | |
moves: moves.map((m) => m.name), | |
nature: "Quirky", | |
gender: species.gender, | |
evs: { hp: 84, atk: 84, def: 84, spa: 84, spd: 84, spe: 84 }, | |
ivs, | |
level, | |
teraType, | |
shiny: this.prng.randomChance(1, 1024), | |
happiness: 255 | |
}; | |
} | |
/** | |
* @returns true if the Pokémon is a good fit for the team so far, and no otherwise | |
*/ | |
speciesIsGoodFit(species, stats) { | |
for (const typeName of this.dex.types.names()) { | |
const effectiveness = this.dex.getEffectiveness(typeName, species.types); | |
if (effectiveness === 1) { | |
if (stats.typeWeaknesses[typeName] === void 0) { | |
stats.typeWeaknesses[typeName] = 0; | |
} | |
if (stats.typeWeaknesses[typeName] >= MAX_WEAK_TO_SAME_TYPE) { | |
return false; | |
} | |
} | |
} | |
for (const typeName of this.dex.types.names()) { | |
const effectiveness = this.dex.getEffectiveness(typeName, species.types); | |
if (effectiveness === 1) { | |
stats.typeWeaknesses[typeName]++; | |
} | |
} | |
return true; | |
} | |
/** | |
* @returns A weighting for the Pokémon's ability. | |
*/ | |
getAbilityWeight(ability) { | |
return ability.rating + 1; | |
} | |
static moveIsHazard(move) { | |
return !!(move.sideCondition && move.target === "foeSide") || ["stoneaxe", "ceaselessedge"].includes(move.id); | |
} | |
/** | |
* @returns A weight for a given move on a given Pokémon. | |
*/ | |
getMoveWeight(move, teamStats, species, movesSoFar, movesStats, ability, level) { | |
if (!move.exists) | |
return 0; | |
if (move.target === "adjacentAlly") | |
return 0; | |
if (ability === "Tera Shift") | |
species = this.dex.species.get("Terapagos-Terastal"); | |
const adjustedStats = { | |
hp: species.baseStats.hp * level / 100 + level, | |
atk: species.baseStats.atk * level * level / 1e4, | |
def: species.baseStats.def * level / 100, | |
spa: species.baseStats.spa * level * level / 1e4, | |
spd: species.baseStats.spd * level / 100, | |
spe: species.baseStats.spe * level / 100 | |
}; | |
if (move.category === "Status") { | |
let weight2 = 2400; | |
if (move.status) | |
weight2 *= TeamGenerator.statusWeight(move.status) * 2; | |
if (TeamGenerator.moveIsHazard(move) && (teamStats.hazardSetters[move.id] || 0) < 1) { | |
weight2 *= move.id === "spikes" ? 12 : 16; | |
if (movesStats.hazards) | |
weight2 *= 2; | |
} | |
if (["defog", "courtchange", "tidyup"].includes(move.id) && !teamStats.hazardRemovers) { | |
weight2 *= 32; | |
weight2 *= 0.8 ** Object.values(teamStats.hazardSetters).reduce((total, num) => total + num, 0); | |
} | |
weight2 *= this.boostWeight(move, movesSoFar, species, ability, level); | |
weight2 *= this.opponentDebuffWeight(move); | |
if (move.id === "focusenergy" && ability !== "Super Luck") { | |
const highCritMoves = movesSoFar.filter((m) => m.critRatio && m.critRatio > 1); | |
weight2 *= 1 + highCritMoves.length * (ability === "Sniper" ? 2 : 1); | |
} else if (move.id === "tailwind" && ability === "Wind Rider" && movesSoFar.some((m) => m.category === "Physical")) { | |
weight2 *= 2.5; | |
} | |
if (!movesStats.stallingMoves) { | |
if (adjustedStats.def >= 80 || adjustedStats.spd >= 80 || adjustedStats.hp >= 80) { | |
switch (move.volatileStatus) { | |
case "endure": | |
weight2 *= 2; | |
break; | |
case "protect": | |
weight2 *= 3; | |
break; | |
case "kingsshield": | |
case "silktrap": | |
weight2 *= 4; | |
break; | |
case "banefulbunker": | |
case "burningbulwark": | |
case "spikyshield": | |
weight2 *= 5; | |
break; | |
default: | |
break; | |
} | |
} | |
} | |
if (move.id in import_cg_team_data.HARDCODED_MOVE_WEIGHTS) | |
weight2 *= import_cg_team_data.HARDCODED_MOVE_WEIGHTS[move.id]; | |
const sleepImmunities = [ | |
"Comatose", | |
"Purifying Salt", | |
"Shields Down", | |
"Insomnia", | |
"Vital Spirit", | |
"Sweet Veil", | |
"Misty Surge", | |
"Electric Surge", | |
"Hadron Engine" | |
]; | |
if (["sleeptalk", "rest"].includes(move.id) && sleepImmunities.includes(ability)) | |
return 0; | |
if (move.id === "sleeptalk") { | |
if (movesStats.noSleepTalk) | |
weight2 *= 0.1; | |
} else if (movesSoFar.some((m) => m.id === "sleeptalk")) { | |
let sleepTalkSpammable = ["takeheart", "junglehealing", "healbell"].includes(move.id); | |
if (move.boosts) { | |
for (const stat in move.boosts) { | |
if (move.boosts[stat] === 1) { | |
sleepTalkSpammable = true; | |
break; | |
} | |
} | |
} | |
if (!sleepTalkSpammable) | |
weight2 *= 0.1; | |
} | |
const goodAttacker = adjustedStats.atk > 65 || adjustedStats.spa > 65; | |
if (goodAttacker && movesStats.nonStatusMoves < 2) { | |
weight2 *= 0.3; | |
} | |
if (movesSoFar.length === 3 && movesStats.nonStatusMoves === 0) { | |
weight2 *= 0.6; | |
for (const stat in movesStats.setup) { | |
if (movesStats.setup[stat] > 0) { | |
weight2 *= 0.6; | |
} | |
} | |
} | |
if (move.heal && movesStats.healing) | |
weight2 *= 0.5; | |
return weight2; | |
} | |
let basePower = move.basePower; | |
if (import_cg_team_data.WEIGHT_BASED_MOVES.includes(move.id) || import_cg_team_data.TARGET_HP_BASED_MOVES.includes(move.id)) | |
basePower = 60; | |
const slownessRating = Math.max(0, TOP_SPEED - adjustedStats.spe) / TOP_SPEED; | |
if (move.id === "gyroball") | |
basePower = 150 * slownessRating * slownessRating; | |
if (move.id === "electroball") | |
basePower = 150 * (1 - slownessRating) * (1 - slownessRating); | |
let baseStat = move.category === "Physical" ? adjustedStats.atk : adjustedStats.spa; | |
if (move.id === "foulplay") | |
baseStat = adjustedStats.spe * level / 100; | |
if (move.id === "bodypress") | |
baseStat = adjustedStats.def * level / 100; | |
let accuracy = move.accuracy === true || ability === "No Guard" ? 110 : move.accuracy; | |
if (accuracy < 100) { | |
if (ability === "Compound Eyes") | |
accuracy = Math.min(100, Math.round(accuracy * 1.3)); | |
if (ability === "Victory Star") | |
accuracy = Math.min(100, Math.round(accuracy * 1.1)); | |
} | |
accuracy /= 100; | |
const moveType = TeamGenerator.moveType(move, species); | |
let powerEstimate = basePower * baseStat * accuracy; | |
if (species.types.includes(moveType)) | |
powerEstimate *= ability === "Adaptability" ? 2 : 1.5; | |
if (ability === "Technician" && move.basePower <= 60) | |
powerEstimate *= 1.5; | |
if (ability === "Sheer Force" && (move.secondary || move.secondaries)) | |
powerEstimate *= 1.3; | |
const numberOfHits = Array.isArray(move.multihit) ? ability === "Skill Link" ? move.multihit[1] : (move.multihit[0] + move.multihit[1]) / 2 : move.multihit || 1; | |
powerEstimate *= numberOfHits; | |
if (species.requiredItems) { | |
const item = this.dex.items.get(this.specialItems[species.name]); | |
if (item.onBasePower && (species.types.includes(moveType) || item.name.endsWith("Mask"))) | |
powerEstimate *= 1.2; | |
} else if (this.specialItems[species.name]) { | |
const item = this.dex.items.get(this.specialItems[species.name]); | |
if (item.onBasePower && species.types.includes(moveType)) | |
powerEstimate *= 1.2; | |
if (item.id === "lightball") | |
powerEstimate *= 2; | |
} | |
const specialSetup = movesStats.setup.spa; | |
const physicalSetup = movesStats.setup.atk; | |
if (move.category === "Physical" && !["bodypress", "foulplay"].includes(move.id)) { | |
powerEstimate *= Math.max(0.5, 1 + physicalSetup) / Math.max(0.5, 1 + specialSetup); | |
} | |
if (move.category === "Special") | |
powerEstimate *= Math.max(0.5, 1 + specialSetup) / Math.max(0.5, 1 + physicalSetup); | |
const abilityBonus = (import_cg_team_data.ABILITY_MOVE_BONUSES[this.dex.toID(ability)]?.[move.id] || 1) * (import_cg_team_data.ABILITY_MOVE_TYPE_BONUSES[this.dex.toID(ability)]?.[moveType] || 1); | |
let weight = powerEstimate * abilityBonus; | |
if (move.id in import_cg_team_data.HARDCODED_MOVE_WEIGHTS) | |
weight *= import_cg_team_data.HARDCODED_MOVE_WEIGHTS[move.id]; | |
if (!this.specialItems[species.name] && !species.requiredItem) { | |
if (move.id === "acrobatics") | |
weight *= 1.75; | |
if (move.id === "facade") { | |
if (!["Comatose", "Purifying Salt", "Shields Down", "Natural Cure", "Misty Surge"].includes(ability)) | |
weight *= 1.5; | |
} | |
} | |
if (move.priority > 0 && move.id !== "upperhand") | |
weight *= Math.max(105 - adjustedStats.spe, 0) / 105 * 0.5 + 1; | |
if (move.priority < 0 || move.id === "upperhand") | |
weight *= Math.min(1 / adjustedStats.spe * 25, 1); | |
if (move.flags.charge || move.flags.recharge && ability !== "Truant") | |
weight *= 0.5; | |
if (move.flags.contact) { | |
if (ability === "Tough Claws") | |
weight *= 1.3; | |
if (ability === "Unseen Fist") | |
weight *= 1.1; | |
if (ability === "Poison Touch") | |
weight *= TeamGenerator.statusWeight("psn", 1 - 0.7 ** numberOfHits); | |
} | |
if (move.flags.bite && ability === "Strong Jaw") | |
weight *= 1.5; | |
if (move.flags.bypasssub) | |
weight *= 1.05; | |
if (move.flags.pulse && ability === "Mega Launcher") | |
weight *= 1.5; | |
if (move.flags.punch && ability === "Iron Fist") | |
weight *= 1.2; | |
if (!move.flags.protect) | |
weight *= 1.05; | |
if (move.flags.slicing && ability === "Sharpness") | |
weight *= 1.5; | |
if (move.flags.sound && ability === "Punk Rock") | |
weight *= 1.3; | |
weight *= this.boostWeight(move, movesSoFar, species, ability, level); | |
const secondaryChance = Math.min((move.secondary?.chance || 100) * (ability === "Serene Grace" ? 2 : 1) / 100, 100); | |
if (move.secondary || move.secondaries) { | |
if (ability === "Sheer Force") { | |
weight *= 1.3; | |
} else { | |
const secondaries = move.secondaries || [move.secondary]; | |
for (const secondary of secondaries) { | |
if (secondary.status) { | |
weight *= TeamGenerator.statusWeight(secondary.status, secondaryChance, slownessRating); | |
if (ability === "Poison Puppeteer" && ["psn", "tox"].includes(secondary.status)) { | |
weight *= TeamGenerator.statusWeight("confusion", secondaryChance); | |
} | |
} | |
if (secondary.volatileStatus) { | |
weight *= TeamGenerator.statusWeight(secondary.volatileStatus, secondaryChance, slownessRating); | |
} | |
} | |
} | |
} | |
if (ability === "Toxic Chain") | |
weight *= TeamGenerator.statusWeight("tox", 1 - 0.7 ** numberOfHits); | |
if (move.id === "lashout") | |
weight *= 1 + 0.2 * slownessRating; | |
if (move.id === "burningjealousy") | |
weight *= TeamGenerator.statusWeight("brn", 0.2 * slownessRating); | |
if (move.id === "alluringvoice") | |
weight *= TeamGenerator.statusWeight("confusion", 0.2 * slownessRating); | |
if (move.self?.volatileStatus) | |
weight *= 0.8; | |
if ((movesStats.attackTypes[moveType] || 0) > 60) | |
weight *= 0.3; | |
if (move.selfdestruct) | |
weight *= 0.3; | |
if (move.recoil && ability !== "Rock Head" && ability !== "Magic Guard") { | |
weight *= 1 - move.recoil[0] / move.recoil[1]; | |
if (ability === "Reckless") | |
weight *= 1.2; | |
} | |
if (move.hasCrashDamage && ability !== "Magic Guard") { | |
weight *= 1 - 0.75 * (1.2 - accuracy); | |
if (ability === "Reckless") | |
weight *= 1.2; | |
} | |
if (move.mindBlownRecoil) | |
weight *= 0.25; | |
if (move.flags["futuremove"]) | |
weight *= 0.3; | |
let critRate = move.willCrit ? 4 : move.critRatio || 1; | |
if (ability === "Super Luck") | |
critRate++; | |
if (movesSoFar.some((m) => m.id === "focusenergy")) { | |
critRate += 2; | |
weight *= 0.9; | |
} | |
if (critRate > 4) | |
critRate = 4; | |
weight *= 1 + [0, 1 / 24, 1 / 8, 1 / 2, 1][critRate] * (ability === "Sniper" ? 1 : 0.5); | |
if (["rapidspin", "mortalspin"].includes(move.id)) { | |
weight *= 1 + 20 * 0.25 ** teamStats.hazardRemovers; | |
} | |
if (move.id === "stoneaxe" && teamStats.hazardSetters.stealthrock) | |
weight /= 4; | |
if (move.id === "ceaselessedge" && teamStats.hazardSetters.spikes) | |
weight /= 2; | |
if (move.drain) { | |
const drainedFraction = move.drain[0] / move.drain[1]; | |
weight *= 1 + drainedFraction * 0.5; | |
} | |
if (move.id === "terablast" && (species.baseSpecies === "Oricorio" || species.forceTeraType)) | |
weight *= 0.5; | |
return weight; | |
} | |
/** | |
* @returns The effective type of moves with variable types such as Judgment | |
*/ | |
static moveType(move, species) { | |
switch (move.id) { | |
case "ivycudgel": | |
case "ragingbull": | |
if (species.types.length > 1) | |
return species.types[1]; | |
case "judgment": | |
case "revelationdance": | |
return species.types[0]; | |
} | |
return move.type; | |
} | |
static moveIsPhysical(move, species) { | |
if (move.category === "Physical") { | |
return !(move.damageCallback || move.damage); | |
} else if (["terablast", "terastarstorm", "photongeyser", "shellsidearm"].includes(move.id)) { | |
return species.baseStats.atk > species.baseStats.spa; | |
} else { | |
return false; | |
} | |
} | |
static moveIsSpecial(move, species) { | |
if (move.category === "Special") { | |
return !(move.damageCallback || move.damage); | |
} else if (["terablast", "terastarstorm", "photongeyser", "shellsidearm"].includes(move.id)) { | |
return species.baseStats.atk <= species.baseStats.spa; | |
} else { | |
return false; | |
} | |
} | |
/** | |
* @returns A multiplier to a move weighting based on the status it inflicts. | |
*/ | |
static statusWeight(status, chance = 1, slownessRating) { | |
if (chance !== 1) | |
return 1 + (TeamGenerator.statusWeight(status) - 1) * chance; | |
switch (status) { | |
case "brn": | |
return 2; | |
case "frz": | |
return 5; | |
case "par": | |
return slownessRating && slownessRating > 0.25 ? 2 + slownessRating : 2; | |
case "psn": | |
return 1.75; | |
case "tox": | |
return 4; | |
case "slp": | |
return 4; | |
case "confusion": | |
return 1.5; | |
case "healblock": | |
return 1.75; | |
case "flinch": | |
return slownessRating ? slownessRating * 3 : 1; | |
case "saltcure": | |
return 2; | |
case "sparklingaria": | |
return 0.95; | |
case "syrupbomb": | |
return 1.5; | |
} | |
return 1; | |
} | |
/** | |
* @returns A multiplier to a move weighting based on the boosts it produces for the user. | |
*/ | |
boostWeight(move, movesSoFar, species, ability, level) { | |
const physicalIsRelevant = TeamGenerator.moveIsPhysical(move, species) || movesSoFar.some( | |
(m) => TeamGenerator.moveIsPhysical(m, species) && !m.overrideOffensiveStat && !m.overrideOffensivePokemon | |
); | |
const specialIsRelevant = TeamGenerator.moveIsSpecial(move, species) || movesSoFar.some((m) => TeamGenerator.moveIsSpecial(m, species)); | |
const adjustedStats = { | |
hp: species.baseStats.hp * level / 100 + level, | |
atk: species.baseStats.atk * level * level / 1e4, | |
def: species.baseStats.def * level / 100, | |
spa: species.baseStats.spa * level * level / 1e4, | |
spd: species.baseStats.spd * level / 100, | |
spe: species.baseStats.spe * level / 100 | |
}; | |
let weight = 0; | |
const accuracy = move.accuracy === true ? 100 : move.accuracy / 100; | |
const secondaryChance = move.secondary && ability !== "Sheer Force" ? Math.min((move.secondary.chance || 100) * (ability === "Serene Grace" ? 2 : 1) / 100, 100) * accuracy : 0; | |
const abilityMod = ability === "Simple" ? 2 : ability === "Contrary" ? -1 : 1; | |
const bodyPressMod = movesSoFar.some((m) => m.id === "bodyPress") ? 2 : 1; | |
const electroBallMod = movesSoFar.some((m) => m.id === "electroball") ? 2 : 1; | |
for (const { chance, boosts } of [ | |
{ chance: 1, boosts: move.boosts }, | |
{ chance: 1, boosts: move.self?.boosts }, | |
{ chance: 1, boosts: move.selfBoost?.boosts }, | |
{ | |
chance: secondaryChance, | |
boosts: move.secondary?.self?.boosts | |
} | |
]) { | |
if (!boosts || chance === 0) | |
continue; | |
const statusMod = move.category === "Status" ? 1 : 0.5; | |
if (boosts.atk && physicalIsRelevant) | |
weight += chance * boosts.atk * abilityMod * 2 * statusMod; | |
if (boosts.spa && specialIsRelevant) | |
weight += chance * boosts.spa * abilityMod * 2 * statusMod; | |
if (boosts.def) { | |
weight += chance * boosts.def * abilityMod * bodyPressMod * (adjustedStats.def > 60 ? 0.5 : 1) * statusMod; | |
} | |
if (boosts.spd) | |
weight += chance * boosts.spd * abilityMod * (adjustedStats.spd > 60 ? 0.5 : 1) * statusMod; | |
if (boosts.spe) { | |
weight += chance * boosts.spe * abilityMod * electroBallMod * (adjustedStats.spe > 95 ? 0.5 : 1) * statusMod; | |
} | |
} | |
return weight >= 0 ? 1 + weight : 1 / (1 - weight); | |
} | |
/** | |
* @returns A weight for a move based on how much it will reduce the opponent's stats. | |
*/ | |
opponentDebuffWeight(move) { | |
if (!["allAdjacentFoes", "allAdjacent", "foeSide", "normal"].includes(move.target)) | |
return 1; | |
let averageNumberOfDebuffs = 0; | |
for (const { chance, boosts } of [ | |
{ chance: 1, boosts: move.boosts }, | |
{ | |
chance: move.secondary ? (move.secondary.chance || 100) / 100 : 0, | |
boosts: move.secondary?.boosts | |
} | |
]) { | |
if (!boosts || chance === 0) | |
continue; | |
const numBoosts = Object.values(boosts).filter((x) => x < 0).length; | |
averageNumberOfDebuffs += chance * numBoosts; | |
} | |
return 1 + 0.5 * averageNumberOfDebuffs; | |
} | |
/** | |
* @returns A weight for an item. | |
*/ | |
getItemWeight(item, teamStats, species, moves, ability, level) { | |
const adjustedStats = { | |
hp: species.baseStats.hp * level / 100 + level, | |
atk: species.baseStats.atk * level * level / 1e4, | |
def: species.baseStats.def * level / 100, | |
spa: species.baseStats.spa * level * level / 1e4, | |
spd: species.baseStats.spd * level / 100, | |
spe: species.baseStats.spe * level / 100 | |
}; | |
const statusImmunities = ["Comatose", "Purifying Salt", "Shields Down", "Natural Cure", "Misty Surge"]; | |
let weight; | |
switch (item.id) { | |
case "choiceband": | |
return moves.every((x) => TeamGenerator.moveIsPhysical(x, species)) ? 50 : 0; | |
case "choicespecs": | |
return moves.every((x) => TeamGenerator.moveIsSpecial(x, species)) ? 50 : 0; | |
case "choicescarf": | |
if (moves.some((x) => x.category === "Status" || x.secondary?.self?.boosts?.spe)) | |
return 0; | |
if (adjustedStats.spe > 50 && adjustedStats.spe < 120) | |
return 50; | |
return 10; | |
case "lifeorb": | |
return moves.filter((x) => x.category !== "Status" && !x.damage && !x.damageCallback).length * 8; | |
case "focussash": | |
if (ability === "Sturdy") | |
return 0; | |
if (adjustedStats.hp < 65 && adjustedStats.def < 65 && adjustedStats.spd < 65) | |
return 35; | |
return 10; | |
case "heavydutyboots": | |
switch (this.dex.getEffectiveness("Rock", species)) { | |
case 1: | |
return 30; | |
case 0: | |
return 10; | |
} | |
return 5; | |
case "assaultvest": | |
if (moves.some((x) => x.category === "Status")) | |
return 0; | |
return 30; | |
case "scopelens": | |
const attacks = moves.filter((x) => x.category !== "Status" && !x.damage && !x.damageCallback && !x.willCrit); | |
if (moves.some((m) => m.id === "focusenergy")) { | |
if (ability === "Super Luck") | |
return 0; | |
return attacks.length * (ability === "Sniper" ? 16 : 12); | |
} else if (attacks.filter((x) => (x.critRatio || 1) > 1).length || ability === "Super Luck") { | |
return attacks.reduce((total, x) => { | |
let ratio = ability === "Super Luck" ? 2 : 1; | |
if ((x.critRatio || 1) > 1) | |
ratio++; | |
return total + [0, 3, 6, 12][ratio] * (ability === "Sniper" ? 4 / 3 : 1); | |
}, 0); | |
} | |
return 0; | |
case "eviolite": | |
return species.nfe || species.id === "dipplin" ? 100 : 0; | |
case "flameorb": | |
if (species.types.includes("Fire")) | |
return 0; | |
if (statusImmunities.includes(ability)) | |
return 0; | |
if (["Thermal Exchange", "Water Bubble", "Water Veil"].includes(ability)) | |
return 0; | |
weight = ["Guts", "Flare Boost"].includes(ability) ? 30 : 0; | |
if (moves.some((m) => m.id === "facade")) { | |
if (!weight && !moves.some((m) => TeamGenerator.moveIsPhysical(m, species) && m.id !== "facade")) { | |
weight = 30; | |
} else { | |
weight *= 2; | |
} | |
} | |
return weight; | |
case "toxicorb": | |
if (species.types.includes("Poison") || species.types.includes("Steel")) | |
return 0; | |
if (statusImmunities.includes(ability)) | |
return 0; | |
if (ability === "Immunity") | |
return 0; | |
if (!moves.some((m) => TeamGenerator.moveIsPhysical(m, species) && m.id !== "facade") && !species.types.includes("Fire") && ["Thermal Exchange", "Water Bubble", "Water Veil"].includes(ability)) | |
return 0; | |
weight = 0; | |
if (["Poison Heal", "Toxic Boost"].includes("ability")) | |
weight += 25; | |
if (moves.some((m) => m.id === "facade")) | |
weight += 25; | |
return weight; | |
case "leftovers": | |
return moves.some((m) => m.stallingMove) ? 40 : 20; | |
case "blacksludge": | |
return species.types.includes("Poison") ? moves.some((m) => m.stallingMove) ? 20 : 10 : 0; | |
case "sitrusberry": | |
case "magoberry": | |
return 20; | |
case "throatspray": | |
if (moves.some((m) => m.flags.sound) && moves.some((m) => m.category === "Special")) | |
return 30; | |
return 0; | |
default: | |
return 0; | |
} | |
} | |
/** | |
* @returns The level a Pokémon should be. | |
*/ | |
static getLevel(species) { | |
if (["Zacian", "Zamazenta"].includes(species.name)) { | |
species = import_sim.Dex.species.get(species.otherFormes[0]); | |
} else if (species.baseSpecies === "Squawkabilly") { | |
if (["Yellow", "White"].includes(species.forme)) { | |
species = import_sim.Dex.species.get("Squawkabilly-Yellow"); | |
} else { | |
species = import_sim.Dex.species.get("Squawkabilly"); | |
} | |
} else if (useBaseSpecies.includes(species.baseSpecies)) { | |
species = import_sim.Dex.species.get(species.baseSpecies); | |
} | |
if (levelOverride[species.id]) | |
return levelOverride[species.id]; | |
switch (species.tier) { | |
case "AG": | |
return 60; | |
case "Uber": | |
return 70; | |
case "OU": | |
case "Unreleased": | |
return 80; | |
case "UU": | |
return 90; | |
case "LC": | |
case "NFE": | |
return 100; | |
} | |
return 100; | |
} | |
/** | |
* Picks a choice from `choices` based on the weights in `weights`. | |
* `weights` must be the same length as `choices`. | |
*/ | |
weightedRandomPick(choices, weights, options) { | |
if (!choices.length) | |
throw new Error(`Can't pick from an empty list`); | |
if (choices.length !== weights.length) | |
throw new Error(`Choices and weights must be the same length`); | |
const totalWeight = weights.reduce((a, b) => a + b, 0); | |
let randomWeight = this.prng.random(0, totalWeight); | |
for (let i = 0; i < choices.length; i++) { | |
randomWeight -= weights[i]; | |
if (randomWeight < 0) { | |
const choice = choices[i]; | |
if (options?.remove) | |
choices.splice(i, 1); | |
return choice; | |
} | |
} | |
if (options?.remove && choices.length) | |
return choices.pop(); | |
return choices[choices.length - 1]; | |
} | |
setSeed(seed) { | |
this.prng.setSeed(seed); | |
} | |
} | |
//# sourceMappingURL=cg-teams.js.map | |