Open the Battle Tower and add the badges' type boosts (Closes #32)
The Route 40 gate, the Battle Tower and its rooms are open with the offline challenge: the rules, the save before entering, the level menu with its level and uber checks, seven trainers drawn from BattleTowerTrainers with Pokémon from BattleTowerMons, BattleTowerText, save and quit with the receptionist resuming it, cancelling, losing and the vitamin reward. After each battle the party and Pokédex return to the quick save's, as LoadPokemonData does. Battle Tower battles turn off obedience, badge boosts, experience, items, AI items and prize money, use Falkner's AI attributes, keep Sleep to one to three turns and never fail the AI's status moves at random. New script command battletowertext; new specials BattleTowerAction (offline actions), BattleTowerRoomMenu, CheckForBattleTowerRules, BattleTowerBattle, LoadOpponentTrainerAndPokemonWithOTSprite, Menu_ChallengeExplanationCancel, TryQuickSave and Reset. DoBadgeTypeBoosts was missing from every battle; it now adds an eighth of the damage for the first badge matching the move's type on the player's turn, checked against the original routine. This completes Johto from Azalea to Blackthorn: Kanto, the League and the S.S. Aqua continue on #33; radio, rematches and Buena's password on #30. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3fbc6798eb
commit
fe2b0262c5
13 files changed
+1267
-14
No files matched your search
@@ -115,7 +115,9 @@ export type BattleEffect =
|
||||
| { kind: "hud" }
|
||||
/** The battle menu: answered with an action. In the Bug-Catching
|
||||
* Contest, PACK is PARKBALL× with this wParkBallsRemaining. */
|
||||
| { kind: "action"; parkBalls?: number }
|
||||
/** BattleMenu. `noItems`: PACK only says items can't be used (link and
|
||||
* Battle Tower battles), without opening the pack. */
|
||||
| { kind: "action"; parkBalls?: number; noItems?: boolean }
|
||||
/** DisplayCaughtContestMonStats: the stocked contest Pokémon beside the
|
||||
* one just caught, with ContestAskSwitchText; answered Yes/No (1/0). */
|
||||
| { kind: "contestswitch"; text: string; stock: ContestMonSummary; caught: ContestMonSummary }
|
||||
@@ -219,8 +221,14 @@ export interface BattleOptions {
|
||||
pokedex: { seen: Set<number>; caught: Set<number> };
|
||||
playerId: number;
|
||||
playerName: string;
|
||||
/** wJohtoBadges, for traded Pokémon obedience. */
|
||||
/** wJohtoBadges, for traded Pokémon obedience and the badges' boosts. */
|
||||
johtoBadges?: number;
|
||||
/** wKantoBadges, for DoBadgeTypeBoosts. */
|
||||
kantoBadges?: number;
|
||||
/** wInBattleTowerBattle: a Battle Tower battle (no obedience, badge
|
||||
* boosts, experience, items, prize money or whiteout; the AI's status
|
||||
* moves never fail at random; Sleep lasts one to three turns). */
|
||||
battleTower?: boolean;
|
||||
/** wTimeOfDay: morning 0, day 1, night 2. */
|
||||
timeOfDay: number;
|
||||
random: () => number;
|
||||
@@ -964,6 +972,7 @@ export class Battle {
|
||||
* Special Defense boost tests the register that BoostStat leaves after
|
||||
* Special Attack, so it depends on that stat (a kept bug). */
|
||||
private badgeStatBoosts() {
|
||||
if (this.options.battleTower) return;
|
||||
const badges = this.options.johtoBadges ?? 0;
|
||||
// ZEPHYR, HIVE, FOG, STORM, GLACIER and RISING stay; PLAIN (bit 2)
|
||||
// moves to bit 4 and MINERAL (bit 4) to bit 2.
|
||||
@@ -1128,6 +1137,15 @@ export class Battle {
|
||||
yield this.hud();
|
||||
this.turn = 0; // SetPlayerTurn before SpikesDamage
|
||||
yield* this.battleTurn();
|
||||
if (trainer && this.options.battleTower) {
|
||||
// WinTrainerBattle and LostBattle's .battle_tower: BattleTowerText
|
||||
// instead of the prize money or the whiteout.
|
||||
const won = (this.battleResult & 0x3f) === C.WIN;
|
||||
if (won) yield* this.say(BATTLE.BattleText_EnemyWasDefeated);
|
||||
const text = won ? trainer.winText : trainer.lossText;
|
||||
if (text) yield* this.say(text);
|
||||
return { result: this.battleResult, evolvable: this.evolvable };
|
||||
}
|
||||
if (trainer && (this.battleResult & 0x3f) === C.WIN) {
|
||||
yield* this.say(BATTLE.BattleText_EnemyWasDefeated);
|
||||
if (this.options.canLose) {
|
||||
@@ -1318,8 +1336,17 @@ export class Battle {
|
||||
private *battleMenu(): Gen<boolean> {
|
||||
for (;;) {
|
||||
const contest = this.battleType === C.BATTLETYPE_CONTEST ? this.options.contest! : undefined;
|
||||
const input = yield contest ? { kind: "action", parkBalls: contest.parkBalls } : { kind: "action" };
|
||||
const input = yield contest
|
||||
? { kind: "action", parkBalls: contest.parkBalls }
|
||||
: this.options.battleTower
|
||||
? { kind: "action", noItems: true }
|
||||
: { kind: "action" };
|
||||
if (!input || typeof input !== "object") continue;
|
||||
if (input.action === "item" && this.options.battleTower) {
|
||||
// BattleMenu_Pack.ItemsCantBeUsed.
|
||||
yield* this.say(BATTLE.BattleText_ItemsCantBeUsedHere);
|
||||
continue;
|
||||
}
|
||||
if (input.action === "item") {
|
||||
// BattleMenu_Pack's .contest: PARKBALL× throws a Park Ball.
|
||||
const used = contest
|
||||
@@ -2903,6 +2930,7 @@ export class Battle {
|
||||
/** BattleCommand_CheckObedience: only foreign Pokémon may disobey. */
|
||||
private *checkObedience(): Gen {
|
||||
if (this.turn !== 0 || this.user.charging || this.alreadyDisobeyed) return;
|
||||
if (this.options.battleTower) return;
|
||||
if (this.partyMon.otId === this.options.playerId) return;
|
||||
const user = this.user, badges = this.options.johtoBadges ?? 0;
|
||||
const limit = badges & 0x80 ? 101 : badges & 0x20 ? 70 : badges & 8 ? 50 : badges & 2 ? 30 : 10;
|
||||
@@ -3157,7 +3185,10 @@ export class Battle {
|
||||
target = this.target;
|
||||
if (user.move.anim === C.STRUGGLE) return;
|
||||
const result = applyTypeMatchups(this.matchups, {
|
||||
damage: this.weatherModifier(this.curDamage, user.move.type, user.move.effect),
|
||||
damage: this.badgeTypeBoost(
|
||||
this.weatherModifier(this.curDamage, user.move.type, user.move.effect),
|
||||
user.move.type,
|
||||
),
|
||||
modifier: this.typeModifier,
|
||||
moveType: user.move.type,
|
||||
userTypes: user.mon.types,
|
||||
@@ -3183,6 +3214,24 @@ export class Battle {
|
||||
const value = Math.floor((damage * multiplier) / 10);
|
||||
return value > 0xffff ? 0xffff : value || 1;
|
||||
}
|
||||
/** DoBadgeTypeBoosts, after the weather: on the player's turn, the first
|
||||
* badge (Johto, then Kanto) whose BadgeTypeBoosts type is the move's adds
|
||||
* an eighth of the damage (at least 1, to at most $ffff). */
|
||||
private badgeTypeBoost(damage: number, type: number) {
|
||||
if (this.options.battleTower || this.turn !== 0) return damage;
|
||||
const r = this.reader;
|
||||
let johto = this.options.johtoBadges ?? 0,
|
||||
kanto = this.options.kantoBadges ?? 0;
|
||||
for (let p = BATTLE.BadgeTypeBoosts; r.byte(p) !== 0xff; p++) {
|
||||
// srl b / rr c: the Johto byte's low bit, then Kanto's shifted in.
|
||||
const carry = johto & 1;
|
||||
johto = (johto >> 1) | ((kanto & 1) << 7);
|
||||
kanto >>= 1;
|
||||
if (carry && r.byte(p) === type)
|
||||
return Math.min(0xffff, damage + ((damage >> 3) || 1));
|
||||
}
|
||||
return damage;
|
||||
}
|
||||
/** BattleCheckTypeMatchup: wTypeMatchup for the move against the target. */
|
||||
private battleCheckTypeMatchup() {
|
||||
const target = this.target;
|
||||
@@ -4817,7 +4866,8 @@ export class Battle {
|
||||
}
|
||||
/** The AI's 25% failure for status moves (not for the player). */
|
||||
private aiRandomFail() {
|
||||
if (this.turn === 0 || this.player.has(SUB5, C.SUBSTATUS_LOCK_ON)) return false;
|
||||
if (this.turn === 0 || this.options.battleTower) return false;
|
||||
if (this.player.has(SUB5, C.SUBSTATUS_LOCK_ON)) return false;
|
||||
return this.random() < percent(25) + 1;
|
||||
}
|
||||
private *didntAffect2(): Gen {
|
||||
@@ -4834,8 +4884,10 @@ export class Battle {
|
||||
if (this.attackMissed) return yield* this.didntAffect2();
|
||||
if (this.aiRandomFail() || target.mon.status || target.has(SUB4, C.SUBSTATUS_SUBSTITUTE))
|
||||
return yield* this.say(BATTLE.DidntAffect1Text);
|
||||
// The Battle Tower keeps the sleep counter to %011.
|
||||
const mask = this.options.battleTower ? 0b011 : C.SLP_MASK;
|
||||
let turns: number;
|
||||
do turns = this.random() & C.SLP_MASK;
|
||||
do turns = this.random() & mask;
|
||||
while (!turns || turns === C.SLP_MASK);
|
||||
target.mon.status = turns + 1;
|
||||
this.updateOpponentInParty();
|
||||
@@ -6264,6 +6316,8 @@ export class Battle {
|
||||
}
|
||||
}
|
||||
private *giveExperience(shared = false): Gen {
|
||||
// GiveExperiencePoints: none in the Battle Tower.
|
||||
if (this.options.battleTower) return;
|
||||
const party = this.options.party,
|
||||
data = this.options.data;
|
||||
const count = [0, 1, 2, 3, 4, 5].filter((index) => this.participants & (1 << index)).length;
|
||||
|
||||
Reference in new issue
Block a user