mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
316 lines
14 KiB
Lua
316 lines
14 KiB
Lua
-- Decorations: owning them, setting them up, and the two PLAYERS_HOUSE_2F map
|
|
-- callbacks that put the room together (engine/overworld/decorations.asm, with
|
|
-- data/decorations/attributes.asm and decorations.asm beside it).
|
|
--
|
|
-- ROM-free: `luajit tests/gen2_decorations_test.lua`. The cache section at the
|
|
-- bottom SKIPs when no Gold cache is present.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 decorations")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local Decorations = require("src.core.gen2.Decorations")
|
|
local Events = require("src.world.gen2.Events")
|
|
local Vm = require("src.script.gen2.Vm")
|
|
|
|
-- constants/deco_constants.asm, the handful this file names.
|
|
local BEDS, DECO_FEATHERY_BED, DECO_PINK_BED = 1, 2, 3
|
|
local DECO_RED_CARPET = 7
|
|
local DECO_TOWN_MAP, DECO_PIKACHU_POSTER = 16, 17
|
|
local DECO_FAMICOM = 21
|
|
local DECO_BIG_SNORLAX_DOLL = 26
|
|
local DOLLS, DECO_PIKACHU_DOLL, DECO_CLEFAIRY_DOLL = 29, 30, 32
|
|
local DECO_GOLD_TROPHY_DOLL, DECO_SILVER_TROPHY_DOLL = 51, 52
|
|
|
|
-- ---- the attributes table -------------------------------------------------
|
|
-- Its length is asserted in the asm as NUM_DECOS + NUM_DECO_CATEGORIES + 1 =
|
|
-- 45 + 7 + 1, and row 0 is the CANCEL row every category menu ends on.
|
|
do
|
|
local rows = 0
|
|
for _ in pairs(Decorations.ATTRIBUTES) do rows = rows + 1 end
|
|
eq(rows, 53, "DecorationAttributes is 53 rows, row 0 included")
|
|
eq(Decorations.ATTRIBUTES[0].name, "CANCEL", "row 0 is CANCEL")
|
|
check(Decorations.ATTRIBUTES[0].action == nil,
|
|
"and DecoAction_nothing, so it does nothing at all")
|
|
eq(Decorations.ATTRIBUTES[BEDS].name, "PUT IT AWAY",
|
|
"the category row is the PUT IT AWAY one")
|
|
eq(Decorations.ATTRIBUTES[BEDS].action, "PUT_AWAY_BED",
|
|
"and it puts away that category's slot")
|
|
end
|
|
|
|
-- ---- GetDecoName ----------------------------------------------------------
|
|
do
|
|
eq(Decorations.name(DECO_FEATHERY_BED), "FEATHERY BED", "a bed appends BED")
|
|
eq(Decorations.name(DECO_RED_CARPET), "RED CARPET", "a carpet appends CARPET")
|
|
eq(Decorations.name(DECO_PIKACHU_DOLL), "PIKACHU DOLL", "a doll appends DOLL")
|
|
eq(Decorations.name(DECO_BIG_SNORLAX_DOLL), "BIG SNORLAX",
|
|
"a big doll PREPENDS BIG , it does not append DOLL")
|
|
eq(Decorations.name(DECO_PIKACHU_POSTER), "PIKACHU POSTER",
|
|
"a poster appends POSTER")
|
|
-- Both of these are DECO_PLANT rows precisely so that nothing is appended.
|
|
eq(Decorations.name(DECO_TOWN_MAP), "TOWN MAP",
|
|
"the TOWN MAP poster is a DecorationNames string, not a mon plus POSTER")
|
|
eq(Decorations.name(DECO_GOLD_TROPHY_DOLL), "GOLD TROPHY",
|
|
"and so is the trophy")
|
|
end
|
|
|
|
-- ---- owning ---------------------------------------------------------------
|
|
do
|
|
local events = Events.new()
|
|
check(not Decorations.owns(events, DECO_PINK_BED), "a new game owns no beds")
|
|
Decorations.give(events, DECO_PINK_BED)
|
|
check(Decorations.owns(events, DECO_PINK_BED), "give sets the DECOATTR flag")
|
|
check(not Decorations.owns(events, DECO_FEATHERY_BED),
|
|
"and only that one -- the flags are one bit each")
|
|
|
|
-- SetSpecificDecorationFlag names its decoration by DECOFLAG_*, which is a
|
|
-- DecorationIDs index and NOT the DECO_* the attributes table is keyed by.
|
|
eq(Decorations.idForFlag(Decorations.DECOFLAG_GOLD_TROPHY_DOLL),
|
|
DECO_GOLD_TROPHY_DOLL, "DECOFLAG_GOLD_TROPHY_DOLL is DECO_GOLD_TROPHY_DOLL")
|
|
eq(Decorations.idForFlag(Decorations.DECOFLAG_SILVER_TROPHY_DOLL),
|
|
DECO_SILVER_TROPHY_DOLL, "and the silver one likewise")
|
|
eq(Decorations.idForFlag(0), DECO_FEATHERY_BED,
|
|
"DECOFLAG_* is a const_def block, so flag 0 is the first bed")
|
|
|
|
local boxed = Events.new()
|
|
Decorations.giveFlag(boxed, Decorations.DECOFLAG_SILVER_TROPHY_DOLL)
|
|
check(Decorations.owns(boxed, DECO_SILVER_TROPHY_DOLL),
|
|
"the NORMAL BOX's trophy arrives through the DECOFLAG path")
|
|
end
|
|
|
|
-- ---- the category and item lists ------------------------------------------
|
|
do
|
|
local events = Events.new()
|
|
eq(#Decorations.ownedCategories(events), 0,
|
|
"with nothing owned there is not a single category")
|
|
Decorations.give(events, DECO_PIKACHU_DOLL)
|
|
local cats = Decorations.ownedCategories(events)
|
|
eq(#cats, 1, "one doll shows one category")
|
|
eq(cats[1].label, "ORNAMENT", "and it is the ornament menu")
|
|
|
|
local rows = Decorations.rows(events, cats[1])
|
|
eq(#rows, 3, "the list is the doll, PUT IT AWAY and CANCEL")
|
|
eq(rows[1], DECO_PIKACHU_DOLL, "the owned decoration first")
|
|
eq(rows[2], DOLLS, "then the category's own row")
|
|
eq(rows[3], 0, "then row 0, which is CANCEL")
|
|
|
|
-- The trophies live in the ornament list, past the twenty-one dolls.
|
|
local trophies = Events.new()
|
|
Decorations.give(trophies, DECO_GOLD_TROPHY_DOLL)
|
|
eq(Decorations.ownedCategories(trophies)[1].label, "ORNAMENT",
|
|
"a trophy is an ornament")
|
|
end
|
|
|
|
-- ---- DoDecorationAction2 --------------------------------------------------
|
|
do
|
|
local state = { }
|
|
local changed, pages = Decorations.apply(state, DECO_FEATHERY_BED)
|
|
check(changed, "setting up an empty slot changes the room")
|
|
eq(state.bed, DECO_FEATHERY_BED, "and fills wDecoBed")
|
|
eq(pages[1], "Set up the\nFEATHERY BED.", "SetUpTheDecoText")
|
|
|
|
changed, pages = Decorations.apply(state, DECO_FEATHERY_BED)
|
|
check(not changed, ".alreadythere writes nothing")
|
|
eq(pages[1], "That's already set\nup.", "AlreadySetUpText")
|
|
|
|
changed, pages = Decorations.apply(state, DECO_PINK_BED)
|
|
check(changed, "a second bed replaces the first")
|
|
eq(state.bed, DECO_PINK_BED, "the slot holds ONE thing")
|
|
eq(#pages, 2, "PutAwayAndSetUpText is two pages")
|
|
eq(pages[1], "Put away the\nFEATHERY BED", "the old one is named first")
|
|
eq(pages[2], "and set up the\nPINK BED.", "then the new one")
|
|
|
|
-- The PUT IT AWAY row, whose action is the category's put-away half.
|
|
changed, pages = Decorations.apply(state, BEDS)
|
|
check(changed, "putting a bed away changes the room too")
|
|
eq(state.bed, 0, "and empties the slot")
|
|
eq(pages[1], "Put away the\nPINK BED.",
|
|
"the text names what WAS out, not the row that was picked")
|
|
|
|
changed, pages = Decorations.apply(state, BEDS)
|
|
check(not changed, "an empty slot has nothing to put away")
|
|
eq(pages[1], "There's nothing to\nput away.", "NothingToPutAwayText")
|
|
end
|
|
|
|
-- ---- the two ornament sides -----------------------------------------------
|
|
do
|
|
local state = {}
|
|
local changed = Decorations.apply(state, DECO_PIKACHU_DOLL, nil)
|
|
check(not changed, "no side chosen is DecoAction_AskWhichSide's cancel")
|
|
eq(state.leftOrnament, nil, "and neither side is touched")
|
|
|
|
Decorations.apply(state, DECO_PIKACHU_DOLL, "right")
|
|
eq(state.rightOrnament, DECO_PIKACHU_DOLL, "RIGHT SIDE fills wDecoRightOrnament")
|
|
Decorations.apply(state, DECO_CLEFAIRY_DOLL, "left")
|
|
eq(state.leftOrnament, DECO_CLEFAIRY_DOLL, "LEFT SIDE fills the other")
|
|
|
|
-- .getwhichside: there is only one of each doll, so putting the Pikachu on
|
|
-- the left takes it off the right.
|
|
Decorations.apply(state, DECO_PIKACHU_DOLL, "left")
|
|
Decorations.clearOtherSide(state, DECO_PIKACHU_DOLL, "left")
|
|
eq(state.leftOrnament, DECO_PIKACHU_DOLL, "the doll moved to the left")
|
|
eq(state.rightOrnament, 0, "and is gone from the right")
|
|
end
|
|
|
|
-- ---- ToggleDecorationsVisibility ------------------------------------------
|
|
do
|
|
local state = {}
|
|
local rows = Decorations.visibility(state)
|
|
eq(#rows, 4, "four objects hang off the room: console, two dolls, big doll")
|
|
for _, row in ipairs(rows) do
|
|
check(row.hidden, "an empty slot SETS the object's flag, which hides it")
|
|
end
|
|
eq(rows[1].flag, 1857, "EVENT_PLAYERS_HOUSE_2F_CONSOLE")
|
|
eq(rows[4].flag, 1860, "EVENT_PLAYERS_HOUSE_2F_BIG_DOLL")
|
|
eq(rows[1].sprite, 0, "and the slots are SPRITE_VARS-relative")
|
|
eq(rows[4].sprite, 3, "SPRITE_BIG_DOLL is the fourth")
|
|
|
|
state.console = DECO_FAMICOM
|
|
state.bigDoll = DECO_BIG_SNORLAX_DOLL
|
|
rows = Decorations.visibility(state)
|
|
check(not rows[1].hidden, "a filled slot CLEARS the flag")
|
|
eq(rows[1].byte, 0x5c, "and writes SPRITE_FAMICOM into wVariableSprites")
|
|
eq(rows[4].byte, 0x33, "SPRITE_BIG_SNORLAX for the big doll")
|
|
check(rows[2].hidden, "the two ornaments are still empty")
|
|
end
|
|
|
|
-- ---- ToggleMaptileDecorations ---------------------------------------------
|
|
do
|
|
local state = {}
|
|
eq(#Decorations.tiles(state), 0,
|
|
"SetDecorationTile's `and a / ret z`: an empty slot paints nothing")
|
|
check(not Decorations.posterVisible(state),
|
|
"and a bare wall is not readable")
|
|
|
|
state.bed = DECO_FEATHERY_BED
|
|
state.poster = DECO_TOWN_MAP
|
|
state.carpet = DECO_RED_CARPET
|
|
local tiles = Decorations.tiles(state)
|
|
local function blockAt(x, y)
|
|
for _, tile in ipairs(tiles) do
|
|
if tile.x == x and tile.y == y then return tile.block end
|
|
end
|
|
return nil
|
|
end
|
|
-- The asm's coordinates are CELLS and PadCoords_de/GetBlockLocation halve
|
|
-- them, the same way `changeblock` does.
|
|
eq(blockAt(0, 2), 0x1b, "bed cell (0,4) is block (0,2)")
|
|
eq(blockAt(3, 0), 0x1f, "poster cell (6,0) is block (3,0)")
|
|
eq(blockAt(0, 0), 0x08, "carpet top-left")
|
|
eq(blockAt(0, 1), 0x09, "carpet bottom row is +1")
|
|
eq(blockAt(1, 1), 0x0a, "+2")
|
|
eq(blockAt(2, 1), 0x09, "and +1 again")
|
|
check(Decorations.posterVisible(state), "a poster on the wall is readable")
|
|
end
|
|
|
|
-- ---- the state on the save ------------------------------------------------
|
|
do
|
|
local save = {}
|
|
local state = Decorations.state(save)
|
|
-- InitDecorations, farcall'd from intro_menu.asm at New Game.
|
|
eq(state.bed, DECO_FEATHERY_BED, "a new game starts with the feathery bed")
|
|
eq(state.poster, DECO_TOWN_MAP, "and the TOWN MAP on the wall")
|
|
state.bed = DECO_PINK_BED
|
|
eq(Decorations.state(save).bed, DECO_PINK_BED,
|
|
"and the state lives on the save, not in the module")
|
|
end
|
|
|
|
-- ---- describedecoration, through the VM -----------------------------------
|
|
-- Script_describedecoration is a ScriptJump: the arm hands back a script and
|
|
-- the command never returns. Which script depends on what is standing there.
|
|
do
|
|
local scripts = {
|
|
["09:718b"] = { { op = "writetext", text = "t:bare" }, { op = "end" } },
|
|
["09:7000"] = { { op = "writetext", text = "t:map" }, { op = "end" } },
|
|
["09:71a8"] = { { op = "writetext", text = "t:doll" }, { op = "end" } },
|
|
}
|
|
local TEXTS = {
|
|
["t:bare"] = "bare wall", ["t:map"] = "TOWN MAP", ["t:doll"] = "adorable",
|
|
}
|
|
local decorations = {
|
|
DECODESC_POSTER = { script = "09:718b", posters = {
|
|
{ decoration = DECO_TOWN_MAP, script = "09:7000" },
|
|
} },
|
|
DECODESC_LEFT_DOLL = { script = "09:71a8" },
|
|
}
|
|
local function run(descName, slotFn)
|
|
local shown = {}
|
|
scripts.generation = 2
|
|
scripts["s:t"] = { { op = "describedecoration", args = { 0 },
|
|
decorationName = descName } }
|
|
local vm = Vm.new(scripts, TEXTS, Events.new(), {
|
|
decorationSlot = slotFn,
|
|
eventTables = { decorations = decorations },
|
|
showText = function(text, done) shown[#shown + 1] = text; done() end,
|
|
})
|
|
vm:start("s:t")
|
|
return shown, vm
|
|
end
|
|
|
|
local shown = run("DECODESC_POSTER", function() return 0, nil end)
|
|
eq(shown[1], "bare wall",
|
|
"no poster placed falls to DecorationDesc_NullPoster")
|
|
|
|
shown = run("DECODESC_POSTER", function() return DECO_TOWN_MAP, nil end)
|
|
eq(shown[1], "TOWN MAP",
|
|
"IsInArray finds the placed poster and jumps to ITS script")
|
|
|
|
local _, vm = run("DECODESC_LEFT_DOLL", function()
|
|
return DECO_PIKACHU_DOLL, "PIKACHU DOLL"
|
|
end)
|
|
eq(vm.stringBuffer, "PIKACHU DOLL",
|
|
"the ornament arm puts the name in wStringBuffer3 for the text to print")
|
|
end
|
|
|
|
-- ---- the cache ------------------------------------------------------------
|
|
-- Same default every other gen2 suite uses, so a run with no GOLD_CACHE set
|
|
-- (tests/run_tests.lua) still reads the cache instead of skipping silently.
|
|
local cache = os.getenv("GOLD_CACHE")
|
|
or ((os.getenv("HOME") or "") .. "/Library/Application Support/LOVE/gold-dev/gold")
|
|
local function loadCache(name)
|
|
if not cache then return nil end
|
|
local chunk = loadfile(cache .. "/data/generated/" .. name .. ".lua")
|
|
return chunk and chunk() or nil
|
|
end
|
|
|
|
local events = loadCache("events")
|
|
if not events then
|
|
check(true, "no GOLD_CACHE: the extracted arms and flags are not checked (SKIP)")
|
|
else
|
|
-- Every DECODESC_* the cache knows has a slot here, or describedecoration
|
|
-- would read a wDeco* byte that does not exist.
|
|
for _, name in ipairs(events.decorationOrder or {}) do
|
|
check(Decorations.DESC_SLOTS[name] ~= nil,
|
|
("%s names a wDeco* slot"):format(name))
|
|
end
|
|
-- The poster arm's table is the four posters, and each of their DECO_* ids
|
|
-- is a real attributes row of the poster category.
|
|
local poster = (events.decorations or {}).DECODESC_POSTER
|
|
eq(#((poster and poster.posters) or {}), 4, "four posters can hang there")
|
|
for _, row in ipairs((poster and poster.posters) or {}) do
|
|
local attr = Decorations.attributes(row.decoration)
|
|
check(attr ~= nil and attr.action == "SET_UP_POSTER",
|
|
("poster %d is a poster row here too"):format(row.decoration))
|
|
end
|
|
end
|
|
|
|
local initial = loadCache("initial_events")
|
|
if not initial then
|
|
check(true, "no GOLD_CACHE: InitializeEventsScript's flags are not checked (SKIP)")
|
|
else
|
|
-- PlayersHouse2FInitializeRoomCallback ends in `jumpstd InitializeEventsScript`,
|
|
-- and that script is where the room's four objects are hidden and the two
|
|
-- decorations the player starts with are marked owned.
|
|
local set = {}
|
|
for _, flag in ipairs(initial.flags or {}) do set[flag] = true end
|
|
for _, row in ipairs(Decorations.OBJECT_SLOTS) do
|
|
check(set[row.flag], ("object flag %d starts SET, i.e. hidden"):format(row.flag))
|
|
end
|
|
check(set[Decorations.attributes(DECO_FEATHERY_BED).flag],
|
|
"and the feathery bed starts owned")
|
|
check(set[Decorations.attributes(DECO_TOWN_MAP).flag],
|
|
"as does the TOWN MAP poster")
|
|
end
|
|
|
|
S.finish()
|