-- Parity: the Rocket Hideout lift gates (#372). -- -- Oracle: scripts/RocketHideoutB4F.asm RocketHideoutB4FDoorCallbackScript -- stamps $2d over the lift doorway (lb bc, 5, 12) until CheckBothEventsSet -- EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_0 / _1, then plays SFX_GO_INSIDE and -- writes $e; scripts/RocketHideoutB1F.asm is the same shape with $54 at -- lb bc, 8, 12 over EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_4. The callback -- runs whenever BIT_CUR_MAP_LOADED_1 is set, which home/trainers.asm -- EndTrainerBattle does, so the gate opens the moment the last guard falls -- rather than on the next map load. -- -- The .blk layouts ship both doorways open, so a cache whose closedDoors -- predates the two rows left the gates open all game: FieldDefaults has to -- seed them the way it seeds the PrintTrashText bins. -- -- Self-contained: `luajit tests/parity_hideout_gate.lua`; also globbed by -- tests/run_tests.lua. package.path = "./?.lua;./?/init.lua;" .. package.path if not _G.love then _G.love = require("tests.love_stub") end local Data = require("src.core.Data") if not (Data.maps and Data.maps.ROCKET_HIDEOUT_B4F) then Data:load() end local S = require("tests.harness").suite("parity hideout gate") local check, eq = S.check, S.eq local FieldDefaults = require("src.world.FieldDefaults") local B4F, B1F = "ROCKET_HIDEOUT_B4F", "ROCKET_HIDEOUT_B1F" local GUARD_0 = "EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_0" local GUARD_1 = "EVENT_BEAT_ROCKET_HIDEOUT_4_TRAINER_1" local B1F_GUARD = "EVENT_BEAT_ROCKET_HIDEOUT_1_TRAINER_4" -- ---- the shipped layout --------------------------------------------------- -- ReplaceTileBlock only ever writes; nothing in the .blk closes these, which -- is why a missing closedDoors row reads as a permanently open gate. local function shippedBlock(mapId, bx, by) local def = Data.maps[mapId] return def.blocks[by * def.width + bx + 1] end eq(shippedBlock(B4F, 12, 5), 0x0e, "B4F ships the lift doorway open (block $e)") eq(shippedBlock(B1F, 12, 8), 0x0e, "B1F ships its doorway open (block $e)") -- ---- the door rows ------------------------------------------------------- local doors = FieldDefaults.fieldValue(Data, "cardKeyDoors", "closedDoors") check(type(doors) == "table", "field.cardKeyDoors.closedDoors resolves") local b4 = doors[B4F] and doors[B4F][1] check(b4 ~= nil, "B4F has a closed-door row") if b4 then eq(b4.bx, 12, "B4F door x (lb bc, 5, 12)") eq(b4.by, 5, "B4F door y (lb bc, 5, 12)") eq(b4.block, 0x2d, "B4F closed block is $2d") eq(b4.open, 0x0e, "B4F open block is $e") eq(b4.event, nil, "B4F is not gated on a single event") eq(b4.events and b4.events[1], GUARD_0, "CheckBothEventsSet arg 1") eq(b4.events and b4.events[2], GUARD_1, "CheckBothEventsSet arg 2") end local b1 = doors[B1F] and doors[B1F][1] check(b1 ~= nil, "B1F has a closed-door row") if b1 then eq(b1.bx, 12, "B1F door x (lb bc, 8, 12)") eq(b1.by, 8, "B1F door y (lb bc, 8, 12)") eq(b1.block, 0x54, "B1F closed block is $54") eq(b1.open, 0x0e, "B1F open block is $e") eq(b1.event, B1F_GUARD, "B1F waits on the fifth grunt") end -- Yellow ships no RocketHideoutB4FDoorCallbackScript (its -- scripts/RocketHideoutB4F.asm calls EnableAutoTextBoxDrawing first thing) -- and the same open .blk doorway, and Jessie & James hold the two guard -- slots without setting either guard flag, so the B4F row must not be -- stamped on a Yellow boot (#650). Read through fieldValue, which is how -- a cache carrying closedDoors but no skipMaps still resolves it. local skipMaps = FieldDefaults.fieldValue(Data, "cardKeyDoors", "skipMaps") check(skipMaps and skipMaps.yellow and skipMaps.yellow[B4F], "Yellow skips the B4F lift gate") check(not (skipMaps and skipMaps.red), "Red keeps both gates") check(not (skipMaps and skipMaps.blue), "Blue keeps both gates") -- the events the rows name have to be the ones the guards actually set: -- interact() writes trainerHeader().event on a win eq(Data:trainerHeader("RocketHideoutB4F", 2).event, GUARD_0, "the (23,12) guard sets trainer_0") eq(Data:trainerHeader("RocketHideoutB4F", 3).event, GUARD_1, "the (26,12) guard sets trainer_1") eq(Data:trainerHeader("RocketHideoutB1F", 5).event, B1F_GUARD, "B1F's fifth grunt sets trainer_4") -- ---- the stale cache the reporters have ---------------------------------- -- closedDoors exists but holds only the Silph rows, so fieldValue's per-path -- fallback resolves the cache's table and finds no hideout floors. seed() -- fills map keys in place, which is what heals it. do local silph = { { block = 0x54, bx = 2, by = 2, open = 0x0e, event = "EVENT_SILPH_CO_2_UNLOCKED_DOOR1" } } local stale = { field = { cardKeyDoors = { doorTiles = { 24 }, closedDoors = { SILPH_CO_2F = silph } } } } check(FieldDefaults.fieldValue(stale, "cardKeyDoors", "closedDoors")[B4F] == nil, "the stale cache really is missing the B4F row") FieldDefaults.seed(stale) local healed = FieldDefaults.fieldValue(stale, "cardKeyDoors", "closedDoors") eq(healed[B4F] and healed[B4F][1].block, 0x2d, "seed fills the B4F gate") eq(healed[B1F] and healed[B1F][1].block, 0x54, "seed fills the B1F gate") eq(healed.SILPH_CO_2F, silph, "and leaves the cache's own rows alone") eq(stale.field.cardKeyDoors.doorTiles[1], 24, "including the card-key tiles") end -- ---- the live floor ------------------------------------------------------ require("src.render.Font").load(Data) local Game = require("src.core.Game") local Input = require("src.core.Input") local StateStack = require("src.core.StateStack") local Renderer = require("src.render.Renderer") local SaveData = require("src.core.SaveData") local Pokemon = require("src.pokemon.Pokemon") local Sound = require("src.core.Sound") local OW = require("src.world.OverworldController") Game.data = Data Game.input = Input; Input:init() Game.renderer = Renderer; Renderer:init() Game.stack = StateStack; StateStack:init() Game.save = SaveData.newGame() Game.save.party = { Pokemon.new(Data, "CHARMANDER", 20) } local sfx = {} local realPlay = Sound.play Sound.play = function(_, id) sfx[#sfx + 1] = id end local function goInsides() local n = 0 for _, id in ipairs(sfx) do if id == "Go_Inside" then n = n + 1 end end return n end -- Map:setBlock writes through to the shared Data record every later suite in -- this process reads, so both gates are put back at the bottom of the file local restore = {} local function remember(mapId, bx, by) restore[#restore + 1] = { mapId, bx, by, shippedBlock(mapId, bx, by) } end remember(B4F, 12, 5) remember(B1F, 12, 8) local function arrive(mapId, x, y) while Game.stack:top() do Game.stack:pop() end Game.stack:push(OW, mapId, x, y, "up") return Game.stack:top() end -- one cell south of the gate, the way a player walks up to it local ow = arrive(B4F, 24, 13) eq(ow.map:blockAt(12, 5), 0x2d, "arriving with both guards alive bars the gate") check(not ow.map:isWalkableCell(24, 11) and not ow.map:isWalkableCell(25, 11), "and neither doorway cell can be stepped into") sfx = {} Game.save.flags[GUARD_0] = true ow:afterBattle("win", {}) eq(ow.map:blockAt(12, 5), 0x2d, "one guard down leaves the gate barred") eq(goInsides(), 0, "and plays no door sound") Game.save.flags[GUARD_1] = true ow:afterBattle("win", {}) eq(ow.map:blockAt(12, 5), 0x0e, "the second guard opens the gate without leaving the floor") eq(goInsides(), 1, "with one SFX_GO_INSIDE") check(ow.map:isWalkableCell(24, 11) and ow.map:isWalkableCell(25, 11), "the doorway cells are floor now") -- EVENT_ROCKET_HIDEOUT_4_DOOR_UNLOCKED: the stamp is a transition, not a -- per-load sound ow:afterBattle("win", {}) eq(goInsides(), 1, "a later battle on the open floor is silent") ow = arrive(B4F, 24, 13) eq(ow.map:blockAt(12, 5), 0x0e, "re-entering the floor keeps it open") eq(goInsides(), 1, "and does not re-play the door sound") -- ---- B1F, the same seed row --------------------------------------------- sfx = {} ow = arrive(B1F, 24, 17) eq(ow.map:blockAt(12, 8), 0x54, "B1F is barred until the fifth grunt") Game.save.flags[B1F_GUARD] = true ow:afterBattle("win", {}) eq(ow.map:blockAt(12, 8), 0x0e, "beating him opens it on the spot") eq(goInsides(), 1, "with its own door sound") for _, r in ipairs(restore) do local def = Data.maps[r[1]] def.blocks[r[3] * def.width + r[2] + 1] = r[4] end Sound.play = realPlay while Game.stack:top() do Game.stack:pop() end S.finish()