Files
gen1recomp/src/core/gen2/Decorations.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

524 lines
24 KiB
Lua

-- The ornaments in the player's bedroom: what the player owns, what is set up
-- where, and the two routines that put both on the map.
-- engine/overworld/decorations.asm, with data/decorations/attributes.asm,
-- names.asm and decorations.asm beside it.
--
-- Three separate pieces of state, and keeping them apart is the whole model:
--
-- OWNED one wEventFlags bit per decoration (DECOATTR_EVENT_FLAG). Set
-- means the player has it; nothing else ever clears one. This is
-- the same bitfield `setevent` writes, which is why owning a
-- decoration survives in the save with no new field.
-- PLACED eight bytes (wDecoBed .. wDecoRightOrnament), each holding the
-- DECO_* id standing in that slot or 0 for nothing. A slot holds
-- ONE thing: setting up a second bed puts the first away.
-- VISIBLE what the map shows, which is neither of the above. It is
-- rebuilt from PLACED by ToggleDecorationsVisibility (the four
-- object slots) and ToggleMaptileDecorations (the four blocks) --
-- and only ever on a MAP LOAD, because those are the
-- PLAYERS_HOUSE_2F NEWMAP and TILES callbacks. A flag a running
-- script sets does not move an object; the object list is read
-- when the map loads and not again, which is why the PC's own
-- `warp NONE, 0, 0` (Script_warp's MAPSETUP_BADWARP arm) is what
-- makes a placement appear.
--
-- Everything here is love-free and takes its state by argument, so the menu on
-- top of it (src/ui/gen2/DecorationMenu.lua) and the tests can drive the same
-- routines the map callbacks do.
local Strings = require("src.core.Strings")
local Decorations = {}
-- constants/deco_constants.asm, decoration types. The type decides how
-- GetDecoName spells the row and, for the four maptile kinds, that
-- DECOATTR_SPRITE is a BLOCK id rather than a sprite one.
local PLANT, BED, CARPET, POSTER, DOLL, BIGDOLL = 1, 2, 3, 4, 5, 6
-- The eight wDeco* bytes. `slot` on an action names one of these.
Decorations.SLOTS = {
"bed", "carpet", "plant", "poster", "console", "bigDoll",
"leftOrnament", "rightOrnament",
}
-- DoDecorationAction2.DecoActions, as a slot plus a direction rather than a
-- jumptable index: the fourteen entries are seven pairs, and the pair is the
-- only thing any caller cares about. The ornament pair is the odd one out --
-- it asks which side first, so its slot is decided at run time.
local ACTIONS = {
SET_UP_BED = { slot = "bed" },
PUT_AWAY_BED = { slot = "bed", put = true },
SET_UP_CARPET = { slot = "carpet" },
PUT_AWAY_CARPET = { slot = "carpet", put = true },
SET_UP_PLANT = { slot = "plant" },
PUT_AWAY_PLANT = { slot = "plant", put = true },
SET_UP_POSTER = { slot = "poster" },
PUT_AWAY_POSTER = { slot = "poster", put = true },
SET_UP_CONSOLE = { slot = "console" },
PUT_AWAY_CONSOLE = { slot = "console", put = true },
SET_UP_BIG_DOLL = { slot = "bigDoll" },
PUT_AWAY_BIG_DOLL = { slot = "bigDoll", put = true },
SET_UP_DOLL = { ornament = true },
PUT_AWAY_DOLL = { ornament = true, put = true },
}
Decorations.ACTIONS = ACTIONS
-- wEventFlags bit numbers, from constants/event_flags.asm. Numbers rather
-- than names because that is what the bitfield is keyed by everywhere else in
-- this port (src/world/gen2/Events.lua), and because the extracted scripts
-- that share these bits carry numbers too.
local EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1 = 0
local EVENT_DECO_BED_1 = 676
local EVENT_DECO_CARPET_1 = 680
local EVENT_DECO_PLANT_1 = 684
local EVENT_DECO_POSTER_1 = 687
local EVENT_DECO_FAMICOM = 691
local EVENT_DECO_PIKACHU_DOLL = 695
local EVENT_PLAYERS_ROOM_POSTER = 716
local EVENT_DECO_GOLD_TROPHY = 717
local EVENT_DECO_SILVER_TROPHY = 718
local EVENT_DECO_BIG_SNORLAX_DOLL = 719
Decorations.EVENT_PLAYERS_ROOM_POSTER = EVENT_PLAYERS_ROOM_POSTER
-- The four objects PLAYERS_HOUSE_2F hangs its decorations off. Each is a
-- wVariableSprites slot (SPRITE_VARS-relative, the way `variablesprite`'s byte
-- already is) paired with the object's own event flag, and
-- ToggleDecorationVisibility writes both: the sprite byte says WHAT stands
-- there and the flag says WHETHER it stands there at all.
Decorations.OBJECT_SLOTS = {
{ slot = "console", sprite = 0, flag = 1857 }, -- SPRITE_CONSOLE
{ slot = "leftOrnament", sprite = 1, flag = 1858 }, -- SPRITE_DOLL_1
{ slot = "rightOrnament", sprite = 2, flag = 1859 }, -- SPRITE_DOLL_2
{ slot = "bigDoll", sprite = 3, flag = 1860 }, -- SPRITE_BIG_DOLL
}
-- data/decorations/attributes.asm, verbatim and in its order: row 0 is the
-- unnamed CANCEL row every category menu ends on, and the seven rows whose
-- name is PUT_IT_AWAY are the category headers the deco constants share their
-- numbering with (BEDS = 1, CARPETS = 6, ...). So this table is indexed by
-- DECO_*, and `wMenuSelection` on the cart is an index straight into it.
--
-- `sprite` is one byte with two meanings, exactly as DECOATTR_SPRITE is: a
-- BLOCK id for the four kinds ToggleMaptileDecorations paints, and a SPRITE_*
-- byte for the four an object stands on. The SPRITE_* names are in comments
-- because the value the cart stores IS the byte -- wVariableSprites holds it
-- raw and World:resolveSprite looks it up in constants.spriteOrder.
local function deco(kind, name, action, flag, sprite)
return { type = kind, name = name, action = action, flag = flag,
sprite = sprite }
end
local TEMP = EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1
local ATTRIBUTES = {
[0] = deco(PLANT, "CANCEL", nil, TEMP, 0),
deco(PLANT, "PUT IT AWAY", "PUT_AWAY_BED", TEMP, 0), -- BEDS
deco(BED, "FEATHERY", "SET_UP_BED", EVENT_DECO_BED_1 + 0, 0x1b),
deco(BED, "PINK", "SET_UP_BED", EVENT_DECO_BED_1 + 1, 0x1c),
deco(BED, "POLKADOT", "SET_UP_BED", EVENT_DECO_BED_1 + 2, 0x1d),
deco(BED, "PIKACHU", "SET_UP_BED", EVENT_DECO_BED_1 + 3, 0x1e),
deco(PLANT, "PUT IT AWAY", "PUT_AWAY_CARPET", TEMP, 0), -- CARPETS
deco(CARPET, "RED", "SET_UP_CARPET", EVENT_DECO_CARPET_1 + 0, 0x08),
deco(CARPET, "BLUE", "SET_UP_CARPET", EVENT_DECO_CARPET_1 + 1, 0x0b),
deco(CARPET, "YELLOW", "SET_UP_CARPET", EVENT_DECO_CARPET_1 + 2, 0x0e),
deco(CARPET, "GREEN", "SET_UP_CARPET", EVENT_DECO_CARPET_1 + 3, 0x11),
deco(PLANT, "PUT IT AWAY", "PUT_AWAY_PLANT", TEMP, 0), -- PLANTS
deco(PLANT, "MAGNAPLANT", "SET_UP_PLANT", EVENT_DECO_PLANT_1 + 0, 0x20),
deco(PLANT, "TROPICPLANT", "SET_UP_PLANT", EVENT_DECO_PLANT_1 + 1, 0x21),
deco(PLANT, "JUMBOPLANT", "SET_UP_PLANT", EVENT_DECO_PLANT_1 + 2, 0x22),
deco(PLANT, "PUT IT AWAY", "PUT_AWAY_POSTER", TEMP, 0), -- POSTERS
-- The TOWN MAP poster is a DECO_PLANT: its name is a DecorationNames entry
-- rather than a species, so GetDecoName must not append " POSTER" to it.
deco(PLANT, "TOWN MAP", "SET_UP_POSTER", EVENT_DECO_POSTER_1 + 0, 0x1f),
deco(POSTER, "PIKACHU", "SET_UP_POSTER", EVENT_DECO_POSTER_1 + 1, 0x23),
deco(POSTER, "CLEFAIRY", "SET_UP_POSTER", EVENT_DECO_POSTER_1 + 2, 0x24),
deco(POSTER, "JIGGLYPUFF", "SET_UP_POSTER", EVENT_DECO_POSTER_1 + 3, 0x25),
deco(PLANT, "PUT IT AWAY", "PUT_AWAY_CONSOLE", TEMP, 0), -- CONSOLES
deco(PLANT, "NES", "SET_UP_CONSOLE", EVENT_DECO_FAMICOM + 0, 0x5c), -- SPRITE_FAMICOM
deco(PLANT, "SUPER NES", "SET_UP_CONSOLE", EVENT_DECO_FAMICOM + 1, 0x5b),
deco(PLANT, "NINTENDO64", "SET_UP_CONSOLE", EVENT_DECO_FAMICOM + 2, 0x51),
deco(PLANT, "VIRTUAL BOY", "SET_UP_CONSOLE", EVENT_DECO_FAMICOM + 3, 0x57),
deco(PLANT, "PUT IT AWAY", "PUT_AWAY_BIG_DOLL", TEMP, 0), -- BIG_DOLLS
deco(BIGDOLL, "SNORLAX", "SET_UP_BIG_DOLL", EVENT_DECO_BIG_SNORLAX_DOLL + 0, 0x33),
deco(BIGDOLL, "ONIX", "SET_UP_BIG_DOLL", EVENT_DECO_BIG_SNORLAX_DOLL + 1, 0x50),
deco(BIGDOLL, "LAPRAS", "SET_UP_BIG_DOLL", EVENT_DECO_BIG_SNORLAX_DOLL + 2, 0x47),
deco(PLANT, "PUT IT AWAY", "PUT_AWAY_DOLL", TEMP, 0), -- DOLLS
deco(DOLL, "PIKACHU", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 0, 0x8e),
-- The surfing Pikachu doll is a DECO_PLANT too, and for the same reason:
-- "SURF PIKACHU DOLL" is one DecorationNames string, not a mon plus " DOLL".
deco(PLANT, "SURF PIKACHU DOLL", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 1, 0x34),
deco(DOLL, "CLEFAIRY", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 2, 0x8f),
deco(DOLL, "JIGGLYPUFF", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 3, 0x94),
deco(DOLL, "BULBASAUR", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 4, 0x93),
deco(DOLL, "CHARMANDER", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 5, 0x90),
deco(DOLL, "SQUIRTLE", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 6, 0x89),
deco(DOLL, "POLIWAG", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 7, 0x8d),
deco(DOLL, "DIGLETT", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 8, 0x8c),
-- STARYU's doll stands on SPRITE_STARMIE; the cart's own row says so.
deco(DOLL, "STARYU", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 9, 0x92),
deco(DOLL, "MAGIKARP", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 10, 0x88),
deco(DOLL, "ODDISH", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 11, 0x85),
deco(DOLL, "GENGAR", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 12, 0x86),
deco(DOLL, "SHELLDER", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 13, 0x84),
deco(DOLL, "GRIMER", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 14, 0x95),
deco(DOLL, "VOLTORB", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 15, 0x9b),
deco(DOLL, "WEEDLE", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 16, 0x83),
deco(DOLL, "UNOWN", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 17, 0x80),
deco(DOLL, "GEODUDE", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 18, 0x81),
deco(DOLL, "MACHOP", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 19, 0x9a),
deco(DOLL, "TENTACOOL", "SET_UP_DOLL", EVENT_DECO_PIKACHU_DOLL + 20, 0x98),
-- Both trophies are SET_UP_DOLL: a trophy stands in an ornament slot.
deco(PLANT, "GOLD TROPHY", "SET_UP_DOLL", EVENT_DECO_GOLD_TROPHY, 0x5e),
deco(PLANT, "SILVER TROPHY", "SET_UP_DOLL", EVENT_DECO_SILVER_TROPHY, 0x5f),
}
Decorations.ATTRIBUTES = ATTRIBUTES
-- The seven category menus, in _PlayerDecorationMenu's .owned_pointers order.
-- `id` is the DECO_* of the category's own PUT_IT_AWAY row, which is exactly
-- what FindOwnedDecosInCategory appends to its list, and `members` is that
-- routine's own db list -- transcribed rather than derived from a range,
-- because the doll list runs past the two trophies and the big dolls do not
-- sit next to the small ones.
local function range(first, last)
local out = {}
for id = first, last do out[#out + 1] = id end
return out
end
Decorations.CATEGORIES = {
{ id = 1, label = "BED", members = range(2, 5) },
{ id = 6, label = "CARPET", members = range(7, 10) },
{ id = 11, label = "PLANT", members = range(12, 14) },
{ id = 15, label = "POSTER", members = range(16, 19) },
{ id = 20, label = "GAME CONSOLE", members = range(21, 24) },
{ id = 29, label = "ORNAMENT", members = range(30, 52) },
{ id = 25, label = "BIG DOLL", members = range(26, 28) },
}
-- data/decorations/decorations.asm DecorationIDs: DECOFLAG_* -> DECO_*. The
-- only thing that reads it is GetDecorationID, i.e. the routines that GIVE a
-- decoration, which name what they hand over by DECOFLAG.
local DECORATION_IDS = {}
do
local order = {
range(2, 5), range(7, 10), range(12, 14), range(16, 19), range(21, 24),
range(30, 50), range(26, 28), { 51, 52 },
}
for _, group in ipairs(order) do
for _, id in ipairs(group) do
DECORATION_IDS[#DECORATION_IDS + 1] = id
end
end
end
-- DECOFLAG_* is a `const_def` block, so it is 0-based: shift the 1-based Lua
-- list rather than leaving a caller to guess.
function Decorations.idForFlag(decoFlag)
return DECORATION_IDS[(decoFlag or 0) + 1]
end
-- constants/deco_constants.asm DECOFLAG_*, for the two callers that name one.
Decorations.DECOFLAG_GOLD_TROPHY_DOLL = 43
Decorations.DECOFLAG_SILVER_TROPHY_DOLL = 44
-- DescribeDecoration's five arms and the wDeco* byte each one reads
-- (constants/script_constants.asm DECODESC_*, which is what the cache's
-- decorationOrder carries). Only the three that share
-- DecorationDesc_OrnamentOrConsole put a NAME in wStringBuffer3; the poster
-- arm picks a different script instead, and the giant ornament's says the same
-- thing whatever is standing there.
Decorations.DESC_SLOTS = {
DECODESC_POSTER = { slot = "poster" },
DECODESC_LEFT_DOLL = { slot = "leftOrnament", named = true },
DECODESC_RIGHT_DOLL = { slot = "rightOrnament", named = true },
DECODESC_BIG_DOLL = { slot = "bigDoll" },
DECODESC_CONSOLE = { slot = "console", named = true },
}
--------------------------------------------------------------------------
-- State
--------------------------------------------------------------------------
-- The eight wDeco* bytes, on the save. InitDecorations (called from
-- intro_menu.asm at New Game) is the two defaults below: the feathery bed and
-- the TOWN MAP poster are set up before the player has chosen anything, which
-- is why a new game's room already has a bed in it. Filling them in lazily
-- rather than in Save.newGame means an older save gets the same room.
function Decorations.state(save)
if type(save) ~= "table" then return {} end
local state = save.decorations
if not state then
state = { bed = 2, poster = 16 } -- DECO_FEATHERY_BED, DECO_TOWN_MAP
save.decorations = state
end
return state
end
-- ------------------------------------------------------------ the registry
--
-- The `decorations` registry (src/mods/Schemas.lua), one of the Gen 2-only
-- six: Red's bedroom has no PC decoration menu, so the name is gated under
-- Gen 1 and routed to data.gen2Decorations under Gen 2. src/mods/Builtins.lua
-- seeds it with the ATTRIBUTES rows above, engine-owned.
--
-- Ids are "deco:<n>", where n is the attribute row's index -- the DECO_* byte,
-- which is what wMenuSelection holds and what every caller passes. The cart's
-- decoration constants are a bare const_def block with no name table in the
-- ROM behind it, so there is nothing to spell them by; battle_anims addresses
-- its unnamed rows the same way ("subanim:<n>").
local DECO_ID_PREFIX = "deco:"
local registryRows = nil
function Decorations.idFor(decoId)
return DECO_ID_PREFIX .. tostring(decoId)
end
-- One read point for the attribute row, so the merged record reaches every
-- caller: name/owns/give/apply/visibility/tiles below all come through here,
-- as do src/ui/gen2/DecorationMenu.lua and the two DECO_* screens. Falls back
-- to the module's own table, which is what a headless test and a boot with no
-- loader get.
function Decorations.attributes(decoId)
if decoId == nil then return nil end
local merged = registryRows and registryRows[DECO_ID_PREFIX .. tostring(decoId)]
return merged or ATTRIBUTES[decoId]
end
-- vanilla registrations, engine-owned
function Decorations.registerInto(registry, _, owner)
local count = 0
for decoId, attr in pairs(ATTRIBUTES) do
registry:register(Decorations.idFor(decoId), attr, owner)
count = count + 1
end
return count
end
-- the merged table, held by reference; nil forgets it
function Decorations.useRegistry(data)
registryRows = data and data.gen2Decorations or nil
return registryRows ~= nil
end
-- GetDecoName: the display name, built from the type and the name column. The
-- four types that name a SPECIES read the mon's name out of the data table,
-- which is what `monName` is for; with no resolver the constant is already the
-- English name for all twenty-four of them.
function Decorations.name(decoId, monName)
local attr = Decorations.attributes(decoId)
if not attr then return "" end
local base = attr.name
if attr.type == BED then return base .. " BED" end
if attr.type == CARPET then return base .. " CARPET" end
local mon = (monName and monName(base)) or base
if attr.type == POSTER then return mon .. " POSTER" end
if attr.type == DOLL then return mon .. " DOLL" end
if attr.type == BIGDOLL then return "BIG " .. mon end
return base
end
--------------------------------------------------------------------------
-- Owning
--------------------------------------------------------------------------
-- DecorationFlagAction CHECK_FLAG. `events` is the src/world/gen2/Events.lua
-- bitfield the rest of the port keys by number.
function Decorations.owns(events, decoId)
local attr = Decorations.attributes(decoId)
if not (events and attr and attr.flag) then return false end
return events:get(attr.flag) and true or false
end
-- SetSpecificDecorationFlag, i.e. how a decoration is acquired at all: the
-- NORMAL_BOX / GORGEOUS_BOX trophies (engine/items/item_effects.asm), Mom's
-- doll purchases (engine/events/mom_phone.asm Mom_GiveItemOrDoll) and Mystery
-- Gift all end here. Named by DECOFLAG_*, because that is what every caller
-- passes.
function Decorations.giveFlag(events, decoFlag)
return Decorations.give(events, Decorations.idForFlag(decoFlag))
end
function Decorations.give(events, decoId)
local attr = Decorations.attributes(decoId)
if not (events and attr and attr.flag) then return false end
events:set(attr.flag, true)
return true
end
-- .FindOwnedDecos: the categories with at least one owned decoration, in the
-- .owned_pointers order. EXIT is not in that list -- DecoExitMenu is the
-- eighth .category_pointers row and is always on the menu -- so the caller
-- appends it, the way .FindCategoriesWithOwnedDecos appends its own 7.
function Decorations.ownedCategories(events)
local out = {}
for _, category in ipairs(Decorations.CATEGORIES) do
for _, id in ipairs(category.members) do
if Decorations.owns(events, id) then
out[#out + 1] = category
break
end
end
end
return out
end
-- FindOwnedDecosInCategory: every owned decoration in the category, then the
-- category's own PUT_IT_AWAY row, then row 0 (CANCEL). An empty category
-- answers an empty list and PopulateDecoCategoryMenu prints "There's nothing
-- to choose." instead of opening a menu.
function Decorations.rows(events, category)
local out = {}
for _, id in ipairs(category and category.members or {}) do
if Decorations.owns(events, id) then out[#out + 1] = id end
end
if #out == 0 then return out end
out[#out + 1] = category.id
out[#out + 1] = 0
return out
end
--------------------------------------------------------------------------
-- Placing
--------------------------------------------------------------------------
-- data/text/common_1.asm. Declared up here and formatted at the call site, so
-- Strings.source is what registers them.
local SET_UP = Strings.source("Set up the\n%s.")
local PUT_AWAY = Strings.source("Put away the\n%s.")
local NOTHING_TO_PUT_AWAY = Strings.source("There's nothing to\nput away.")
local ALREADY_SET_UP = Strings.source("That's already set\nup.")
local NOTHING_TO_CHOOSE = Strings.source("There's nothing to\nchoose.")
-- _PutAwayAndSetUpText is one text with a `para` in it, so it is two pages.
local PUT_AWAY_PAGE = Strings.source("Put away the\n%s")
local AND_SET_UP = Strings.source("and set up the\n%s.")
Decorations.NOTHING_TO_CHOOSE = NOTHING_TO_CHOOSE
-- DoDecorationAction2 for one menu row. Returns
-- changed wChangedDecorations: TRUE only when the room actually changed,
-- which is what makes the PC reload the map on the way out
-- pages the text to print, in order
-- `side` is "left" or "right" and only an ornament row reads it; a nil side on
-- an ornament row is DecoAction_AskWhichSide's cancel (`scf`), which changes
-- nothing and prints nothing.
function Decorations.apply(state, decoId, side, monName)
local attr = Decorations.attributes(decoId)
if not (state and attr) then return false, {} end
local action = attr.action and ACTIONS[attr.action]
-- DecoAction_nothing: row 0, the CANCEL row. `scf` and no text.
if not action then return false, {} end
local slot = action.slot
if action.ornament then
if side ~= "left" and side ~= "right" then return false, {} end
slot = (side == "right") and "rightOrnament" or "leftOrnament"
end
local current = state[slot] or 0
local name = function(id) return Decorations.name(id, monName) end
if action.put then
-- DecoAction_TryPutItAway clears the slot BEFORE it checks what was in it,
-- so putting away an empty slot still writes a 0 over the 0.
state[slot] = 0
if current == 0 then return false, { Strings(NOTHING_TO_PUT_AWAY) } end
-- DecoAction_PutItAway_Ornament names the thing that WAS out, not the row
-- the player picked (the row is the PUT IT AWAY row and has no name).
return true, { Strings(PUT_AWAY, name(current)) }
end
if current == decoId then
-- .alreadythere: carry, so nothing is written and nothing changed.
return false, { Strings(ALREADY_SET_UP) }
end
state[slot] = decoId
if current == 0 then
return true, { Strings(SET_UP, name(decoId)) }
end
return true, { Strings(PUT_AWAY_PAGE, name(current)),
Strings(AND_SET_UP, name(decoId)) }
end
-- DecoAction_SetItUp_Ornament .getwhichside: setting a doll up on one side
-- when the SAME doll is already on the other takes it off the other side --
-- there is only one of each. Called by the menu right after apply() on an
-- ornament row, because the cart does it inside the same action.
function Decorations.clearOtherSide(state, decoId, side)
if not (state and decoId and decoId ~= 0) then return end
local other = (side == "right") and "leftOrnament" or "rightOrnament"
if state[other] == decoId then state[other] = 0 end
end
--------------------------------------------------------------------------
-- Showing: the two map callbacks
--------------------------------------------------------------------------
-- ToggleDecorationsVisibility (the PLAYERS_HOUSE_2F MAPCALLBACK_NEWMAP). One
-- row per object slot: an empty slot SETS the object's event flag, which hides
-- it, and a filled one clears the flag and writes the decoration's sprite byte
-- into wVariableSprites.
--
-- Answers a plain list so the caller can apply it to a live world or a test
-- table; nothing here touches love or the map.
function Decorations.visibility(state)
local out = {}
for _, row in ipairs(Decorations.OBJECT_SLOTS) do
local decoId = state and state[row.slot] or 0
local attr = Decorations.attributes(decoId)
if decoId ~= 0 and attr then
out[#out + 1] = { sprite = row.sprite, byte = attr.sprite,
flag = row.flag, hidden = false }
else
out[#out + 1] = { sprite = row.sprite, flag = row.flag, hidden = true }
end
end
return out
end
-- ToggleMaptileDecorations (the MAPCALLBACK_TILES one). Its coordinates "work
-- the same way as for changeblock": PadCoords_de adds 4 to each and
-- GetBlockLocation halves them, so the pairs in the asm are CELL coordinates
-- and the block written is (x / 2, y / 2) -- the same halving
-- src/script/gen2/Vm.lua does for `changeblock`.
--
-- bed cell (0, 4) -> block (0, 2)
-- plant cell (7, 4) -> block (3, 2)
-- poster cell (6, 0) -> block (3, 0)
-- carpet cell (0, 0) -> block (0, 0), and cell (0, 2) -> block row (0, 1)
--
-- The carpet is the only one that writes more than one block: its top-left
-- block is the sprite byte and the row under it is +1, +2, +1. An empty slot
-- writes NOTHING (SetDecorationTile's `and a / ret z`), so the map keeps the
-- bare block it was loaded with.
function Decorations.tiles(state)
local out = {}
local function put(slot, blockX, blockY)
local attr = Decorations.attributes(state and state[slot])
if attr and attr.sprite and attr.sprite ~= 0 then
out[#out + 1] = { x = blockX, y = blockY, block = attr.sprite }
return attr.sprite
end
return nil
end
put("bed", 0, 2)
put("plant", 3, 2)
put("poster", 3, 0)
local carpet = put("carpet", 0, 0)
if carpet then
out[#out + 1] = { x = 0, y = 1, block = carpet + 1 }
out[#out + 1] = { x = 1, y = 1, block = carpet + 2 }
out[#out + 1] = { x = 2, y = 1, block = carpet + 1 }
end
return out
end
-- SetPosterVisibility, which rides along inside ToggleMaptileDecorations: the
-- bedroom's poster bg_event is BGEVENT_IFSET on EVENT_PLAYERS_ROOM_POSTER, so
-- a bare wall must not be readable at all.
function Decorations.posterVisible(state)
return ((state and state.poster) or 0) ~= 0
end
return Decorations