mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
366 lines
14 KiB
Lua
366 lines
14 KiB
Lua
-- 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",
|
|
-- pokeyellow engine/pikachu/pikachu_follow.asm:26
|
|
passable = true,
|
|
}
|
|
local Sound = require("src.core.Sound")
|
|
local realPikaCry = Sound.playPikaCry
|
|
local cry
|
|
Sound.playPikaCry = function(_, index) cry = index return true end
|
|
|
|
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 and ow.emote.frames == 60
|
|
and not ow.emote.pikaPic,
|
|
"Pikachu shows its confused reaction after reaching Bill")
|
|
-- emotion 23 -- engine/pikachu/pikachu_emotions.asm:14
|
|
check(npc.stepFrames == 16,
|
|
"the scripted walk runs at the Pikachu movement step length")
|
|
ow.emote.onDone()
|
|
check(ow.emote and ow.emote.pikaPic and ow.emote.frames > 0
|
|
and not ow.emote.skippable,
|
|
"and the bubble hands off to the unskippable emotion animation")
|
|
|
|
-- 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()
|
|
-- InitializePikachuTextID -- scripts/BillsHouse.asm:100
|
|
check(ow.emote and ow.emote.bubble == false and ow.emote.pikaPic
|
|
and npc.facing == "up",
|
|
"Pikachu looks up at the cell separator with no bubble, just the emotion")
|
|
|
|
-- 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 == false and ow.emote.frames == 12
|
|
and npc.facing ~= "left",
|
|
"SetSpriteFacingDirectionAndDelay + DELAY/LOOK_LEFT hold before the bubble")
|
|
ow.emote.onDone()
|
|
check(ow.emote and ow.emote.bubble == 2 and ow.emote.frames == 60
|
|
and npc.facing == "left",
|
|
"Pikachu reacts when Bill comes back out")
|
|
ow.emote.onDone()
|
|
check(ow.emote and ow.emote.pikaPic,
|
|
"and emotion 27 plays behind that bubble")
|
|
|
|
-- 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
|
|
yellowGame.save.pikachuMapScriptActive = 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
|
|
|
|
-- of the starter-status and mood fallbacks -- scripts/BillsHouse_2.asm:88
|
|
npc.facePlayer = function(self) self.facing = "up" end
|
|
ow.player.facing = "down"
|
|
|
|
ow.pikachuBillsScene = true
|
|
PikachuFollower.talk(yellowGame, ow, npc)
|
|
check(cry == 19, "talking during the confused beat picks emotion 23")
|
|
|
|
ow.pikachuBillsScene = nil
|
|
PikachuFollower.talk(yellowGame, ow, npc)
|
|
check(cry == 26, "before Bill is met again it is emotion 32")
|
|
|
|
yellowGame.save.flags.EVENT_MET_BILL_2 = true
|
|
PikachuFollower.talk(yellowGame, ow, npc)
|
|
check(cry == 19, "and emotion 31 once EVENT_MET_BILL_2 is set")
|
|
|
|
yellowGame.save.party = { { species = "PIKACHU", hp = 12, status = "SLP" } }
|
|
PikachuFollower.talk(yellowGame, ow, npc)
|
|
check(cry == 19, "the map check still wins over a statused starter")
|
|
yellowGame.save.party = nil
|
|
yellowGame.save.flags.EVENT_MET_BILL_2 = nil
|
|
Sound.playPikaCry = realPikaCry
|
|
|
|
-- CheckPikachuFollowingPlayer is one flag, and CollisionCheckOnLand blocks
|
|
-- the player outright while it is set (home/overworld.asm:1238-1240). All
|
|
-- three scenes that call DisablePikachuFollowingPlayer must park a solid
|
|
-- companion: scripts/BillsHouse_2.asm:121, PokemonFanClub.asm:60,
|
|
-- PewterPokecenter_2.asm:67.
|
|
for _, scene in ipairs({ "pikachuBillsScene", "pikachuFanClubScene",
|
|
"pikachuPewterSleepScene" }) do
|
|
-- update() drops the follower once shouldSpawn goes false, so re-seed it
|
|
ow.npcs, ow.entities = { npc }, { npc }
|
|
npc.passable = true
|
|
ow[scene] = true
|
|
PikachuFollower.update(yellowGame, ow)
|
|
check(not npc.passable, scene .. " parks a solid companion")
|
|
ow[scene] = nil
|
|
PikachuFollower.update(yellowGame, ow)
|
|
check(npc.passable, scene .. " ending lets the player walk through again")
|
|
end
|
|
|
|
-- The other two states of CollisionCheckOnLand's Pikachu branch
|
|
-- (home/overworld.asm:1234-1252): B held walks straight through, and
|
|
-- wPikachuCollisionCounter is a soft 8-count bump seeded on a direction
|
|
-- change (:189) and cleared with no d-pad held (:130) or once a step
|
|
-- commits (:242).
|
|
local held = {}
|
|
yellowGame.input = { isDown = function(_, b) return held[b] == true end }
|
|
local cp = ow.player
|
|
cp.cellX, cp.cellY, cp.facing, cp.moving = 3, 7, "up", false
|
|
npc.cellX, npc.cellY = 3, 6
|
|
|
|
local function tick()
|
|
ow.npcs, ow.entities = { npc }, { npc }
|
|
PikachuFollower.update(yellowGame, ow)
|
|
end
|
|
|
|
held = {}
|
|
tick()
|
|
check(npc.passable and ow.pikachuCollisionCounter == 0,
|
|
"no d-pad held clears the counter and the companion is walk-through")
|
|
|
|
held = { up = true }
|
|
tick()
|
|
check(ow.pikachuCollisionCounter == 8 and not npc.passable,
|
|
"turning to a new direction seeds the 8-count bump")
|
|
|
|
local blocked = 0
|
|
for _ = 1, 8 do
|
|
tick()
|
|
if not npc.passable then blocked = blocked + 1 end
|
|
end
|
|
check(blocked == 7,
|
|
"the bump blocks seven pushes and then yields, like dec [hl] / jr nz")
|
|
check(npc.passable and ow.pikachuCollisionCounter == 0,
|
|
"the drained counter leaves the companion passable")
|
|
|
|
held = {}
|
|
tick()
|
|
held = { up = true }
|
|
tick()
|
|
check(ow.pikachuCollisionCounter == 0,
|
|
"re-pressing the same direction after a release does not re-seed")
|
|
|
|
held = {}
|
|
tick()
|
|
held = { left = true }
|
|
tick()
|
|
check(ow.pikachuCollisionCounter == 8, "a new direction seeds again")
|
|
held = { left = true, b = true }
|
|
tick()
|
|
check(npc.passable, "holding B walks straight through the bump")
|
|
|
|
held = { left = true }
|
|
cp.moving = true
|
|
tick()
|
|
check(ow.pikachuCollisionCounter == 0 and npc.passable,
|
|
"a committed step clears the counter")
|
|
cp.moving = false
|
|
held = {}
|
|
tick()
|
|
yellowGame.input = nil
|
|
|
|
-- engine/pikachu/pikachu_follow.asm:227,119
|
|
check(PikachuFollower.warpSpawnState(true, "ROUTE_25", "BILLS_HOUSE", false,
|
|
"up") == 1,
|
|
"Bill's House door from Route 25 is spawn state 1")
|
|
|
|
local emergeMoves = {}
|
|
local emergeOw = {
|
|
map = {
|
|
id = "BILLS_HOUSE",
|
|
def = { tileset = "INTERIOR" },
|
|
inBounds = function() return true end,
|
|
isWalkableCell = function() return true end,
|
|
isWaterCell = function() return false end,
|
|
cellTile = function() return 0 end,
|
|
},
|
|
npcs = {}, entities = {},
|
|
player = { cellX = 2, cellY = 7, facing = "up" },
|
|
scriptMove = function(_, entity, dir, tiles, onDone)
|
|
emergeMoves[#emergeMoves + 1] = { dir = dir, tiles = tiles, onDone = onDone }
|
|
end,
|
|
}
|
|
local emergeNpc = {
|
|
pikachuFollower = true, cellX = 3, cellY = 7, px = 48, py = 112,
|
|
facing = "up", passable = true,
|
|
}
|
|
emergeOw.npcs[1], emergeOw.entities[1] = emergeNpc, emergeNpc
|
|
yellowGame.save.flags = {}
|
|
yellowGame.save.pikachuMapScriptActive = nil
|
|
PikachuFollower.onBillsHouseEnter(yellowGame, emergeOw)
|
|
check(#emergeMoves == 1 and emergeMoves[1].dir == "right"
|
|
and emergeMoves[1].tiles == 3,
|
|
"the confused walk runs PikachuMovement_Confused's three STEP_RIGHTs first")
|
|
emergeMoves[1].onDone()
|
|
check(#emergeMoves == 2 and emergeMoves[2].dir == "up"
|
|
and emergeMoves[2].tiles == 1,
|
|
"and ends one cell up, directly below Bill")
|
|
|
|
-- engine/overworld/player_animations.asm:37
|
|
PikachuFollower.placeAtSpawnState(emergeOw, 1)
|
|
check(emergeNpc.cellX == 3 and emergeNpc.cellY == 7
|
|
and emergeNpc.facing == "up",
|
|
"a warp-pad arrival puts the companion back east of the player")
|
|
PikachuFollower.placeAtSpawnState(emergeOw, 3)
|
|
check(emergeNpc.cellX == 2 and emergeNpc.cellY == 7
|
|
and emergeNpc.facing == "down",
|
|
"and state 3 is the player's own cell facing down")
|
|
|
|
GameVersion.set("red")
|
|
S.finish()
|