Port the moves that span several turns
A recharging, charged, rampaging or rolling Pokémon plays on without the battle menu on either side, and a biding one uses FIGHT without the move list. Razor Wind, Solarbeam, Skull Bash, Sky Attack, Fly and Dig charge first; flying and digging Pokémon can only be hit by the original's exceptions. Hyper Beam recharges, Thrash and its kind end in confusion, Rollout and Fury Cutter double up, and Bide returns twice the damage it took. The trapping moves hurt their target between turns until it is released, Mind Reader and Lock-On make the next move hit, and Pursuit strikes a Pokémon being withdrawn first, on either side. The new commands are compared with the original in the reference emulator. Its fixture no longer zeroes wPlayerScreens with its sentinel. Part of #28 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e0a2cd2a8
commit
c53edb77e5
7 files changed
+713
-39
No files matched your search
+333
-31
@@ -73,7 +73,9 @@ class Side {
|
||||
furyCutterCount = 0;
|
||||
protectCount = 0;
|
||||
rageCounter = 0;
|
||||
/** wWrapCount: turns this side stays trapped, by trappingMove. */
|
||||
wrapCount = 0;
|
||||
trappingMove = 0;
|
||||
damageTaken = 0;
|
||||
screens = 0;
|
||||
lightScreenCount = 0;
|
||||
@@ -495,6 +497,8 @@ export class Battle {
|
||||
lastPlayerMon = 0;
|
||||
/** wEnemyIsSwitching: ParseEnemyAction keeps the current move. */
|
||||
private enemyIsSwitching = 0;
|
||||
/** wPlayerIsSwitching, which Pursuit doubles against. */
|
||||
private playerIsSwitching = 0;
|
||||
/** wEnemyHPAtTimeOfPlayerSwitch, which WithdrawMonText compares with. */
|
||||
private enemyHPAtSwitch = 0;
|
||||
/** wPlayerUsedMoves: the player's distinct moves since sending out. */
|
||||
@@ -551,6 +555,11 @@ export class Battle {
|
||||
get partyMon(): PartyMon {
|
||||
return this.options.party[this.curBattleMon];
|
||||
}
|
||||
/** ParsePlayerAction skips MoveSelectionScreen: FIGHT goes on with the
|
||||
* current move, as while biding. */
|
||||
get fightKeepsMove() {
|
||||
return this.player.has(SUB3, C.SUBSTATUS_BIDE);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- messages
|
||||
|
||||
@@ -638,6 +647,17 @@ export class Battle {
|
||||
}
|
||||
case at("Text_1_2_and_Poof"):
|
||||
return BATTLE["Text_1_2_and_Poof.MoveForgotText"];
|
||||
case at("BattleCommand_Charge.UsedText"): {
|
||||
const text = {
|
||||
[C.RAZOR_WIND]: "BattleCommand_Charge.BattleMadeWhirlwindText",
|
||||
[C.SOLARBEAM]: "BattleCommand_Charge.BattleTookSunlightText",
|
||||
[C.SKULL_BASH]: "BattleCommand_Charge.BattleLoweredHeadText",
|
||||
[C.SKY_ATTACK]: "BattleCommand_Charge.BattleGlowingText",
|
||||
[C.FLY]: "BattleCommand_Charge.BattleFlewText",
|
||||
}[this.user.move.anim];
|
||||
// Any other move falls through to the Dig text.
|
||||
return BATTLE[(text ?? "BattleCommand_Charge.BattleDugText") as keyof typeof BATTLE];
|
||||
}
|
||||
case at("Text_GotchaMonWasCaught"):
|
||||
// Capture music (not ported), then WaitButtonText.
|
||||
return BATTLE.WaitButtonText;
|
||||
@@ -978,6 +998,7 @@ export class Battle {
|
||||
for (let turns = 0; turns < 1000; turns++) {
|
||||
this.curDamage = 0;
|
||||
this.enemyIsSwitching = 0;
|
||||
this.playerIsSwitching = 0;
|
||||
this.player.justGotFrozen = 0;
|
||||
this.enemy.justGotFrozen = 0;
|
||||
this.updateBattleMonInParty();
|
||||
@@ -1115,6 +1136,8 @@ export class Battle {
|
||||
continue;
|
||||
}
|
||||
this.battlePlayerAction = 0;
|
||||
// ParsePlayerAction: Bide goes on without MoveSelectionScreen.
|
||||
if (this.player.has(SUB3, C.SUBSTATUS_BIDE)) return false;
|
||||
if (!(yield* this.moveSelection(input.move))) continue;
|
||||
return false;
|
||||
}
|
||||
@@ -1693,8 +1716,10 @@ export class Battle {
|
||||
/** ParsePlayerAction then ParseEnemyAction. Returns false to re-open the menu. */
|
||||
private *parsePlayerAction(): Gen<boolean> {
|
||||
const player = this.player;
|
||||
if (this.checkPlayerLockedIn() || player.has(SUB5, C.SUBSTATUS_ENCORED) || player.has(SUB3, C.SUBSTATUS_BIDE))
|
||||
throw new UnsupportedBattle("Locked-in, encored and Bide moves are not ported yet.");
|
||||
// A locked-in or biding Pokémon keeps last turn's move and move struct.
|
||||
if (this.checkPlayerLockedIn()) return this.playerLockedIn();
|
||||
if (player.has(SUB5, C.SUBSTATUS_ENCORED))
|
||||
throw new UnsupportedBattle("Encore is not ported yet.");
|
||||
if (this.battlePlayerAction === C.BATTLEPLAYERACTION_SWITCH) {
|
||||
// PlayerSwitch already ran ParseEnemyAction.
|
||||
player.furyCutterCount = 0;
|
||||
@@ -1704,6 +1729,7 @@ export class Battle {
|
||||
return true;
|
||||
}
|
||||
if (this.battlePlayerAction === 0) {
|
||||
if (player.has(SUB3, C.SUBSTATUS_BIDE)) return this.playerLockedIn();
|
||||
this.turn = 0;
|
||||
this.updateMoveData();
|
||||
player.charging = 0;
|
||||
@@ -1716,23 +1742,36 @@ export class Battle {
|
||||
player.protectCount = 0;
|
||||
} else {
|
||||
player.reset(SUB3, C.SUBSTATUS_BIDE);
|
||||
player.furyCutterCount = 0;
|
||||
player.protectCount = 0;
|
||||
player.rageCounter = 0;
|
||||
player.reset(SUB4, C.SUBSTATUS_RAGE);
|
||||
return this.playerLockedIn();
|
||||
}
|
||||
this.parseEnemyAction();
|
||||
return true;
|
||||
}
|
||||
/** ParsePlayerAction's .locked_in. */
|
||||
private playerLockedIn() {
|
||||
const player = this.player;
|
||||
player.furyCutterCount = 0;
|
||||
player.protectCount = 0;
|
||||
player.rageCounter = 0;
|
||||
player.reset(SUB4, C.SUBSTATUS_RAGE);
|
||||
this.parseEnemyAction();
|
||||
return true;
|
||||
}
|
||||
/** ParseEnemyAction: use the trainer AI selection or a random wild move. */
|
||||
private parseEnemyAction() {
|
||||
if (this.enemyIsSwitching) return;
|
||||
const enemy = this.enemy,
|
||||
mon = enemy.mon;
|
||||
if (enemy.has(SUB5, C.SUBSTATUS_ENCORED) || this.checkEnemyLockedIn())
|
||||
throw new UnsupportedBattle(
|
||||
"Locked-in and encored enemy moves are not ported yet.",
|
||||
);
|
||||
if (enemy.has(SUB5, C.SUBSTATUS_ENCORED))
|
||||
throw new UnsupportedBattle("Encore is not ported yet.");
|
||||
if (this.checkEnemyLockedIn()) {
|
||||
// ResetVarsForSubstatusRage: the move and its struct stay.
|
||||
enemy.furyCutterCount = 0;
|
||||
enemy.protectCount = 0;
|
||||
enemy.rageCounter = 0;
|
||||
enemy.reset(SUB4, C.SUBSTATUS_RAGE);
|
||||
return;
|
||||
}
|
||||
let move: number = C.STRUGGLE;
|
||||
const usable = mon.moves.some(
|
||||
(candidate, slot) =>
|
||||
@@ -2204,6 +2243,43 @@ export class Battle {
|
||||
return;
|
||||
case COMMAND.fakeout:
|
||||
return this.fakeOut();
|
||||
case COMMAND.checkcharge:
|
||||
return this.checkCharge();
|
||||
case COMMAND.charge:
|
||||
return yield* this.charge();
|
||||
case COMMAND.skipsuncharge:
|
||||
// A sunny day skips the charging turn.
|
||||
if (this.weather === C.WEATHER_SUN) this.skipTo(COMMAND.charge);
|
||||
return;
|
||||
case COMMAND.rechargenextturn:
|
||||
this.user.set(SUB4, C.SUBSTATUS_RECHARGE);
|
||||
return;
|
||||
case COMMAND.checkrampage:
|
||||
return this.checkRampage();
|
||||
case COMMAND.rampage:
|
||||
return this.rampage();
|
||||
case COMMAND.checkrollout:
|
||||
return this.checkRollout();
|
||||
case COMMAND.rolloutpower:
|
||||
return this.rolloutPower();
|
||||
case COMMAND.furycutter:
|
||||
return this.furyCutter();
|
||||
case COMMAND.storeenergy:
|
||||
return yield* this.storeEnergy();
|
||||
case COMMAND.unleashenergy:
|
||||
return this.unleashEnergy();
|
||||
case COMMAND.bidefailtext:
|
||||
if (!this.attackMissed) return;
|
||||
return yield* this.say(this.typeModifier & C.EFFECTIVENESS_MASK ? BATTLE.ButItFailedText : BATTLE.DoesntAffectText);
|
||||
case COMMAND.traptarget:
|
||||
return yield* this.trapTarget();
|
||||
case COMMAND.lockon:
|
||||
return yield* this.lockOn();
|
||||
case COMMAND.pursuit:
|
||||
// Double damage against a Pokémon being withdrawn.
|
||||
if (this.turn === 0 ? this.enemyIsSwitching : this.playerIsSwitching)
|
||||
this.curDamage = Math.min(0xffff, this.curDamage * 2);
|
||||
return;
|
||||
case COMMAND.failuretext:
|
||||
return yield* this.failureText();
|
||||
case COMMAND.applydamage:
|
||||
@@ -2509,10 +2585,18 @@ export class Battle {
|
||||
}
|
||||
if (target.has(SUB4, C.SUBSTATUS_SUBSTITUTE) && (effect === C.EFFECT_LEECH_HIT || effect === C.EFFECT_DREAM_EATER))
|
||||
return miss();
|
||||
if (target.has(SUB5, C.SUBSTATUS_LOCK_ON))
|
||||
throw new UnsupportedBattle("Lock-On is not ported yet.");
|
||||
if (target.sub[SUB3] & ((1 << C.SUBSTATUS_FLYING) | (1 << C.SUBSTATUS_UNDERGROUND)))
|
||||
throw new UnsupportedBattle("Fly and Dig are not ported yet.");
|
||||
// .LockOn: a locked-on target is hit, unless it is flying from an
|
||||
// Earthquake, Fissure or Magnitude; the lock is spent either way.
|
||||
const anim = user.move.anim,
|
||||
groundMoves: number[] = [C.EARTHQUAKE, C.FISSURE, C.MAGNITUDE];
|
||||
if (target.has(SUB5, C.SUBSTATUS_LOCK_ON)) {
|
||||
target.reset(SUB5, C.SUBSTATUS_LOCK_ON);
|
||||
if (!(target.has(SUB3, C.SUBSTATUS_FLYING) && groundMoves.includes(anim))) return;
|
||||
}
|
||||
// .FlyDigMoves
|
||||
if (target.has(SUB3, C.SUBSTATUS_FLYING)) {
|
||||
if (!([C.GUST, C.WHIRLWIND, C.THUNDER, C.TWISTER] as number[]).includes(anim)) return miss();
|
||||
} else if (target.has(SUB3, C.SUBSTATUS_UNDERGROUND) && !groundMoves.includes(anim)) return miss();
|
||||
if (effect === C.EFFECT_THUNDER && this.weather === C.WEATHER_RAIN) return;
|
||||
if (user.has(SUB4, C.SUBSTATUS_X_ACCURACY)) return;
|
||||
if (effect === C.EFFECT_ALWAYS_HIT) return;
|
||||
@@ -2548,8 +2632,12 @@ export class Battle {
|
||||
if (!this.attackMissed) return;
|
||||
yield* this.getFailureResultText();
|
||||
const user = this.user;
|
||||
if (user.move.anim === C.FLY || user.move.anim === C.DIG)
|
||||
throw new UnsupportedBattle("Fly and Dig are not ported yet.");
|
||||
if (user.move.anim === C.FLY || user.move.anim === C.DIG) {
|
||||
user.reset(SUB3, C.SUBSTATUS_UNDERGROUND);
|
||||
user.reset(SUB3, C.SUBSTATUS_FLYING);
|
||||
this.requireNoSubstitute(); // AppearUserRaiseSub
|
||||
return this.endMoveEffect();
|
||||
}
|
||||
// Multi-hit moves other than Triple Kick and Beat Up raise the
|
||||
// substitute again (Beat Up not doing so is a kept bug).
|
||||
const effect = user.move.effect;
|
||||
@@ -2771,10 +2859,7 @@ export class Battle {
|
||||
throw new UnsupportedBattle("Beat Up with an Egg in the party is not ported yet.");
|
||||
// The player's members go by nickname, a trainer's by species name.
|
||||
this.strings[RAM.stringBuffer1] = player ? mon.nickname : data.speciesName(mon.species);
|
||||
const skip = () => {
|
||||
while (this.script[this.scriptIndex++] !== COMMAND.buildopponentrage)
|
||||
if (this.scriptIndex >= this.script.length) throw new UnsupportedBattle("Beat Up script has no buildopponentrage.");
|
||||
};
|
||||
const skip = () => this.skipTo(COMMAND.buildopponentrage);
|
||||
if (!mon.hp) return skip();
|
||||
// The player's side takes the battle status when wCurBattleMon equals
|
||||
// the member's HP low byte rather than its index (a kept bug).
|
||||
@@ -2995,6 +3080,174 @@ export class Battle {
|
||||
yield this.hud();
|
||||
}
|
||||
|
||||
/** SkipToBattleCommand: go on after the next `command`. */
|
||||
private skipTo(command: number) {
|
||||
while (this.script[this.scriptIndex++] !== command)
|
||||
if (this.scriptIndex >= this.script.length)
|
||||
throw new UnsupportedBattle(`Battle script has no command 0x${command.toString(16)}.`);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ multi-turn moves
|
||||
|
||||
/** BattleCommand_CheckCharge: a charged move's second turn skips to
|
||||
* after `charge`. */
|
||||
private checkCharge() {
|
||||
const user = this.user;
|
||||
if (!user.has(SUB3, C.SUBSTATUS_CHARGED)) return;
|
||||
user.reset(SUB3, C.SUBSTATUS_CHARGED);
|
||||
user.reset(SUB3, C.SUBSTATUS_UNDERGROUND);
|
||||
user.reset(SUB3, C.SUBSTATUS_FLYING);
|
||||
this.skipTo(COMMAND.charge);
|
||||
}
|
||||
/** BattleCommand_Charge: the charging turn. Fly and Dig take the user
|
||||
* out of reach; Skull Bash goes on to its Defense raise. A sleeping
|
||||
* user (through Sleep Talk) fails. */
|
||||
private *charge(): Gen {
|
||||
const user = this.user;
|
||||
if (user.mon.status & C.SLP_MASK) {
|
||||
this.requireNoSubstitute(); // BattleCommand_RaiseSub
|
||||
yield* this.say(BATTLE.ButItFailedText);
|
||||
return this.endMoveEffect();
|
||||
}
|
||||
user.set(SUB3, C.SUBSTATUS_CHARGED);
|
||||
if (this.alreadyDisobeyed) yield* this.say(BATTLE.IgnoredOrders2Text);
|
||||
this.requireNoSubstitute(); // BattleCommand_LowerSub and RaiseSub
|
||||
this.animParam = 1;
|
||||
const anim = user.move.anim;
|
||||
if (anim === C.FLY) user.set(SUB3, C.SUBSTATUS_FLYING);
|
||||
else if (anim === C.DIG) user.set(SUB3, C.SUBSTATUS_UNDERGROUND);
|
||||
if (!user.charging) {
|
||||
user.lastCounterMove = anim;
|
||||
user.lastMove = anim;
|
||||
}
|
||||
this.curDamage = 0;
|
||||
yield* this.say(BATTLE["BattleCommand_Charge.UsedText"]);
|
||||
if (user.move.effect === C.EFFECT_SKULL_BASH) return this.skipTo(COMMAND.endturn);
|
||||
this.endMoveEffect();
|
||||
}
|
||||
/** BattleCommand_CheckRampage: a rampage's later turns skip to after
|
||||
* `rampage`; the last one confuses the user for 2-3 turns unless its
|
||||
* side has Safeguard. */
|
||||
private checkRampage() {
|
||||
const user = this.user;
|
||||
if (!user.has(SUB3, C.SUBSTATUS_RAMPAGE)) return;
|
||||
user.rolloutCount = (user.rolloutCount - 1) & 0xff;
|
||||
if (!user.rolloutCount) {
|
||||
user.reset(SUB3, C.SUBSTATUS_RAMPAGE);
|
||||
if (!(user.screens & (1 << C.SCREENS_SAFEGUARD))) {
|
||||
user.set(SUB3, C.SUBSTATUS_CONFUSED);
|
||||
user.confuseCount = (this.random() & 1) + 2;
|
||||
}
|
||||
}
|
||||
this.skipTo(COMMAND.rampage);
|
||||
}
|
||||
/** BattleCommand_Rampage: 1 or 2 more turns, not while asleep. */
|
||||
private rampage() {
|
||||
const user = this.user;
|
||||
if (user.mon.status & C.SLP_MASK) return;
|
||||
user.set(SUB3, C.SUBSTATUS_RAMPAGE);
|
||||
user.rolloutCount = (this.random() & 1) + 1;
|
||||
}
|
||||
/** BattleCommand_CheckRollout: a rolling Pokémon skips to after doturn. */
|
||||
private checkRollout() {
|
||||
const user = this.user;
|
||||
if (user.has(SUB1, C.SUBSTATUS_ROLLOUT)) return this.skipTo(COMMAND.doturn);
|
||||
user.rolloutCount = 0;
|
||||
}
|
||||
/** BattleCommand_RolloutPower: the damage doubles for each hit so far
|
||||
* (once more after Defense Curl), for up to five hits; a miss stops. */
|
||||
private rolloutPower() {
|
||||
const user = this.user;
|
||||
if (user.mon.status & C.SLP_MASK) return;
|
||||
if (this.attackMissed) return user.reset(SUB1, C.SUBSTATUS_ROLLOUT);
|
||||
user.rolloutCount = (user.rolloutCount + 1) & 0xff;
|
||||
let doublings = user.rolloutCount;
|
||||
if (doublings >= 5) user.reset(SUB1, C.SUBSTATUS_ROLLOUT);
|
||||
else user.set(SUB1, C.SUBSTATUS_ROLLOUT);
|
||||
if (user.has(SUB2, C.SUBSTATUS_CURLED)) doublings++;
|
||||
this.doubleDamage(doublings - 1);
|
||||
}
|
||||
/** BattleCommand_FuryCutter: doubles for each hit in a row, up to 16×;
|
||||
* a miss starts over. */
|
||||
private furyCutter() {
|
||||
const user = this.user;
|
||||
if (this.attackMissed) {
|
||||
user.furyCutterCount = 0;
|
||||
return;
|
||||
}
|
||||
user.furyCutterCount = (user.furyCutterCount + 1) & 0xff;
|
||||
this.doubleDamage(Math.min(user.furyCutterCount, 5) - 1);
|
||||
}
|
||||
/** Double wCurDamage `times` times, stopping at $ffff. */
|
||||
private doubleDamage(times: number) {
|
||||
for (; times > 0; times--) {
|
||||
this.curDamage *= 2;
|
||||
if (this.curDamage > 0xffff) {
|
||||
this.curDamage = 0xffff;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** BattleCommand_StoreEnergy: while biding, the user stores energy; on
|
||||
* the last turn it unleashes twice the damage it took. */
|
||||
private *storeEnergy(): Gen {
|
||||
const user = this.user;
|
||||
if (!user.has(SUB3, C.SUBSTATUS_BIDE)) return;
|
||||
user.rolloutCount = (user.rolloutCount - 1) & 0xff;
|
||||
if (user.rolloutCount) {
|
||||
yield* this.say(BATTLE.StoringEnergyText);
|
||||
return this.endMoveEffect();
|
||||
}
|
||||
user.reset(SUB3, C.SUBSTATUS_BIDE);
|
||||
yield* this.say(BATTLE.UnleashedEnergyText);
|
||||
user.move.power = 1;
|
||||
this.curDamage = Math.min(0xffff, user.damageTaken * 2);
|
||||
if (!this.curDamage) this.attackMissed = 1;
|
||||
user.damageTaken = 0;
|
||||
user.charging = 0;
|
||||
user.move.anim = C.BIDE;
|
||||
this.skipTo(COMMAND.unleashenergy);
|
||||
}
|
||||
/** BattleCommand_UnleashEnergy: Bide's first turn. It also clears both
|
||||
* move structs' effects. */
|
||||
private unleashEnergy() {
|
||||
const user = this.user;
|
||||
user.set(SUB3, C.SUBSTATUS_BIDE);
|
||||
user.damageTaken = 0;
|
||||
this.player.move.effect = 0;
|
||||
this.enemy.move.effect = 0;
|
||||
user.rolloutCount = (this.random() & 1) + 2;
|
||||
this.animParam = 1;
|
||||
this.endMoveEffect();
|
||||
}
|
||||
/** BattleCommand_TrapTarget: trapped for 2-5 more turns, unless already
|
||||
* trapped or behind a substitute. */
|
||||
private *trapTarget(): Gen {
|
||||
if (this.attackMissed) return;
|
||||
const target = this.target;
|
||||
if (target.wrapCount || target.has(SUB4, C.SUBSTATUS_SUBSTITUTE)) return;
|
||||
target.wrapCount = (this.random() & 3) + 3;
|
||||
const anim = this.user.move.anim;
|
||||
target.trappingMove = anim;
|
||||
const text = {
|
||||
[C.BIND]: BATTLE.UsedBindText,
|
||||
[C.WRAP]: BATTLE.WrappedByText,
|
||||
[C.FIRE_SPIN]: BATTLE.FireSpinTrapText,
|
||||
[C.CLAMP]: BATTLE.ClampedByText,
|
||||
[C.WHIRLPOOL]: BATTLE.WhirlpoolTrapText,
|
||||
}[anim];
|
||||
if (text === undefined) throw new UnsupportedBattle("Unknown trapping move.");
|
||||
yield* this.say(text);
|
||||
}
|
||||
/** BattleCommand_LockOn: the user's next move cannot miss the target. */
|
||||
private *lockOn(): Gen {
|
||||
const target = this.target;
|
||||
if (target.has(SUB4, C.SUBSTATUS_SUBSTITUTE) || this.attackMissed)
|
||||
return yield* this.say(BATTLE.DidntAffect1Text);
|
||||
target.set(SUB5, C.SUBSTATUS_LOCK_ON);
|
||||
yield* this.say(BATTLE.TookAimText);
|
||||
}
|
||||
|
||||
/** BattleCommand_HeldFlinch (kingsrock). */
|
||||
private kingsRock() {
|
||||
if (this.attackMissed) return;
|
||||
@@ -3481,9 +3734,11 @@ export class Battle {
|
||||
private *handleBetweenTurnEffects(): Gen {
|
||||
if (yield* this.checkFaintPlayerThenEnemy()) return;
|
||||
if (this.weather) throw new UnsupportedBattle("Weather is not ported yet.");
|
||||
yield* this.handleWrap();
|
||||
if (yield* this.checkFaintPlayerThenEnemy()) return;
|
||||
for (const side of [this.player, this.enemy]) {
|
||||
if (side.wrapCount || side.has(SUB1, C.SUBSTATUS_PERISH) || side.has(SUB5, C.SUBSTATUS_ENCORED))
|
||||
throw new UnsupportedBattle("Wrap, Perish Song and Encore are not ported yet.");
|
||||
if (side.has(SUB1, C.SUBSTATUS_PERISH) || side.has(SUB5, C.SUBSTATUS_ENCORED))
|
||||
throw new UnsupportedBattle("Perish Song and Encore are not ported yet.");
|
||||
if (side.screens & ~((1 << C.SCREENS_LIGHT_SCREEN) | (1 << C.SCREENS_REFLECT)))
|
||||
throw new UnsupportedBattle("Other screen effects are not ported yet.");
|
||||
const effect = this.data.heldEffect(side.mon.item).effect;
|
||||
@@ -3522,6 +3777,25 @@ export class Battle {
|
||||
}
|
||||
this.updateBattleMonInParty();
|
||||
}
|
||||
/** HandleWrap: a trapped side loses 1/16 of its maximum HP, or is
|
||||
* released on its last turn. A substitute shields it and stops the
|
||||
* count. */
|
||||
private *handleWrap(): Gen {
|
||||
for (const [turn, side] of [this.player, this.enemy].entries()) {
|
||||
this.turn = turn;
|
||||
if (!side.wrapCount || side.has(SUB4, C.SUBSTATUS_SUBSTITUTE)) continue;
|
||||
this.strings[RAM.stringBuffer1] = this.options.data.moveName(side.trappingMove);
|
||||
side.wrapCount--;
|
||||
if (!side.wrapCount) {
|
||||
yield* this.say(BATTLE.BattleText_UserWasReleasedFromStringBuffer1);
|
||||
continue;
|
||||
}
|
||||
// GetSixteenthMaxHP
|
||||
this.subtractHP(side, (side.mon.maxHp >> 2 || 1) >> 2 || 1);
|
||||
yield this.hud();
|
||||
yield* this.say(BATTLE.BattleText_UsersHurtByStringBuffer1);
|
||||
}
|
||||
}
|
||||
private *checkFaintPlayerThenEnemy(): Gen<boolean> {
|
||||
if (this.hasFainted(this.player)) {
|
||||
yield* this.handlePlayerMonFaint();
|
||||
@@ -3696,18 +3970,47 @@ export class Battle {
|
||||
}
|
||||
/** PlayerSwitch: the enemy chooses its move, then BattleMonEntrance. */
|
||||
private *playerSwitch(): Gen {
|
||||
this.playerIsSwitching = 1;
|
||||
this.parseEnemyAction();
|
||||
yield* this.say(BATTLE["WithdrawMonText.WithdrawMonText"]);
|
||||
this.player.reset(SUB4, C.SUBSTATUS_RAGE);
|
||||
this.turn = 1;
|
||||
// PursuitSwitch reads the effect of wCurEnemyMove as GetMoveEffect
|
||||
// does, even for no move (0 reads past the table).
|
||||
const r = this.reader;
|
||||
if (r.byte(SYMBOLS.moves + ((this.enemy.curMove - 1) & 0xff) * 7 + 1) === C.EFFECT_PURSUIT)
|
||||
throw new UnsupportedBattle("Pursuit on a switching Pokémon is not ported yet.");
|
||||
yield* this.pursuitSwitch();
|
||||
this.curPartyMon = this.curBattleMon;
|
||||
yield* this.sendOutPlayerMon();
|
||||
}
|
||||
/** PursuitSwitch: an opponent that chose Pursuit uses it on the Pokémon
|
||||
* being withdrawn, then cannot move this turn. A withdrawn Pokémon that
|
||||
* faints skips the usual faint handling and keeps its status (a kept
|
||||
* bug). Returns true if it fainted. */
|
||||
private *pursuitSwitch(): Gen<boolean> {
|
||||
const user = this.user,
|
||||
turn = this.turn;
|
||||
// GetMoveEffect, which reads past the table for no move.
|
||||
if (this.reader.byte(SYMBOLS.moves + ((user.curMove - 1) & 0xff) * 7 + 1) !== C.EFFECT_PURSUIT)
|
||||
return false;
|
||||
const current = this.curBattleMon;
|
||||
if (turn === 1) this.curBattleMon = this.lastPlayerMon;
|
||||
// DoPlayerTurn does nothing after an item or a switch.
|
||||
if (turn === 1 || !this.battlePlayerAction) yield* this.doTurn();
|
||||
this.turn = turn;
|
||||
user.curMove = 0xff; // CANNOT_MOVE
|
||||
this.curBattleMon = current;
|
||||
if (turn === 0) {
|
||||
if (this.enemy.mon.hp) return false;
|
||||
yield* this.say(BATTLE.BattleText_EnemyMonFainted);
|
||||
return true;
|
||||
}
|
||||
// UpdateBattleMon: the withdrawn Pokémon's level, status and HP.
|
||||
const outgoing = this.options.party[this.lastPlayerMon];
|
||||
outgoing.level = this.player.mon.level;
|
||||
outgoing.status = this.player.mon.status;
|
||||
outgoing.hp = this.player.mon.hp;
|
||||
if (this.player.mon.hp) return false;
|
||||
this.participants &= ~(1 << this.lastPlayerMon);
|
||||
yield* this.say(BATTLE.BattleText_MonFainted);
|
||||
return true;
|
||||
}
|
||||
/** PlayerPartyMonEntrance: send out wCurPartyMon. */
|
||||
private *playerPartyMonEntrance(): Gen {
|
||||
this.lastPlayerMon = this.curBattleMon;
|
||||
@@ -3897,17 +4200,16 @@ export class Battle {
|
||||
/** AI_Switch: withdraw the enemy Pokémon and send out the chosen one. */
|
||||
private *aiSwitch(): Gen {
|
||||
const trainer = this.options.trainer!;
|
||||
this.enemyIsSwitching = 1;
|
||||
this.enemyGoesFirst = 1;
|
||||
this.enemy.reset(SUB4, C.SUBSTATUS_RAGE);
|
||||
this.turn = 0;
|
||||
// PursuitSwitch reads wCurPlayerMove's effect.
|
||||
if (this.player.curMove && this.options.data.move(this.player.curMove).effect === C.EFFECT_PURSUIT)
|
||||
throw new UnsupportedBattle("Pursuit on a switching Pokémon is not ported yet.");
|
||||
const fainted = yield* this.pursuitSwitch();
|
||||
// Status and HP go back to the party (MON_STATUS through MON_HP).
|
||||
const outgoing = trainer.party[this.curOTMon];
|
||||
outgoing.status = this.enemy.mon.status;
|
||||
outgoing.hp = this.enemy.mon.hp;
|
||||
yield* this.say(BATTLE.EnemyWithdrewText);
|
||||
if (!fainted) yield* this.say(BATTLE.EnemyWithdrewText);
|
||||
this.battleHasJustStarted = 1;
|
||||
this.newEnemyMonStatus();
|
||||
this.enemy.levels = [7, 7, 7, 7, 7, 7, 7];
|
||||
|
||||
Reference in new issue
Block a user