Open the Battle Tower and add the badges' type boosts (Closes #32)
Checks / web (push) Successful in 1m45s
Private ROM checks / private-rom (push) Failing after 11m30s

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:
kawaandClaude Opus 5.5 committed 2026-10-13 15:28:07 +02:00
1 parent 3fbc6798eb
commit fe2b0262c5
13 files changed
+1267 -14

No files matched your search

+60 -6
View File
@@ -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;