Open Mr. Pokémon's house, the first rival battle and the return to Elm
Checks / web (push) Canceled after 0s

Mr. Pokémon's ROM scene gives the Mystery Egg and Pokédex, heals the
party, sets the Cherrygrove spawn with blackoutmod and queues Elm's
call. Cherrygrove's rival scene runs both triggers and all three
RIVAL1_1 parties as a CANLOSE battle; reloadmap returns to the script.
Smart AI is accepted for effects with no AI_Smart_EffectHandlers entry
(Normal Hit, Attack Down, Defense Down) and still refuses others before
any random draw, including for reserve Pokémon.

Special calls read SpecialPhoneCallList: the entry's condition decides,
and the ROM's CheckSpecialPhoneCall.script and Script_ReceivePhoneCall
run its caller script through memcall, ending with HangUp's timed
"Click!". Ringing and the caller box are not presented yet.

In Elm's lab, NameRival shows the ROM's "RIVAL'S NAME?" title (the
prompt previously had no page and soft-locked) and applies InitName's
default to blank names. Handing over the Egg and the aide's Poké Balls
run from the ROM. Saves gain rivalName and specialPhoneCall.

The quest fixtures use a seeded RNG (a constant byte never ends the
damage-variation loop) and no longer carry the house's moved objects
into Cherrygrove.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-24 19:38:20 +02:00
1 parent 7c1c0cc45b
commit dc10343520
18 files changed
+812 -34

No files matched your search

+37 -3
View File
@@ -79,10 +79,44 @@ function fixture(moves = [move(1), move(2)], bytes = [0]): TrainerAIInput {
}
describe("trainer AI native scoring", () => {
it("rejects the untranslated Smart layer, including Rival1, before consuming randomness", () => {
for (const weights of [16, 0x3d3, -1, 65536])
it("accepts all defined layer bits and rejects invalid layer words", () => {
for (const weights of [-1, 65536, 1.5, NaN])
expect(() => validateTrainerAI(weights)).toThrow(/not translated/);
expect(() => validateTrainerAI(0xffef)).not.toThrow();
for (const weights of [16, 0x3d3, 0xffff])
expect(() => validateTrainerAI(weights)).not.toThrow();
});
it.each([C.EFFECT_ATTACK_DOWN, C.EFFECT_DEFENSE_DOWN])(
"Smart leaves the first rival's attack and stat-lowering effect %i unchanged",
(effect) => {
const input = fixture([move(1), move(2, effect, 0)], [3, 1]);
input.weights = 16;
expect(chooseTrainerMove(input)).toEqual({
move: 2,
index: 1,
scores: [20, 20, 80, 80],
});
},
);
it("refuses untranslated Smart effects before any earlier layer consumes randomness", () => {
const input = fixture(
[move(1, C.EFFECT_ATTACK_DOWN, 0), move(2, C.EFFECT_REFLECT, 0)],
[],
);
input.weights = 0x3d3;
input.enemy.mon.pp[1] = 0; // Exhaustion does not bypass scoring or validation.
expect(() => chooseTrainerMove(input)).toThrow(/Smart AI for move effect/);
});
it("validates Smart effects across a supplied full trainer party", () => {
expect(() =>
validateTrainerAI(0x3d3, [
C.EFFECT_NORMAL_HIT,
C.EFFECT_ATTACK_DOWN,
C.EFFECT_DEFENSE_DOWN,
]),
).not.toThrow();
expect(() =>
validateTrainerAI(0x3d3, [C.EFFECT_NORMAL_HIT, C.EFFECT_SLEEP]),
).toThrow(/Smart AI/);
});
it("uses masked random slots and retries blanks without first-slot tie bias", () => {
const input = fixture(undefined, [3, 6, 253]);