Spaces:
Paused
Paused
| ; | |
| 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 | |