-- lib-core.world-overlay-test v0.1.0 — 23 assertions across 11 spec items -- Exercises: spawn/list, kind-validation, anchor-validation, ttl expiry, -- despawn idempotency, entity-anchor position tracking, fixed-anchor snapshot, -- composite child count, alpha envelope (mid-hold + fade-out), entity-destroyed -- fallback, rise_px math verification. local world_overlay = require("lib-core.world-overlay") local composition = require("lib-core.composition") local T = engine.test local M = {} -- ---------- fixture: anchor entity template ---------- composition.define_template{ id = "test_anchor_entity", properties = { position = {x = 100, y = 200}, }, tags = {}, } -- Helper: reset all state between groups local function setup() world_overlay._test_clear_all() end function M.run_tests(_ctx) -- ================================================================== -- 1. spawn text-primitive with ttl + fixed anchor → handle + list() -- ================================================================== setup() local h1 = world_overlay.spawn{ kind = "text", text = "hello", color = 0xFFFFFFFF, anchor = {x = 10, y = 20}, ttl = 2.0, } T.assert(type(h1) == "table" and type(h1.id) == "number", "spawn returns handle with numeric id") local listed = world_overlay.list() T.equals(#listed, 1, "list() contains 1 overlay after spawn") -- ================================================================== -- 2. spawn with invalid kind → loud-error -- ================================================================== setup() local ok_bad_kind = pcall(world_overlay.spawn, { kind = "triangle", anchor = {x = 0, y = 0}, }) T.assert(not ok_bad_kind, "spawn with unknown kind raises an error") -- ================================================================== -- 3. spawn without anchor → loud-error -- ================================================================== setup() local ok_no_anchor = pcall(world_overlay.spawn, { kind = "rect", w = 10, h = 10, }) T.assert(not ok_no_anchor, "spawn without anchor raises an error") -- ================================================================== -- 4. spawn with ttl, update past ttl → list() empty -- ================================================================== setup() local saved_time_4 = engine.time engine.time = { now = function() return 0.0 end } world_overlay.spawn{ kind = "rect", w = 8, h = 8, color = 0xFFFFFFFF, anchor = {x = 0, y = 0}, ttl = 1.0, } T.equals(world_overlay._test_overlay_count(), 1, "overlay present before ttl expiry") engine.time = { now = function() return 5.0 end } world_overlay.update(0) engine.time = saved_time_4 T.equals(world_overlay._test_overlay_count(), 0, "overlay expired after update past ttl") -- ================================================================== -- 5. despawn(handle) removes; second despawn is idempotent (no error) -- ================================================================== setup() local h5 = world_overlay.spawn{ kind = "text", text = "bye", color = 0xFFFFFFFF, anchor = {x = 0, y = 0}, } world_overlay.despawn(h5) T.equals(world_overlay._test_overlay_count(), 0, "despawn removes overlay") local ok_second_despawn = pcall(world_overlay.despawn, h5) T.assert(ok_second_despawn, "second despawn is idempotent (no error)") -- ================================================================== -- 6. entity-anchor → _test_get_world_pos reads from entity after update -- ================================================================== setup() local ent6 = composition.create{ template = "test_anchor_entity" } local h6 = world_overlay.spawn{ kind = "text", text = "entity-follow", color = 0xFFFFFFFF, anchor = ent6, } world_overlay.update(0) local pos6 = world_overlay._test_get_world_pos(h6) T.assert(pos6 ~= nil, "entity-anchor: _test_get_world_pos is non-nil after update") T.equals(pos6.x, 100, "entity-anchor: position.x matches template default (100)") T.equals(pos6.y, 200, "entity-anchor: position.y matches template default (200)") composition.destroy(ent6) -- ================================================================== -- 7. fixed-anchor → _test_get_world_pos is static across updates -- ================================================================== setup() local h7 = world_overlay.spawn{ kind = "rect", w = 4, h = 4, anchor = {x = 55, y = 77}, } local pos7a = world_overlay._test_get_world_pos(h7) world_overlay.update(0) world_overlay.update(0) local pos7b = world_overlay._test_get_world_pos(h7) T.assert(pos7a ~= nil and pos7b ~= nil, "fixed-anchor: pos is non-nil before and after updates") T.equals(pos7b.x, 55, "fixed-anchor: x remains 55 after updates") T.equals(pos7b.y, 77, "fixed-anchor: y remains 77 after updates") -- ================================================================== -- 8. composite spawn → _test_child_count returns correct count -- ================================================================== setup() local h8 = world_overlay.spawn{ anchor = {x = 0, y = 0}, ttl = 2.0, children = { { kind = "rect", w = 16, h = 16, color = 0xFFFFFFFF }, { kind = "rect_lines", w = 16, h = 16, color = 0xFF0000FF }, { kind = "text", text = "+loot", color = 0xFFFF00FF }, }, } T.assert(world_overlay._test_child_count(h8) == 3, "composite overlay has child count 3") -- ================================================================== -- 9. alpha envelope: mid-hold ≈ 1.0; fade-out phase < 1.0 -- ================================================================== setup() -- spawn_time=0, ttl=2.0, default fade_in=0.15 (0.3s), fade_out=0.15 (0.3s) -- mid-hold: progress=0.5 → alpha should be 1.0 -- fade-out: progress=0.9 → alpha < 1.0 local saved_time_9 = engine.time engine.time = { now = function() return 0.0 end } local h9 = world_overlay.spawn{ kind = "rect", w = 8, h = 8, anchor = {x = 0, y = 0}, ttl = 2.0, } -- Mid-hold: now = 1.0 → progress = 0.5 → in hold zone engine.time = { now = function() return 1.0 end } local alpha_mid = world_overlay._test_get_alpha(h9) T.assert(alpha_mid ~= nil and alpha_mid >= 0.99, "alpha is ~1.0 in mid-hold phase (progress=0.5)") -- Fade-out: now = 1.9 → progress = 0.95 → in fade-out zone engine.time = { now = function() return 1.9 end } local alpha_fade = world_overlay._test_get_alpha(h9) T.assert(alpha_fade ~= nil and alpha_fade < 1.0, "alpha < 1.0 in fade-out phase (progress=0.95)") engine.time = saved_time_9 -- ================================================================== -- 10. entity destroyed mid-flight: last_pos falls back to snapshot -- ================================================================== setup() local ent10 = composition.create{ template = "test_anchor_entity", properties = { position = {x = 300, y = 400} }, } local h10 = world_overlay.spawn{ kind = "text", text = "dmg", color = 0xFF0000FF, anchor = ent10, ttl = 5.0, } -- First update — position snapshot recorded world_overlay.update(0) local pos_before = world_overlay._test_get_world_pos(h10) T.assert(pos_before ~= nil, "entity-anchor pos snapshotted before destroy") -- Destroy entity; next update should not crash and pos should be preserved composition.destroy(ent10) local ok_update = pcall(world_overlay.update, 0) T.assert(ok_update, "update does not crash when entity-anchor is destroyed") local pos_after = world_overlay._test_get_world_pos(h10) T.assert(pos_after ~= nil, "last_pos preserved after entity destroy") T.equals(pos_after.x, pos_before.x, "last_pos.x unchanged after entity destroy") -- ================================================================== -- 11. rise_px math: at 50% progress, rise = 0.5 * rise_px -- ================================================================== setup() -- spawn at t=0; ttl=2.0; rise_px=48 -- at t=1.0 → progress=0.5 → expected rise_offset = 0.5 * 48 = 24 px local saved_time_11 = engine.time engine.time = { now = function() return 0.0 end } local h11 = world_overlay.spawn{ kind = "rect", w = 8, h = 8, anchor = {x = 0, y = 0}, ttl = 2.0, rise_px = 48, } -- At progress=0.5, alpha should be 1.0 (hold zone) and rise = 24 px engine.time = { now = function() return 1.0 end } local alpha_11 = world_overlay._test_get_alpha(h11) T.assert(alpha_11 ~= nil and alpha_11 >= 0.99, "rise_px test: alpha=1.0 at progress=0.5 confirms hold zone") local rise_11 = world_overlay._test_get_rise(h11) T.assert(rise_11 ~= nil and math.abs(rise_11 - 24.0) < 0.01, "rise at 50% progress is rise_px * 0.5 (expected 24, got " .. tostring(rise_11) .. ")") engine.time = saved_time_11 end return M