-- 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") -- BillsHouseScript3 seeds hl with ..._EnterCellSeparatorDown and only swaps -- in ..._EnterCellSeparatorNotDown on the facing-down fallthrough, so the -- table names are inverted relative to the branch that picks them: facing -- down takes the five-step detour, every other facing walks straight up -- (pokeyellow scripts/BillsHouse.asm:100-133) (#455). ow.player.facing = "down" PikachuFollower.onBillEnteredMachine(yellowGame, ow) check(#moves == 3 and moves[3].dir == "up" and moves[3].tiles == 1, "facing down, Pikachu starts the long way round the cell separator") moves[3].onDone() check(#moves == 4 and moves[4].dir == "left" and moves[4].tiles == 1, "the detour steps aside before climbing") moves[4].onDone() check(#moves == 5 and moves[5].dir == "up" and moves[5].tiles == 2, "the detour climbs past the separator") moves[5].onDone() check(#moves == 6 and moves[6].dir == "right" and moves[6].tiles == 1, "the detour steps back in beside it") moves[6].onDone() check(ow.emote and ow.emote.bubble == 1 and npc.facing == "up", "Pikachu looks up and wonders at the cell separator") -- the other branch: any non-down facing takes the straight three-step route local before = #moves ow.player.facing = "up" PikachuFollower.onBillEnteredMachine(yellowGame, ow) check(#moves == before + 1 and moves[before + 1].dir == "up" and moves[before + 1].tiles == 3, "facing up, Pikachu walks straight to the cell separator") moves[before + 1].onDone() 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") -- BillsHousePikachuWatchPlayer (scripts/BillsHouse_2.asm:133-156) is the -- other side of BillsHouseScript2: it only runs while Pikachu still follows -- the player, and TryApplyPikachuMovementData keys each table on -- GetPikachuFacingDirectionAndReturnToE, which is Pikachu's position -- relative to the player rather than its facing byte, so WatchPlayer1 wants -- Pikachu above the player and WatchPlayer2 wants it level and east (#455). local function placePikachu(cx, cy) npc.cellX, npc.cellY, npc.facing = cx, cy, "down" end ow.pikachuBillsScene = nil moves = {} placePikachu(ow.player.cellX, ow.player.cellY - 1) PikachuFollower.onBillWalksAroundPlayer(yellowGame, ow) check(#moves == 1 and moves[1].dir == "left" and moves[1].tiles == 1, "a Pikachu standing above the player steps aside for Bill's detour") moves[1].onDone() check(#moves == 2 and moves[2].dir == "down" and moves[2].tiles == 1, "the watch route drops clear of Bill's detour") moves[2].onDone() check(npc.facing == "right", "PIKAMOVEMENT_LOOK_RIGHT watches the player") moves = {} placePikachu(ow.player.cellX + 1, ow.player.cellY) PikachuFollower.onBillWalksAroundPlayer(yellowGame, ow) check(#moves == 1 and moves[1].dir == "up" and moves[1].tiles == 1, "level with and east of the player takes the longer WatchPlayer2 route") moves[1].onDone() check(#moves == 2 and moves[2].dir == "left" and moves[2].tiles == 2, "WatchPlayer2 crosses two cells west") moves[2].onDone() check(#moves == 3 and moves[3].dir == "down" and moves[3].tiles == 1, "WatchPlayer2 drops back level with the player") moves[3].onDone() check(npc.facing == "right", "WatchPlayer2 ends watching the player too") -- below the player is SPRITE_FACING_DOWN and west of it is -- SPRITE_FACING_LEFT, and neither call asks for those moves = {} placePikachu(ow.player.cellX, ow.player.cellY + 1) PikachuFollower.onBillWalksAroundPlayer(yellowGame, ow) check(#moves == 0, "a Pikachu trailing from the south has no watch route") placePikachu(ow.player.cellX - 1, ow.player.cellY) PikachuFollower.onBillWalksAroundPlayer(yellowGame, ow) check(#moves == 0, "nor does one already standing west of the player") -- ApplyPikachuMovementData_ finishes a table before the second -- TryApplyPikachuMovementData reads the geometry again, but WatchPlayer1 -- lands on (playerX - 1, playerY), so WatchPlayer2 can never chain onto it moves = {} placePikachu(ow.player.cellX, ow.player.cellY - 1) PikachuFollower.onBillWalksAroundPlayer(yellowGame, ow) moves[1].onDone() moves[2].onDone() placePikachu(ow.player.cellX - 1, ow.player.cellY) moves = {} PikachuFollower.onBillWalksAroundPlayer(yellowGame, ow) check(#moves == 0, "the two tables stay mutually exclusive by geometry") -- the parked Pikachu of the confused beat is the not-following side of -- CheckPikachuFollowingPlayer, so it never watches ow.pikachuBillsScene = true placePikachu(ow.player.cellX, ow.player.cellY - 1) PikachuFollower.onBillWalksAroundPlayer(yellowGame, ow) check(#moves == 0, "the parked Pikachu of the confused beat stays put") -- BillsHouseScript0 skips the whole entry beat for a statused starter -- (CheckPikachuStatusCondition, scripts/BillsHouse.asm:45-46) ow.pikachuBillsScene = nil moves = {} yellowGame.save.party = { { species = "PIKACHU", hp = 12, status = "PAR" } } PikachuFollower.onBillsHouseEnter(yellowGame, ow) check(not ow.pikachuBillsScene and #moves == 0, "a statused starter keeps following instead of walking over to Bill") yellowGame.save.party = nil GameVersion.set("red") S.finish()