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
parent
994da9813b
commit
72467388fc
|
@ -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()) // 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)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
if (this.isBoss())
|
||||||
movePool = movePool.filter(m => !allMoves[m].getAttrs(SacrificialAttr).length);
|
weightMultiplier += 0.4;
|
||||||
|
const baseWeights: [Moves, number][] = movePool.map(m => [m[0], Math.ceil(Math.pow(m[1], weightMultiplier)*100)]);
|
||||||
|
|
||||||
movePool.reverse();
|
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));
|
||||||
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 (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) {
|
if (attackMovePool.length) {
|
||||||
const randomAttackMove = Utils.randSeedEasedWeightedItem(attackMovePool, easeType);
|
const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0);
|
||||||
this.moveset.push(new PokemonMove(randomAttackMove, 0, 0));
|
let rand = Utils.randSeedInt(totalWeight);
|
||||||
console.log(allMoves[randomAttackMove]);
|
let index = 0;
|
||||||
movePool.splice(movePool.findIndex(m => m === randomAttackMove), 1);
|
while (rand > attackMovePool[index][1])
|
||||||
|
rand -= attackMovePool[index++][1];
|
||||||
|
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (movePool.length && this.moveset.length < 4) {
|
while (movePool.length > 1 && this.moveset.length < 4) {
|
||||||
const randomMove = Utils.randSeedEasedWeightedItem(movePool, easeType);
|
if (this.hasTrainer()) {
|
||||||
this.moveset.push(new PokemonMove(randomMove, 0, 0));
|
// Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier.
|
||||||
console.log(allMoves[randomMove]);
|
// Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights double if STAB.
|
||||||
movePool.splice(movePool.indexOf(randomMove), 1);
|
// 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);
|
||||||
|
|
Loading…
Reference in New Issue