Files
last-hearth/tests/construction_spec.lua
2026-11-03 12:41:09 -05:00

151 lines
7.9 KiB
Lua
Executable File

-- Standalone, mocked engine only. arg[1] = construction.lua.
local factory = dofile(assert(arg[1], "construction.lua path required"))
local total = 0
local function test(name, fn)
local ok, err = pcall(fn)
assert(ok, name .. ": " .. tostring(err))
total = total + 1; print("ok " .. total .. " - " .. name)
end
local function harness()
local h = {world = {}, violations = 0, writes = 0, placements = 0, notices = 0, sounds = 0}
local function key(p) return p.x .. ":" .. p.y .. ":" .. p.z end
function h.put(x, y, z, name) h.world[key({x = x, y = y, z = z})] = name end
local api = {registered_nodes = {
air = {walkable = false, buildable_to = true}, ignore = {},
stone = {}, ["hearth:wall"] = {}, ["hearth:roof"] = {}, ["hearth:planks"] = {},
["hearth:foundation"] = {}, ["hearth:frame"] = {}, ["hearth:stairs"] = {},
["hearth:door"] = {}, ["hearth:door_open"] = {walkable = false},
["hearth:window"] = {}, ["hearth:vent"] = {},
water = {walkable = false, buildable_to = true, liquidtype = "source"},
leaves = {groups = {leaves = 1}},
}}
function api.get_node_or_nil(p)
local name = h.world[key(p)]
if name == false then return nil end
return {name = name or "air"}
end
function api.is_protected() return h.protected == true end
function api.record_protection_violation() h.violations = h.violations + 1 end
function api.get_meta() h.writes = h.writes + 1; error("Preview wrote metadata") end
function api.item_place(stack) h.placements = h.placements + 1; return stack, "placed" end
local hearth = {claim_allowed = function() return not h.denied end,
claim_owner_at = function(p) return h.owners and h.owners[key(p)] end,
notify = function() h.notices = h.notices + 1 end}
local player = {pos = {x = 0, y = 0, z = 0}}
function player:is_player() return true end
function player:get_pos() return self.pos end
function player:get_player_name() return "tester" end
function player:get_player_control() return {sneak = false} end
h.api, h.hearth, h.player = api, hearth, player
h.key = key
h.module = factory(hearth, api)
function h.preview(item) return h.module.preview({x = 0, y = 0, z = 0}, player, item or "hearth:wall") end
return h
end
test("supported previews do not mutate inventory, metadata, notifications or placement", function()
local h = harness(); h.put(0, -1, 0, "stone")
local stack = {get_name = function() return "hearth:wall" end,
take_item = function() error("Consumed preview") end}
for _ = 1, 20 do local ok, reason = h.preview(stack); assert(ok and reason == "ok") end
assert(h.violations == 0 and h.writes == 0 and h.placements == 0 and h.notices == 0)
end)
test("distance is six blocks inclusive", function()
local h = harness(); h.put(0, -1, 0, "stone")
h.player.pos.x = 6; assert(h.preview())
h.player.pos.x = 6.01; local ok, reason = h.preview(); assert(not ok and reason == "distance")
end)
test("protected and claim-denied preview never records a violation", function()
local h = harness(); h.put(0, -1, 0, "stone")
h.protected = true; local ok, reason = h.preview(); assert(not ok and reason == "protected")
h.protected, h.denied = false, true
ok, reason = h.preview(); assert(not ok and reason == "protected" and h.violations == 0)
assert(not h.module.allowed({x = 0, y = 0, z = 0}, h.player) and h.violations == 1)
end)
test("nil, ignore and unregistered target nodes fail unloaded", function()
for _, value in ipairs({false, "ignore", "missing"}) do
local h = harness(); h.put(0, 0, 0, value)
local ok, reason = h.preview(); assert(not ok and reason == "unloaded")
end
end)
test("occupied target is not buildable", function()
local h = harness(); h.put(0, 0, 0, "stone")
local ok, reason = h.preview(); assert(not ok and reason == "occupied")
end)
test("water replacement follows ordinary placement, but water is not an anchor", function()
local h = harness(); h.put(0, 0, 0, "water"); h.put(0, -1, 0, "water")
assert(h.preview("hearth:planks"))
local ok, reason = h.preview(); assert(not ok and reason == "support")
end)
test("unloaded support is distinguished from unsupported air or leaves", function()
local h = harness(); h.put(0, -1, 0, false)
local ok, reason = h.preview(); assert(not ok and reason == "unloaded")
h.put(0, -1, 0, "ignore"); ok, reason = h.preview(); assert(not ok and reason == "unloaded")
h.put(0, -1, 0, "leaves"); ok, reason = h.preview(); assert(not ok and reason == "support")
end)
test("two horizontal roof links allowed, third rejected", function()
local h = harness(); h.put(0, -1, 0, "stone"); h.put(0, 0, 0, "hearth:wall")
for x = 1, 3 do
local ok, reason = h.module.preview({x = x, y = 0, z = 0}, h.player, "hearth:roof")
assert(ok == (x <= 2)); if x == 3 then assert(reason == "support") end
h.put(x, 0, 0, "hearth:roof")
end
end)
test("sixteen-block vertical limit is identical in preview", function()
local h = harness(); h.put(0, -1, 0, "stone")
for y = 0, 16 do
h.player.pos.y = y
local ok, reason = h.module.preview({x = 0, y = y, z = 0}, h.player, "hearth:wall")
assert(ok == (y < 16)); if y == 16 then assert(reason == "support") end
h.put(0, y, 0, "hearth:wall")
end
end)
test("invalid coordinate, player and item inputs fail closed", function()
local h = harness()
for _, p in ipairs({{}, {x = 0/0, y = 0, z = 0}, {x = 0.1, y = 0, z = 0}}) do
local ok, reason = h.module.preview(p, h.player, "hearth:wall"); assert(not ok and reason == "invalid")
end
assert(not h.module.preview({x = 0, y = 0, z = 0}, nil, "hearth:wall"))
for _, item in ipairs({{}, 123, "missing"}) do
local ok, reason = h.preview(item); assert(not ok and reason == "item")
end
end)
test("legacy unsupported placement still notifies without feedback", function()
local h = harness(); local stack = {}
local p = {type = "node", under = {x = 0, y = 0, z = 0}, above = {x = 0, y = 1, z = 0}}
assert(h.module.place_building(stack, h.player, p) == stack)
assert(h.notices == 1 and h.placements == 0)
end)
test("optional feedback and denial sound replace only chat presentation", function()
local h = harness(); local messages = 0
h.hearth.feedback = function(_, _, kind) assert(kind == "warning"); messages = messages + 1 end
h.hearth.feedback_sound = function(name) assert(name == "hearth_denial"); h.sounds = h.sounds + 1 end
local p = {type = "node", under = {x = 0, y = 0, z = 0}, above = {x = 0, y = 1, z = 0}}
h.module.place_building({}, h.player, p)
assert(messages == 1 and h.sounds == 1 and h.notices == 0 and h.placements == 0)
end)
test("support removal refuses foundations that would leave dependent walls or roofs floating", function()
local h = harness(); h.put(0, -1, 0, "stone"); h.put(0, 0, 0, "hearth:wall")
assert(not h.module.can_dig({x = 0, y = -1, z = 0}, h.player))
assert(h.module.can_dig({x = 0, y = 0, z = 0}, h.player))
h.put(1, -1, 0, "stone"); h.put(1, 0, 0, "hearth:wall")
assert(h.module.can_dig({x = 0, y = -1, z = 0}, h.player), "alternate natural support should preserve the wall")
end)
test("support removal checks dependent structures and fails closed at unloaded edges", function()
local h = harness(); h.put(0, -1, 0, "stone"); h.put(0, 0, 0, "hearth:wall")
h.put(0, 1, 0, "hearth:roof")
assert(not h.module.can_dig({x = 0, y = 0, z = 0}, h.player))
h.put(2, 0, 0, false)
assert(not h.module.can_dig({x = 0, y = -1, z = 0}, h.player))
end)
test("placement and removal cannot create structural dependencies across homesteads", function()
local h = harness(); h.owners = {}
h.put(0, -1, 0, "stone"); h.owners[h.key({x=0,y=-1,z=0})] = "neighbor"
h.owners[h.key({x=0,y=0,z=0})] = "builder"
local ok, reason = h.preview("hearth:wall")
assert(not ok and reason == "support")
h.put(0, -1, 0, "stone"); h.put(0, 0, 0, "hearth:wall")
h.owners[h.key({x=0,y=0,z=0})] = "neighbor"
assert(not h.module.can_dig({x=0,y=-1,z=0}, h.player))
end)
print("PASS " .. total .. " construction checks")