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.
pull/735/head
Xavion3 2024-05-11 14:20:21 +10:00 committed by GitHub
parent 994da9813b
commit 72467388fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 128 additions and 30 deletions

View File

@ -4,7 +4,7 @@ import { Variant, VariantSet, variantColorCache } from '#app/data/variant';
import { variantData } from '#app/data/variant'; import { variantData } from '#app/data/variant';
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from '../ui/battle-info'; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from '../ui/battle-info';
import { Moves } from "../data/enums/moves"; 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 { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from '../data/pokemon-species';
import * as Utils from '../utils'; import * as Utils from '../utils';
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from '../data/type'; 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 { initMoveAnim, loadMoveAnimAssets } from '../data/battle-anims';
import { Status, StatusEffect, getRandomStatus } from '../data/status-effect'; import { Status, StatusEffect, getRandomStatus } from '../data/status-effect';
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from '../data/pokemon-evolutions'; 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 { DamagePhase, FaintPhase, LearnMovePhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase } from '../phases';
import { BattleStat } from '../data/battle-stat'; import { BattleStat } from '../data/battle-stat';
import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, getBattlerTag } from '../data/battler-tags'; 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 * as Overrides from '../overrides';
import { BerryType } from '../data/berry'; import { BerryType } from '../data/berry';
import i18next from '../plugins/i18n'; import i18next from '../plugins/i18n';
import { speciesEggMoves } from '../data/egg-moves';
import { ModifierTier } from '../modifier/modifier-tier';
export enum FieldPosition { export enum FieldPosition {
CENTER, CENTER,
@ -1137,7 +1139,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
generateAndPopulateMoveset(): void { generateAndPopulateMoveset(): void {
this.moveset = []; this.moveset = [];
let movePool: Moves[] = []; let movePool: [Moves, number][] = [];
const allLevelMoves = this.getLevelMoves(1, true, true); const allLevelMoves = this.getLevelMoves(1, true, true);
if (!allLevelMoves) { if (!allLevelMoves) {
console.log(this.species.speciesId, 'ERROR'); console.log(this.species.speciesId, 'ERROR');
@ -1148,38 +1150,134 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const levelMove = allLevelMoves[m]; const levelMove = allLevelMoves[m];
if (this.level < levelMove[0]) if (this.level < levelMove[0])
break; break;
if (movePool.indexOf(levelMove[1]) === -1) { let weight = levelMove[0];
if (!allMoves[levelMove[1]].name.endsWith(' (N)')) if (weight === 0) // Evo Moves
movePool.push(levelMove[1]); weight = 50;
else if (weight === 1 && allMoves[levelMove[1]].power >= 80) // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight
movePool.unshift(levelMove[1]); 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()) if (this.isBoss()) // Bosses never get self ko moves
movePool = movePool.filter(m => !allMoves[m].getAttrs(SacrificialAttr).length); movePool = movePool.filter(m => !allMoves[m[0]].getAttrs(SacrificialAttr).length);
if (this.hasTrainer()) {
movePool.reverse(); // Trainers never get OHKO moves
movePool = movePool.filter(m => !allMoves[m[0]].getAttrs(OneHitKOAttr).length);
const attackMovePool = movePool.filter(m => { // Half the weight of self KO moves
const move = allMoves[m]; movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].getAttrs(SacrificialAttr).length ? 0.5 : 1)]);
return move.category !== MoveCategory.STATUS; // 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
const easeType = this.hasTrainer() || this.isBoss() ? this.hasTrainer() && this.isBoss() ? 'Quart.easeIn' : 'Cubic.easeIn' : 'Sine.easeIn'; movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].getAttrs(ChargeAttr).length || !!allMoves[m[0]].getAttrs(RechargeAttr).length ? 0.7 : 1)]);
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);
} }
while (movePool.length && this.moveset.length < 4) { // Weight towards higher power moves, by reducing the power of moves below the highest power.
const randomMove = Utils.randSeedEasedWeightedItem(movePool, easeType); // Caps max power at 90 to avoid something like hyper beam ruining the stats.
this.moveset.push(new PokemonMove(randomMove, 0, 0)); // This is a pretty soft weighting factor, although it is scaled with the weight multiplier.
console.log(allMoves[randomMove]); const maxPower = Math.min(movePool.reduce((v, m) => Math.max(allMoves[m[0]].power, v), 40), 90);
movePool.splice(movePool.indexOf(randomMove), 1); 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); this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger);