mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-29 15:08:50 -04:00
yellow: Bill's house Pikachu timing, collision and spawn cell, CLOSES #2165
This commit is contained in:
+18
-1
@@ -620,6 +620,23 @@ function Commands.play_sound(ctx, soundId)
|
||||
require("src.core.Sound").play(ctx.game.data, soundId)
|
||||
end
|
||||
|
||||
-- wait_sound: WaitForSoundToFinish (home/delay.asm:15), the drain that
|
||||
-- follows every PlaySound in the cell-separator chain
|
||||
-- (engine/events/hidden_events/bills_house_pc.asm:22)
|
||||
local WAIT_SOUND_CEILING = 600
|
||||
|
||||
function Commands.wait_sound(ctx)
|
||||
local Sound = require("src.core.Sound")
|
||||
if not Sound.sfxBusy() then return end
|
||||
local runner = ctx.runner
|
||||
local left = WAIT_SOUND_CEILING
|
||||
runner.waitingCheck = function()
|
||||
left = left - 1
|
||||
return left <= 0 or not Sound.sfxBusy()
|
||||
end
|
||||
runner:yield()
|
||||
end
|
||||
|
||||
-- text_sound <soundId>: the jingle the ROM parks at the END of a string as
|
||||
-- a trailing text command (sound_get_item_1, sound_get_key_item ->
|
||||
-- home/text.asm TextCommand_SOUND). It arms the NEXT show_text the same
|
||||
@@ -1476,7 +1493,7 @@ end
|
||||
for _, verb in ipairs({ "show_text", "ask", "choice", "start_battle", "warp",
|
||||
"open_mart", "trade", "push_screen", "record_hall_of_fame",
|
||||
"old_man_demo", "static_battle", "rival_battle", "give_item",
|
||||
"give_pokemon", "wait",
|
||||
"give_pokemon", "wait", "wait_sound",
|
||||
"wait_flag", "move_player", "move_npc", "move_npc_to", "walk_npc",
|
||||
"emote", "fade", "pan_camera", "play_once", "pikachu_make_way",
|
||||
"ss_anne_departs" }) do
|
||||
|
||||
@@ -420,15 +420,69 @@ function PikachuFollower.rebase(ow, dx, dy)
|
||||
if trail then trail.x, trail.y = trail.x + dx, trail.y + dy end
|
||||
end
|
||||
|
||||
local DIRS = { "up", "down", "left", "right" }
|
||||
|
||||
-- wPikachuCollisionCounter: 8 on a direction change (home/overworld.asm:189),
|
||||
-- cleared with no d-pad held (:130) and once a step commits (:242)
|
||||
local function tickCollisionCounter(game, ow, npc)
|
||||
local input = game.input
|
||||
local p = ow.player
|
||||
local dir
|
||||
if input then
|
||||
for _, d in ipairs(DIRS) do
|
||||
if input:isDown(d) then dir = d break end
|
||||
end
|
||||
end
|
||||
if not dir then
|
||||
ow.pikachuCollisionCounter = 0
|
||||
if ow.pikachuMovingDir then
|
||||
ow.pikachuLastStopDir = ow.pikachuMovingDir
|
||||
ow.pikachuMovingDir = nil
|
||||
end
|
||||
ow.pikachuTurnArmed = true
|
||||
return
|
||||
end
|
||||
if ow.pikachuTurnArmed and dir ~= ow.pikachuLastStopDir then
|
||||
ow.pikachuCollisionCounter = 8
|
||||
ow.pikachuTurnArmed = false
|
||||
ow.pikachuMovingDir = dir
|
||||
return
|
||||
end
|
||||
ow.pikachuMovingDir = dir
|
||||
if p.moving then
|
||||
ow.pikachuCollisionCounter = 0
|
||||
return
|
||||
end
|
||||
local n = ow.pikachuCollisionCounter or 0
|
||||
if n <= 0 then return end
|
||||
local tx, ty = Collision.target(p.cellX, p.cellY, dir)
|
||||
if p.facing == dir and npc.cellX == tx and npc.cellY == ty then
|
||||
ow.pikachuCollisionCounter = n - 1
|
||||
end
|
||||
end
|
||||
|
||||
-- CollisionCheckOnLand's Pikachu branch -- home/overworld.asm:1234-1252
|
||||
local function updatePassable(game, ow, npc)
|
||||
if PikachuFollower.isFollowingDisabled(ow) then
|
||||
npc.passable = false
|
||||
ow.pikachuCollisionCounter = 0
|
||||
return
|
||||
end
|
||||
tickCollisionCounter(game, ow, npc)
|
||||
if game.input and game.input:isDown("b") then
|
||||
npc.passable = true
|
||||
return
|
||||
end
|
||||
npc.passable = (ow.pikachuCollisionCounter or 0) <= 0
|
||||
end
|
||||
|
||||
-- one follow step per frame: chase the cell the player last vacated
|
||||
-- (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)
|
||||
local npc = findFollower(ow)
|
||||
-- home/overworld.asm:1238-1240
|
||||
if npc then
|
||||
npc.passable = not PikachuFollower.isFollowingDisabled(ow)
|
||||
end
|
||||
if npc then updatePassable(game, ow, npc) end
|
||||
if PikachuFollower.isFollowingDisabled(ow) then return end
|
||||
if not npc then
|
||||
if shouldSpawn(game, ow) then PikachuFollower.onMapEntered(game, ow) end
|
||||
@@ -895,7 +949,13 @@ function PikachuFollower.onBillsHouseEnter(game, ow)
|
||||
local npc = findFollower(ow)
|
||||
if not npc then return end
|
||||
ow.pikachuBillsScene = true
|
||||
movePikachu(ow, npc, { { "right", 3 }, { "up", 1 } }, function()
|
||||
local steps = { { "right", 3 }, { "up", 1 } }
|
||||
-- engine/pikachu/pikachu_follow.asm:59
|
||||
if npc.cellX == ow.player.cellX and npc.cellY == ow.player.cellY
|
||||
and Collision.canMove(ow.map, ow.entities, npc, "right") then
|
||||
table.insert(steps, 1, { "right", 1 })
|
||||
end
|
||||
movePikachu(ow, npc, steps, function()
|
||||
-- BillsHouse_CheckPikachuEmotion SCRIPT0 -- scripts/BillsHouse_2.asm:88
|
||||
billsHouseEmotion(game, ow, npc, "QUESTION_BUBBLE", 23)
|
||||
end)
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
-- #2165 refix 2: home/overworld.asm:1234-1252 (the B bypass and the
|
||||
-- wPikachuCollisionCounter soft bump), engine/pikachu/pikachu_follow.asm:59
|
||||
-- (the companion never starts the confused walk on the player's cell) and
|
||||
-- engine/events/hidden_events/bills_house_pc.asm:14-40 (268 delay frames plus
|
||||
-- five WaitForSoundToFinish drains). Self-terminating: it quits LOVE with 0
|
||||
-- on all-PASS and 1 otherwise. No POKEPORT_SPEED -- it scales the logic
|
||||
-- clock only and desyncs the cries against the walk.
|
||||
--
|
||||
-- SHOT_DIR=/tmp/shots2165b POKEPORT_SHOT_DIR=/tmp/shots2165b \
|
||||
-- POKEPORT_DRIVER=tests/drivers/bills_pikachu_refix2_2165.lua \
|
||||
-- POKEPORT_IDENTITY=yellow-sep04 POKEPORT_VERSION=yellow POKEPORT_TOUCH=0 love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
|
||||
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots2165b"
|
||||
local failures = 0
|
||||
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
|
||||
local function finish()
|
||||
U.log(failures == 0 and "DONE all checks passed"
|
||||
or ("DONE " .. failures .. " check(s) failed"))
|
||||
U.log("shots in", SHOT_DIR)
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
|
||||
if not check("running the Yellow cache (POKEPORT_VERSION=yellow)",
|
||||
GameVersion.isYellow()) then
|
||||
finish()
|
||||
end
|
||||
|
||||
local MAP = "BILLS_HOUSE"
|
||||
|
||||
local function resetSave()
|
||||
game.save.party = { Pokemon.new(game.data, "PIKACHU", 12) }
|
||||
game.save.player.name = "bryan"
|
||||
game.save.options = game.save.options or {}
|
||||
game.save.options.textSpeed = 1
|
||||
game.save.flags = game.save.flags or {}
|
||||
game.save.flags.EVENT_GOT_STARTER = true
|
||||
game.save.flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
|
||||
game.save.flags.EVENT_MET_BILL = nil
|
||||
game.save.flags.EVENT_MET_BILL_2 = nil
|
||||
game.save.flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = nil
|
||||
game.save.flags.EVENT_USED_CELL_SEPARATOR_ON_BILL = nil
|
||||
game.save.pikachuMapScriptActive = nil
|
||||
end
|
||||
|
||||
local ow
|
||||
local function follower()
|
||||
for _, n in ipairs(ow and ow.npcs or {}) do
|
||||
if n.pikachuFollower then return n end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function objectAt(name)
|
||||
for _, n in ipairs(ow and ow.npcs or {}) do
|
||||
if n.def and n.def.name == name then return n end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function press(dir, frames)
|
||||
for _ = 1, frames do
|
||||
table.insert(game.input.pressQueue, dir)
|
||||
game.input.state[dir] = true
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
|
||||
local function release(...)
|
||||
for _, b in ipairs({ ... }) do game.input.state[b] = false end
|
||||
U.wait(1)
|
||||
end
|
||||
|
||||
-- walk the player exactly one cell: hold until the cell index changes,
|
||||
-- release, then let the in-flight step land
|
||||
local function step(dir)
|
||||
local p0 = ow.player
|
||||
local sx, sy = p0.cellX, p0.cellY
|
||||
for _ = 1, 60 do
|
||||
table.insert(game.input.pressQueue, dir)
|
||||
game.input.state[dir] = true
|
||||
coroutine.yield()
|
||||
if p0.cellX ~= sx or p0.cellY ~= sy then break end
|
||||
end
|
||||
game.input.state[dir] = false
|
||||
for _ = 1, 40 do
|
||||
if not p0.moving then break end
|
||||
coroutine.yield()
|
||||
end
|
||||
U.wait(2)
|
||||
end
|
||||
|
||||
-- =====================================================================
|
||||
-- A. the soft bump and the B bypass, both of which only exist while
|
||||
-- following is ENABLED -- home/overworld.asm:1240-1252
|
||||
-- =====================================================================
|
||||
resetSave()
|
||||
game.save.flags.EVENT_MET_BILL_2 = true -- BillsHouseScript0 stays asleep
|
||||
U.teleport(game, MAP, 3, 6, "up")
|
||||
U.wait(30)
|
||||
ow = game.overworld
|
||||
if not check("Bill's House loaded", ow and ow.map and ow.map.id == MAP) then
|
||||
finish()
|
||||
end
|
||||
check("the confused beat did not run with EVENT_MET_BILL_2 set",
|
||||
ow.pikachuBillsScene ~= true)
|
||||
|
||||
-- take a step so the companion trails onto the cell behind the player
|
||||
step("up")
|
||||
U.wait(20)
|
||||
local npc = follower()
|
||||
if not check("the follower spawned and trailed", npc ~= nil) then finish() end
|
||||
check("a following companion is walk-through when nothing is pressed",
|
||||
npc.passable == true and (ow.pikachuCollisionCounter or 0) == 0)
|
||||
|
||||
local p = ow.player
|
||||
local backDir = (npc.cellY > p.cellY and "down")
|
||||
or (npc.cellY < p.cellY and "up")
|
||||
or (npc.cellX > p.cellX and "right") or "left"
|
||||
U.log(string.format("A player=(%d,%d)%s pika=(%d,%d) push=%s",
|
||||
p.cellX, p.cellY, p.facing, npc.cellX, npc.cellY, backDir))
|
||||
|
||||
local startX, startY = p.cellX, p.cellY
|
||||
local seeded, blockedTicks, movedAt = 0, 0, nil
|
||||
for i = 1, 40 do
|
||||
table.insert(game.input.pressQueue, backDir)
|
||||
game.input.state[backDir] = true
|
||||
coroutine.yield()
|
||||
seeded = math.max(seeded, ow.pikachuCollisionCounter or 0)
|
||||
if p.cellX ~= startX or p.cellY ~= startY or p.moving then
|
||||
movedAt = i
|
||||
break
|
||||
end
|
||||
if p.facing == backDir and npc.passable == false then
|
||||
blockedTicks = blockedTicks + 1
|
||||
end
|
||||
end
|
||||
release(backDir)
|
||||
U.log("A seeded=" .. seeded .. " blockedTicks=" .. blockedTicks
|
||||
.. " movedAt=" .. tostring(movedAt))
|
||||
check("turning toward the companion seeds the 8-count bump", seeded == 8)
|
||||
check("the bump holds the player for several frames, then yields",
|
||||
blockedTicks >= 4 and movedAt ~= nil)
|
||||
|
||||
U.wait(20)
|
||||
npc = follower()
|
||||
p = ow.player
|
||||
backDir = (npc.cellY > p.cellY and "down") or (npc.cellY < p.cellY and "up")
|
||||
or (npc.cellX > p.cellX and "right") or "left"
|
||||
startX, startY = p.cellX, p.cellY
|
||||
local bWalkedThrough = false
|
||||
game.input.state.b = true
|
||||
for _ = 1, 40 do
|
||||
table.insert(game.input.pressQueue, "b")
|
||||
table.insert(game.input.pressQueue, backDir)
|
||||
game.input.state[backDir] = true
|
||||
coroutine.yield()
|
||||
if p.cellX ~= startX or p.cellY ~= startY or p.moving then
|
||||
bWalkedThrough = true
|
||||
break
|
||||
end
|
||||
end
|
||||
release(backDir, "b")
|
||||
check("holding B walks straight through the companion", bWalkedThrough)
|
||||
|
||||
-- =====================================================================
|
||||
-- B. the confused walk lands directly below Bill
|
||||
-- (scripts/BillsHouse_2.asm:125 from the emerged cell)
|
||||
-- =====================================================================
|
||||
resetSave()
|
||||
U.teleport(game, MAP, 2, 7, "up")
|
||||
U.wait(20)
|
||||
ow = game.overworld
|
||||
npc = follower()
|
||||
if not check("the follower spawned for the confused beat", npc ~= nil) then
|
||||
finish()
|
||||
end
|
||||
check("the confused beat started", ow.pikachuBillsScene == true)
|
||||
check("the scripted walk uses the 16-frame Pikachu step",
|
||||
npc.stepFrames == 16)
|
||||
|
||||
for _ = 1, 600 do
|
||||
if ow.emote and ow.emote.pikaPic then break end
|
||||
U.wait(1)
|
||||
end
|
||||
for _ = 1, 900 do
|
||||
if not ow.emote then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(10)
|
||||
|
||||
local bill = objectAt("BILLSHOUSE_BILL_POKEMON")
|
||||
npc = follower()
|
||||
U.log(string.format("B pika=(%d,%d) bill=(%s,%s)",
|
||||
npc.cellX, npc.cellY,
|
||||
bill and bill.cellX or "?", bill and bill.cellY or "?"))
|
||||
check("the confused walk ends directly below Bill (6,6)",
|
||||
npc.cellX == 6 and npc.cellY == 6)
|
||||
check("which is Bill's own column, one row south",
|
||||
bill ~= nil and npc.cellX == bill.cellX and npc.cellY == bill.cellY + 1)
|
||||
U.shot(game, SHOT_DIR .. "/01_confused_walk_ends_below_bill.png")
|
||||
|
||||
-- =====================================================================
|
||||
-- C. the parked companion is solid -- home/overworld.asm:1238-1240
|
||||
-- =====================================================================
|
||||
p = ow.player
|
||||
for _ = 1, 4 do step("right") end
|
||||
U.wait(10)
|
||||
U.log(string.format("C player=(%d,%d)%s pika=(%d,%d) passable=%s",
|
||||
p.cellX, p.cellY, p.facing, npc.cellX, npc.cellY,
|
||||
tostring(npc.passable)))
|
||||
check("the player reached the cell south of the companion",
|
||||
p.cellX == 6 and p.cellY == 7)
|
||||
check("and following is still disabled, so the companion is solid",
|
||||
npc.passable == false)
|
||||
startX, startY = p.cellX, p.cellY
|
||||
press("up", 60)
|
||||
release("up")
|
||||
check("pushing north into the parked companion never moves the player",
|
||||
p.cellX == startX and p.cellY == startY)
|
||||
U.shot(game, SHOT_DIR .. "/02_solid_companion_blocks_player.png")
|
||||
|
||||
-- =====================================================================
|
||||
-- D. the cell separator runs the cartridge's 268 delay frames plus five
|
||||
-- WaitForSoundToFinish drains -- bills_house_pc.asm:14-40, :51-63
|
||||
-- =====================================================================
|
||||
game.save.flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = true
|
||||
game.save.flags.EVENT_USED_CELL_SEPARATOR_ON_BILL = nil
|
||||
U.teleport(game, MAP, 1, 5, "up")
|
||||
U.wait(20)
|
||||
ow = game.overworld
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
for _ = 1, 200 do -- type + prompt-button the initiated text away
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
if ow.runner and ow.runner:isRunning() then break end
|
||||
if #(ow.pendingScripts or {}) > 0 then break end
|
||||
end
|
||||
|
||||
local chainFrames, shot = 0, false
|
||||
for _ = 1, 1200 do
|
||||
if game.save.flags.EVENT_USED_CELL_SEPARATOR_ON_BILL then break end
|
||||
chainFrames = chainFrames + 1
|
||||
U.wait(1)
|
||||
if not shot and chainFrames == 150 then
|
||||
shot = true
|
||||
U.shot(game, SHOT_DIR .. "/03_cell_separator_running.png")
|
||||
end
|
||||
end
|
||||
-- 268 asm delay frames less the handful the text-box drain eats before
|
||||
-- this loop starts; the pre-fix chain was 222 and measures ~218 here
|
||||
U.log("D chainFrames=" .. chainFrames .. " (asm budget 268 + five SFX)")
|
||||
check("the separator chain runs the cartridge's ~268 delay frames",
|
||||
chainFrames >= 255)
|
||||
check("and it sets EVENT_USED_CELL_SEPARATOR_ON_BILL only at the end",
|
||||
game.save.flags.EVENT_USED_CELL_SEPARATOR_ON_BILL == true)
|
||||
|
||||
for _ = 1, 600 do
|
||||
if objectAt("BILLSHOUSE_BILL1") then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(30)
|
||||
check("Bill steps out of the machine when the chain ends",
|
||||
objectAt("BILLSHOUSE_BILL1") ~= nil)
|
||||
U.shot(game, SHOT_DIR .. "/04_bill_out_of_the_machine.png")
|
||||
|
||||
finish()
|
||||
end
|
||||
@@ -85,8 +85,9 @@ check(setUpvalue(OW.billsHousePokemonList, "TextBox", textBoxStub),
|
||||
local realMenu = package.loaded["src.ui.Menu"]
|
||||
package.loaded["src.ui.Menu"] = menuStub
|
||||
|
||||
local queued
|
||||
local fakeSelf = setmetatable({
|
||||
queueScript = function() end,
|
||||
queueScript = function(_, script, extra) queued = { script, extra } end,
|
||||
billsHouseBillExits = function() end,
|
||||
}, { __index = OW })
|
||||
|
||||
@@ -127,11 +128,45 @@ package.loaded["src.core.Sound"] = {
|
||||
play = function() end,
|
||||
playCry = function() end,
|
||||
}
|
||||
queued = nil
|
||||
runPC()
|
||||
check(musicStopped, "cell separator stops map music")
|
||||
check(tostring(lastPush().text):find("Cell", 1, true)
|
||||
or tostring(lastPush().text):find("TELEPORTER", 1, true),
|
||||
"separator path prints initiated text")
|
||||
|
||||
-- bills_house_pc.asm:14-40 plus BillsHouseInitiatedText's text_asm at :51-63:
|
||||
-- 268 delay frames, and every PlaySound is followed by WaitForSoundToFinish
|
||||
-- before the next delay starts.
|
||||
local wantSeparator = {
|
||||
{ "wait", 16 },
|
||||
{ "play_sound", "Switch" }, { "wait_sound" },
|
||||
{ "wait", 60 },
|
||||
{ "wait", 32 },
|
||||
{ "play_sound", "Tink" }, { "wait_sound" },
|
||||
{ "wait", 80 },
|
||||
{ "play_sound", "Shrink" }, { "wait_sound" },
|
||||
{ "wait", 48 },
|
||||
{ "play_sound", "Tink" }, { "wait_sound" },
|
||||
{ "wait", 32 },
|
||||
{ "play_sound", "Get_Item1" }, { "wait_sound" },
|
||||
}
|
||||
check(queued ~= nil, "the separator queues its cutscene script")
|
||||
local gotRows, delayFrames = {}, 0
|
||||
for _, row in ipairs(queued and queued[1] or {}) do
|
||||
gotRows[#gotRows + 1] = table.concat({ tostring(row[1]), tostring(row[2]) }, " ")
|
||||
if row[1] == "wait" then delayFrames = delayFrames + row[2] end
|
||||
end
|
||||
local wantRows = {}
|
||||
for _, row in ipairs(wantSeparator) do
|
||||
wantRows[#wantRows + 1] = table.concat({ tostring(row[1]), tostring(row[2]) }, " ")
|
||||
end
|
||||
eq(table.concat(gotRows, " | "), table.concat(wantRows, " | "),
|
||||
"the separator chain matches bills_house_pc.asm delay for delay")
|
||||
eq(delayFrames, 268, "268 delay frames, DelayFrames call for DelayFrames call")
|
||||
check(not fakeGame.save.flags.EVENT_USED_CELL_SEPARATOR_ON_BILL,
|
||||
"SetEvent waits for the chain, not the text box")
|
||||
queued[2].onDone()
|
||||
check(fakeGame.save.flags.EVENT_USED_CELL_SEPARATOR_ON_BILL,
|
||||
"separator path sets EVENT_USED_CELL_SEPARATOR_ON_BILL")
|
||||
|
||||
|
||||
@@ -248,5 +248,104 @@ for _, scene in ipairs({ "pikachuBillsScene", "pikachuFanClubScene",
|
||||
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
|
||||
|
||||
-- CalculatePikachuPlacementCoords never leaves the companion on the
|
||||
-- player's own cell (engine/pikachu/pikachu_follow.asm:59), so
|
||||
-- BillsHousePikachuConfused's three STEP_RIGHTs start one cell east of the
|
||||
-- door and land it directly below Bill.
|
||||
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 = 2, cellY = 7, px = 32, 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 == 1,
|
||||
"a companion still stacked on the player steps off before the walk")
|
||||
emergeNpc.cellX = 3
|
||||
emergeMoves[1].onDone()
|
||||
check(#emergeMoves == 2 and emergeMoves[2].dir == "right"
|
||||
and emergeMoves[2].tiles == 3,
|
||||
"then runs PikachuMovement_Confused's three STEP_RIGHTs")
|
||||
|
||||
GameVersion.set("red")
|
||||
S.finish()
|
||||
|
||||
Reference in New Issue
Block a user