From 5cbada493ac4f9fcfc3939864847715b7e6b1c62 Mon Sep 17 00:00:00 2001 From: johnjohto Date: Thu, 30 Jul 2026 10:33:54 -0400 Subject: [PATCH] Add Bill's House Pikachu scenes --- data/scripts/story.lua | 4 ++ src/world/OverworldController.lua | 1 + src/world/PikachuFollower.lua | 66 ++++++++++++++++++++ tests/parity_yellow_bills_pikachu.lua | 89 +++++++++++++++++++++++++++ tests/run_tests.lua | 1 + 5 files changed, 161 insertions(+) create mode 100644 tests/parity_yellow_bills_pikachu.lua diff --git a/data/scripts/story.lua b/data/scripts/story.lua index 356a9907..d9ba11fa 100644 --- a/data/scripts/story.lua +++ b/data/scripts/story.lua @@ -183,6 +183,7 @@ M.BILLS_HOUSE = { overworld = ow }, "BILLS_HOUSE", "BILLSHOUSE_BILL_POKEMON") game.save.flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = true + require("src.world.PikachuFollower").onBillEnteredMachine(game, ow) done() end if ow.player.facing == "down" then @@ -273,6 +274,9 @@ M.BILLS_HOUSE = { Commands.hide_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL1") Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL2") end + if not flags.EVENT_MET_BILL_2 then + require("src.world.PikachuFollower").onBillsHouseEnter(game, ow) + end end, } diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 2d5db70f..0ea2ac3e 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -2093,6 +2093,7 @@ function OverworldState:billsHouseBillExits() local Commands = require("src.script.Commands") local ctx = { game = Game, save = Game.save, overworld = self } Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL1") + require("src.world.PikachuFollower").onBillExitedMachine(Game, self) local function done() Game.save.flags.EVENT_MET_BILL = true Game.save.flags.EVENT_MET_BILL_2 = true diff --git a/src/world/PikachuFollower.lua b/src/world/PikachuFollower.lua index 93f44841..636751dc 100644 --- a/src/world/PikachuFollower.lua +++ b/src/world/PikachuFollower.lua @@ -189,6 +189,9 @@ function PikachuFollower.current(ow) end function PikachuFollower.onMapEntered(game, ow, opts) + -- Bill's House owns a short scripted scene that deliberately keeps + -- Pikachu off the normal trailing loop. A new map instance ends it. + ow.pikachuBillsScene = nil remove(ow) if not shouldSpawn(game, ow) then return end -- opts.keepPikachu is the follower a connection crossing kept alive: @@ -393,6 +396,7 @@ end -- (pikachu_follow.asm keeps it one walk step behind) function PikachuFollower.update(game, ow) if ow.pikaHop then return end -- the counter hop owns the follower (#417) + if ow.pikachuBillsScene then return end local npc = findFollower(ow) if not npc then if shouldSpawn(game, ow) then PikachuFollower.onMapEntered(game, ow) end @@ -741,6 +745,68 @@ function PikachuFollower.picLift(emote) return 0 end +-- Bill's House has three map-scripted Yellow companion beats +-- (BillsHouseScript0/2/5): Pikachu walks over to investigate Bill, waits at +-- the cell separator, then reacts when Bill reappears. Keep it at the +-- machine until this map instance is discarded, just like the cartridge's +-- disabled following state. +local function billsHouseEmotion(game, ow, npc, bubble) + local Sprites = require("src.pokemon.Sprites") + ow.emote = { + npc = npc, frames = 50, bubble = bubbleIndex(game, bubble) or false, + pikaPic = Sprites.path(game.data, "PIKACHU", "front", + { kind = "overworld" }), + } +end + +local function movePikachu(ow, npc, steps, onDone) + npc.goalX, npc.goalY = nil, nil + idleReset(npc) + local function nextStep(i) + local step = steps[i] + if not step then + if onDone then onDone() end + return + end + ow:scriptMove(npc, step[1], step[2], function() nextStep(i + 1) end) + end + nextStep(1) +end + +function PikachuFollower.onBillsHouseEnter(game, ow) + if not (GameVersion.isYellow() and ow.map and ow.map.id == "BILLS_HOUSE") then + return + end + if game.save.flags.EVENT_MET_BILL_2 then return end + local npc = findFollower(ow) + if not npc then return end + ow.pikachuBillsScene = true + movePikachu(ow, npc, { { "right", 3 }, { "up", 1 } }, function() + billsHouseEmotion(game, ow, npc, "QUESTION_BUBBLE") + end) +end + +function PikachuFollower.onBillEnteredMachine(game, ow) + if not (GameVersion.isYellow() and ow.pikachuBillsScene) then return end + local npc = findFollower(ow) + if not npc then return end + local steps = ow.player.facing == "down" + and { { "up", 3 } } + or { { "up", 1 }, { "left", 1 }, { "up", 2 }, { "right", 1 } } + movePikachu(ow, npc, steps, function() + billsHouseEmotion(game, ow, npc, "QUESTION_BUBBLE") + end) +end + +function PikachuFollower.onBillExitedMachine(game, ow) + if not (GameVersion.isYellow() and ow.pikachuBillsScene) then return end + local npc = findFollower(ow) + if not npc then return end + idleReset(npc) + npc.facing = "left" + billsHouseEmotion(game, ow, npc, "EXCLAMATION_BUBBLE") +end + -- --------------------------------------------------------------------- -- PikachuWalksToNurseJoy (engine/pikachu/pikachu_emotions.asm, run by -- engine/events/pokecenter.asm once the heal is accepted): the companion diff --git a/tests/parity_yellow_bills_pikachu.lua b/tests/parity_yellow_bills_pikachu.lua new file mode 100644 index 00000000..dc206c9d --- /dev/null +++ b/tests/parity_yellow_bills_pikachu.lua @@ -0,0 +1,89 @@ +-- Yellow starts Bill's House with Pikachu's confused reaction. The map +-- script owns that one-shot, while PikachuFollower owns the movement. + +package.path = "./?.lua;./?/init.lua;" .. package.path +local S = require("tests.harness").suite("parity Yellow Bill's Pikachu") +local check = S.check + +local entered = 0 +local originalFollower = package.loaded["src.world.PikachuFollower"] +package.loaded["src.world.PikachuFollower"] = { + onBillsHouseEnter = function() + entered = entered + 1 + end, +} + +local story = dofile("data/scripts/story.lua") +local game = { save = { flags = {} } } +story.BILLS_HOUSE.onEnter(game, {}) +check(entered == 1, + "entering Bill's House before meeting Bill starts Pikachu's reaction") + +entered = 0 +game.save.flags.EVENT_MET_BILL_2 = true +story.BILLS_HOUSE.onEnter(game, {}) +check(entered == 0, + "Pikachu's Bill reaction does not replay after Bill is met") + +package.loaded["src.world.PikachuFollower"] = originalFollower + +local GameVersion = require("src.core.GameVersion") +local PikachuFollower = require("src.world.PikachuFollower") +GameVersion.set("yellow") + +local npc = { + pikachuFollower = true, cellX = 3, cellY = 8, px = 48, py = 128, + facing = "up", +} +local moves = {} +local yellowGame = { + save = { flags = {} }, + data = { + pokemon = { PIKACHU = { spriteFront = "pikachu.png" } }, + field = { emotionBubbles = { + bubbles = { + { name = "QUESTION_BUBBLE" }, { name = "EXCLAMATION_BUBBLE" }, + }, + } }, + }, +} +local ow = { + map = { id = "BILLS_HOUSE" }, npcs = { npc }, entities = { npc }, + player = { cellX = 3, cellY = 7 }, + scriptMove = function(_, entity, dir, tiles, onDone) + moves[#moves + 1] = { entity = entity, dir = dir, tiles = tiles, + onDone = onDone } + end, +} + +PikachuFollower.onBillsHouseEnter(yellowGame, ow) +check(ow.pikachuBillsScene and #moves == 1 + and moves[1].entity == npc and moves[1].dir == "right" + and moves[1].tiles == 3, + "Bill's House entry parks Pikachu and walks it to Bill") +moves[1].onDone() +check(#moves == 2 and moves[2].dir == "up" and moves[2].tiles == 1, + "Pikachu finishes its cartridge entry route beside Bill") +moves[2].onDone() +check(ow.emote and ow.emote.bubble == 1, + "Pikachu shows its confused reaction after reaching Bill") + +ow.player.facing = "down" +PikachuFollower.onBillEnteredMachine(yellowGame, ow) +check(#moves == 3 and moves[3].dir == "up" and moves[3].tiles == 3, + "Pikachu walks to the cell separator after Bill enters it") +moves[3].onDone() +check(ow.emote and ow.emote.bubble == 1, + "Pikachu wonders at the cell separator") + +npc.goalX, npc.goalY = 9, 9 +PikachuFollower.update(yellowGame, ow) +check(npc.goalX == 9 and npc.goalY == 9, + "Pikachu stays parked in Bill's House during the scene") + +PikachuFollower.onBillExitedMachine(yellowGame, ow) +check(ow.emote and ow.emote.bubble == 2 and npc.facing == "left", + "Pikachu reacts when Bill comes back out") + +GameVersion.set("red") +S.finish() diff --git a/tests/run_tests.lua b/tests/run_tests.lua index 0bf52a1c..1df4e706 100644 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -3278,6 +3278,7 @@ runSuites(orderedGlob("tests/parity_*.lua", { "tests/parity_static.lua", "tests/parity_trashcans.lua", "tests/parity_hof.lua", "tests/parity_trade_gift.lua", "tests/parity_yellow_trades.lua", + "tests/parity_yellow_bills_pikachu.lua", "tests/parity_intro.lua", "tests/parity_tilt.lua", "tests/parity_gbcfx.lua", }))