Translate Opportunist, Aggressive and Risky trainer AI
Add the AI_Opportunist, AI_Aggressive and AI_Risky scoring layers with the ROM's stall, reckless and risky tables, HP thresholds, last-wins ties and strict knockout test. AIDamageCalc is ported as a pre-variation damage estimate, including a critical-hit flag left over from an earlier move, which the original also reads. Smart remains the only refused layer, so the rival stays gated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e92936cb9
commit
0d1bedb3b1
10 files changed
+294
-11
No files matched your search
@@ -574,6 +574,7 @@ export class Battle {
|
||||
player: this.player, enemy: this.enemy,
|
||||
data: this.options.data, battleData: this.data, weather: this.weather,
|
||||
random: () => this.random(),
|
||||
damage: (move) => this.enemyAIDamage(move),
|
||||
});
|
||||
this.enemy.moveNum = choice.index;
|
||||
this.enemy.curMove = choice.move;
|
||||
@@ -594,6 +595,76 @@ export class Battle {
|
||||
throw new UnsupportedBattle("The battle did not end.");
|
||||
}
|
||||
|
||||
/** AIDamageCalc: pre-variation damage used only for scoring. It omits
|
||||
* accuracy and does not roll a critical hit, but, as in the ROM, a
|
||||
* wCriticalHit left over from an earlier move still selects unboosted
|
||||
* stats and doubles the estimate. */
|
||||
private enemyAIDamage(moveId: number): number {
|
||||
const move = this.options.data.move(moveId);
|
||||
switch (move.effect) {
|
||||
case C.EFFECT_SUPER_FANG:
|
||||
return Math.max(1, this.player.mon.hp >> 1);
|
||||
case C.EFFECT_STATIC_DAMAGE:
|
||||
return move.power;
|
||||
case C.EFFECT_LEVEL_DAMAGE:
|
||||
return this.enemy.mon.level;
|
||||
case C.EFFECT_PSYWAVE: {
|
||||
const limit = this.enemy.mon.level + (this.enemy.mon.level >> 1);
|
||||
for (let attempt = 0; attempt < 4096; attempt++) {
|
||||
const damage = this.random();
|
||||
if (damage && damage < limit) return damage;
|
||||
}
|
||||
throw new UnsupportedBattle("Trainer Psywave AI random source did not produce damage.");
|
||||
}
|
||||
}
|
||||
if (!move.power) return 0;
|
||||
if (this.weather) throw new UnsupportedBattle("Weather is not ported yet.");
|
||||
const special = move.type >= C.SPECIAL,
|
||||
attackStat = special ? C.SP_ATTACK : C.ATTACK,
|
||||
defenseStat = special ? C.SP_DEFENSE : C.DEFENSE,
|
||||
screen = special ? C.SCREENS_LIGHT_SCREEN : C.SCREENS_REFLECT;
|
||||
const unboosted =
|
||||
this.criticalHit && this.player.levels[defenseStat] >= this.enemy.levels[attackStat];
|
||||
let attack: number, defense: number;
|
||||
if (unboosted) {
|
||||
attack = this.enemy.stats[attackStat];
|
||||
defense = this.player.stats[defenseStat];
|
||||
} else {
|
||||
attack = this.enemy.mon.stats[attackStat];
|
||||
defense = this.player.mon.stats[defenseStat];
|
||||
if (this.player.screens & (1 << screen)) defense = (defense * 2) & 0xffff;
|
||||
}
|
||||
const boosted = special
|
||||
? this.enemy.mon.species === C.PIKACHU && this.enemy.mon.item === C.LIGHT_BALL
|
||||
: (this.enemy.mon.species === C.CUBONE || this.enemy.mon.species === C.MAROWAK) && this.enemy.mon.item === C.THICK_CLUB;
|
||||
if (boosted) attack = (attack * 2) & 0xffff;
|
||||
[attack, defense] = this.truncate(attack, defense);
|
||||
if (this.player.mon.species === C.DITTO && this.player.mon.item === C.METAL_POWDER)
|
||||
throw new UnsupportedBattle("Metal Powder is not ported yet.");
|
||||
const held = this.data.heldEffect(this.enemy.mon.item),
|
||||
typeBoost = this.data.typeBoost(held.effect),
|
||||
boost = held.effect && typeBoost === move.type ? held.parameter : undefined,
|
||||
baseDamage = calculateDamage({
|
||||
power: move.power,
|
||||
level: this.enemy.mon.level,
|
||||
attack,
|
||||
defense,
|
||||
effect: move.effect,
|
||||
critical: Boolean(this.criticalHit),
|
||||
boost,
|
||||
current: 0,
|
||||
});
|
||||
if (baseDamage === undefined) return 0;
|
||||
return applyTypeMatchups(this.matchups, {
|
||||
damage: baseDamage,
|
||||
modifier: C.EFFECTIVE,
|
||||
moveType: move.type,
|
||||
userTypes: this.enemy.mon.types,
|
||||
targetTypes: this.player.mon.types,
|
||||
identified: this.player.has(SUB1, C.SUBSTATUS_IDENTIFIED),
|
||||
}).damage;
|
||||
}
|
||||
|
||||
/** BattleMenu: FIGHT chooses a move; RUN tries to flee. Returns true if
|
||||
* the battle ended from the menu. */
|
||||
private *battleMenu(): Gen<boolean> {
|
||||
|
||||
Reference in new issue
Block a user