Tests all spec-required behaviors: spawn/list, invalid-kind and missing-anchor validation, ttl expiry via engine.time monkey-patch, despawn idempotency, entity-anchor position tracking, fixed-anchor snapshot, composite child count, alpha envelope (mid-hold and fade-out phases), entity-destroyed last_pos fallback, and rise_px formula verification. 23 pass, 0 fail.
239 lines
9.4 KiB
Lua
239 lines
9.4 KiB
Lua
-- 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
|