-- 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")