From 72467388fcb3f526d3194d1af44e0e109745f60b Mon Sep 17 00:00:00 2001 From: Xavion3 Date: Sat, 11 May 2024 14:20:21 +1000 Subject: [PATCH] Redo moveset generation (#550) * Redo moveset generation * Decrease the weight of lower power moves and egg moves * More weight tweaking Trainers never get OHKO moves, are less likely to get self KO and multiturn moves, and more likely to get stat buffing moves. All pokemon are less likely to get offstat moves. --- src/field/pokemon.ts | 158 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 128 insertions(+), 30 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 8afff1b2724..0b4da4c9cd5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4,7 +4,7 @@ import { Variant, VariantSet, variantColorCache } from '#app/data/variant'; import { variantData } from '#app/data/variant'; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from '../ui/battle-info'; import { Moves } from "../data/enums/moves"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveTypeAttr, VariableMoveCategoryAttr, CounterDamageAttr, IgnoreWeatherTypeDebuffAttr } from "../data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveTypeAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr } from "../data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from '../data/pokemon-species'; import * as Utils from '../utils'; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from '../data/type'; @@ -16,7 +16,7 @@ import { Gender } from '../data/gender'; import { initMoveAnim, loadMoveAnimAssets } from '../data/battle-anims'; import { Status, StatusEffect, getRandomStatus } from '../data/status-effect'; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from '../data/pokemon-evolutions'; -import { reverseCompatibleTms, tmSpecies } from '../data/tms'; +import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from '../data/tms'; import { DamagePhase, FaintPhase, LearnMovePhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase } from '../phases'; import { BattleStat } from '../data/battle-stat'; import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, getBattlerTag } from '../data/battler-tags'; @@ -46,6 +46,8 @@ import { TrainerSlot } from '../data/trainer-config'; import * as Overrides from '../overrides'; import { BerryType } from '../data/berry'; import i18next from '../plugins/i18n'; +import { speciesEggMoves } from '../data/egg-moves'; +import { ModifierTier } from '../modifier/modifier-tier'; export enum FieldPosition { CENTER, @@ -1137,7 +1139,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { generateAndPopulateMoveset(): void { this.moveset = []; - let movePool: Moves[] = []; + let movePool: [Moves, number][] = []; const allLevelMoves = this.getLevelMoves(1, true, true); if (!allLevelMoves) { console.log(this.species.speciesId, 'ERROR'); @@ -1148,38 +1150,134 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const levelMove = allLevelMoves[m]; if (this.level < levelMove[0]) break; - if (movePool.indexOf(levelMove[1]) === -1) { - if (!allMoves[levelMove[1]].name.endsWith(' (N)')) - movePool.push(levelMove[1]); - else - movePool.unshift(levelMove[1]); + let weight = levelMove[0]; + if (weight === 0) // Evo Moves + weight = 50; + if (weight === 1 && allMoves[levelMove[1]].power >= 80) // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight + weight = 40; + if (allMoves[levelMove[1]].name.endsWith(' (N)')) + weight /= 100; // Unimplemented level up moves are possible to generate, but 1% of their normal chance. + if (!movePool.some(m => m[0] === levelMove[1])) + movePool.push([levelMove[1], weight]); + } + + if (this.hasTrainer()) { + const tms = Object.keys(tmSpecies); + for (let tm of tms) { + const moveId = parseInt(tm) as Moves; + let compatible = false; + for (let p of tmSpecies[tm]) { + if (Array.isArray(p)) { + if (p[0] === this.species.speciesId || (this.fusionSpecies && p[0] === this.fusionSpecies.speciesId) && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) { + compatible = true; + break; + } + } else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) { + compatible = true; + break; + } + } + if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(' (N)')) { + if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) + movePool.push([moveId, 4]); + else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) + movePool.push([moveId, 8]); + else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) + movePool.push([moveId, 14]); + } + } + + if (this.level >= 25) { // No egg moves below level 25 + for (let i = 0; i < 3; i++) { + const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i]; + if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(' (N)')) + movePool.push([moveId, Math.min(this.level * 0.5, 40)]); + } + const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3]; + if (this.level >= 60 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(' (N)')) // No rare egg moves before level 60 + movePool.push([moveId, Math.min(this.level * 0.2, 20)]); + if (this.fusionSpecies) { + for (let i = 0; i < 3; i++) { + const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][i]; + if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(' (N)')) + movePool.push([moveId, Math.min(this.level * 0.5, 30)]); + } + const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3]; + if (this.level >= 60 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(' (N)')) // No rare egg moves before level 60 + movePool.push([moveId, Math.min(this.level * 0.2, 20)]); + } } } - if (this.isBoss()) - movePool = movePool.filter(m => !allMoves[m].getAttrs(SacrificialAttr).length); - - movePool.reverse(); - - const attackMovePool = movePool.filter(m => { - const move = allMoves[m]; - return move.category !== MoveCategory.STATUS; - }); - - const easeType = this.hasTrainer() || this.isBoss() ? this.hasTrainer() && this.isBoss() ? 'Quart.easeIn' : 'Cubic.easeIn' : 'Sine.easeIn'; - - if (attackMovePool.length) { - const randomAttackMove = Utils.randSeedEasedWeightedItem(attackMovePool, easeType); - this.moveset.push(new PokemonMove(randomAttackMove, 0, 0)); - console.log(allMoves[randomAttackMove]); - movePool.splice(movePool.findIndex(m => m === randomAttackMove), 1); + if (this.isBoss()) // Bosses never get self ko moves + movePool = movePool.filter(m => !allMoves[m[0]].getAttrs(SacrificialAttr).length); + if (this.hasTrainer()) { + // Trainers never get OHKO moves + movePool = movePool.filter(m => !allMoves[m[0]].getAttrs(OneHitKOAttr).length); + // Half the weight of self KO moves + movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].getAttrs(SacrificialAttr).length ? 0.5 : 1)]); + // Trainers get a weight bump to stat buffing moves + movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatChangeAttr).some(a => (a as StatChangeAttr).levels > 1 && (a as StatChangeAttr).selfTarget) ? 1.25 : 1)]); + // Trainers get a weight decrease to multiturn moves + movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].getAttrs(ChargeAttr).length || !!allMoves[m[0]].getAttrs(RechargeAttr).length ? 0.7 : 1)]); } - while (movePool.length && this.moveset.length < 4) { - const randomMove = Utils.randSeedEasedWeightedItem(movePool, easeType); - this.moveset.push(new PokemonMove(randomMove, 0, 0)); - console.log(allMoves[randomMove]); - movePool.splice(movePool.indexOf(randomMove), 1); + // Weight towards higher power moves, by reducing the power of moves below the highest power. + // Caps max power at 90 to avoid something like hyper beam ruining the stats. + // This is a pretty soft weighting factor, although it is scaled with the weight multiplier. + const maxPower = Math.min(movePool.reduce((v, m) => Math.max(allMoves[m[0]].power, v), 40), 90); + movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]); + + // Weight damaging moves against the lower stat + const worseCategory: MoveCategory = this.stats[Stat.ATK] > this.stats[Stat.SPATK] ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; + const statRatio = worseCategory === MoveCategory.PHYSICAL ? this.stats[Stat.ATK]/this.stats[Stat.SPATK] : this.stats[Stat.SPATK]/this.stats[Stat.ATK]; + movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1)]); + + let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight. + if (this.hasTrainer()) + weightMultiplier += 0.7; + if (this.isBoss()) + weightMultiplier += 0.4; + const baseWeights: [Moves, number][] = movePool.map(m => [m[0], Math.ceil(Math.pow(m[1], weightMultiplier)*100)]); + + if (this.hasTrainer() || this.isBoss()) { // Trainers and bosses always force a stab move + const stabMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS && this.isOfType(allMoves[m[0]].type)); + + if (stabMovePool.length) { + const totalWeight = stabMovePool.reduce((v, m) => v + m[1], 0); + let rand = Utils.randSeedInt(totalWeight); + let index = 0; + while (rand > stabMovePool[index][1]) + rand -= stabMovePool[index++][1]; + this.moveset.push(new PokemonMove(stabMovePool[index][0], 0, 0)); + } + } else { // Normal wild pokemon just force a random damaging move + const attackMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS); + if (attackMovePool.length) { + const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0); + let rand = Utils.randSeedInt(totalWeight); + let index = 0; + while (rand > attackMovePool[index][1]) + rand -= attackMovePool[index++][1]; + this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0)); + } + } + + while (movePool.length > 1 && this.moveset.length < 4) { + if (this.hasTrainer()) { + // Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier. + // Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights double if STAB. + // Status moves remain unchanged on weight, this encourages 1-2 + movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo.moveId)).map(m => [m[0], this.moveset.some(mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[m[0]].type) ? Math.ceil(Math.sqrt(m[1])) : allMoves[m[0]].category !== MoveCategory.STATUS ? Math.ceil(m[1]/Math.max(Math.pow(4, this.moveset.filter(mo => mo.getMove().power > 1).length)/8,0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1)) : m[1]]); + } else { // Non-trainer pokemon just use normal weights + movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo.moveId)); + } + const totalWeight = movePool.reduce((v, m) => v + m[1], 0); + let rand = Utils.randSeedInt(totalWeight); + let index = 0; + while (rand > movePool[index][1]) + rand -= movePool[index++][1]; + this.moveset.push(new PokemonMove(movePool[index][0], 0, 0)); } this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger);