Generic world-space transient UI lib. Spawns text, icon, rect, rect_lines, and composite overlays anchored to entities or fixed world positions. TTL-based expiry with fade-in/hold/fade-out alpha envelope and optional upward rise. Camera-aware world-to-screen transform. Entity-destroyed fallback keeps last_pos snapshot. Five test backdoors for headless testing.
lib-core.world-overlay
Generic world-space transient UI layer. Spawns short-lived overlays (text, icons, rects, composites) anchored to world entities or fixed world-space positions. Camera-aware: converts world coordinates to screen coordinates each frame. Domain-free — intended for combat-lib (damage numbers), interaction-lib (mouseover prompts), and module-level pickup confirmations.
Version: 0.1.0 Lib-ID: lib-core.world-overlay Requires: engine.render (draw_text, draw_rect, draw_rect_lines, draw_sprite_transform), engine.entity (get_property via entity handles), engine.time (optional, for TTL/animation) Tags: world-overlay, ui, hud, transient, world-space, animation, camera
Topology
graph LR
this["lib-core.world-overlay"]
engine_render["engine.render.*"]
engine_entity["engine.entity (via handle.get_property)"]
engine_time["engine.time.now (optional)"]
this --> engine_render
this -.->|entity-anchor| engine_entity
this -.->|optional| engine_time
Scope (v0.1.0)
v0.1 ships:
- Content types: text, icon, rect, rect_lines, composite (children array)
- Lifecycle: TTL with automatic expiry OR manual-dismiss (no TTL)
- Anchor modes: entity-follow (reads
position.x/position.yfrom entity each frame) or fixed-position (world-space{x, y}snapshotted at spawn) - Animation: fade-in / hold / fade-out alpha envelope + optional upward rise (pixels over TTL duration)
- Camera transform: world → screen conversion via camera handle each render
Intentional non-goals (deferred):
- Predicate-based lifecycle (despawn when condition becomes true)
- Auto-layout for composite children (stacking, centering)
- World-space speech-bubble tails and pointer geometry
- Per-primitive animation curves (easing functions)
Spec Schema
Primitive spec
{
kind = "text" | "icon" | "rect" | "rect_lines",
anchor = entity_handle | {x = number, y = number},
-- kind-specific fields:
-- text: text (string), color (RGBA u32), font_size (number)
-- icon: atlas (string), uv = {x,y,w,h}, w (number), h (number)
-- rect: w (number), h (number), color (RGBA u32)
-- rect_lines: w (number), h (number), color (RGBA u32), thickness (number)
-- optional for all:
offset = {x = number, y = number}, -- screen-space offset from anchor
ttl = number, -- seconds; nil = manual-dismiss
fade_in_pct = number, -- fraction of ttl for fade-in (default 0.15)
fade_out_pct = number, -- fraction of ttl for fade-out (default 0.15)
rise_px = number, -- upward screen-px movement over ttl (default 0)
}
Composite spec
{
anchor = entity_handle | {x = number, y = number},
children = { primitive_spec, ... }, -- non-empty array; no anchor on children
-- optional lifecycle fields same as primitive (ttl, fade_in_pct, etc.)
}
Children inherit the composite's anchor position and animation envelope.
Each child may have its own offset for relative positioning.
Anchor Modes
Entity-follow
Pass an engine entity handle as anchor. Each update(dt) call reads
position.x and position.y from the entity via get_property. If the
entity is destroyed between frames, pcall absorbs the error and the
overlay keeps rendering at the last known position until it expires or is
manually despawned.
local overlay = world_overlay.spawn{
kind = "text",
text = "-42",
color = 0xFF4040FF,
anchor = some_entity, -- entity handle
ttl = 1.2,
}
Fixed-position
Pass {x = number, y = number} as anchor. The world-space position is
snapshotted at spawn time and never updated.
local overlay = world_overlay.spawn{
kind = "rect",
w = 32, h = 32,
color = 0x00FF00AA,
anchor = {x = 512, y = 300},
ttl = 0.8,
}
Animation Model
TTL overlays follow a three-phase alpha envelope:
alpha
1 | _______________
| / \
| / \
0 |_____/___________________\____
^ ^
fade_in_pct * ttl (1 - fade_out_pct) * ttl
- fade-in phase
[0, fade_in_pct): alpha ramps 0 → 1 - hold phase
[fade_in_pct, 1-fade_out_pct]: alpha = 1.0 - fade-out phase
(1-fade_out_pct, 1.0]: alpha ramps 1 → 0
rise_px moves the overlay upward by progress * rise_px screen pixels over
the full TTL duration. At 50% TTL with rise_px = 48, the overlay is 24 px
above its anchor screen position.
Overlays without TTL (ttl = nil) always render at alpha 1.0, no rise.
Camera Handle Convention
M.render(camera) expects:
camera = {
target_x = number, -- world-space center of view (x)
target_y = number, -- world-space center of view (y)
zoom = number, -- pixels-per-world-unit scale (default 1.0)
offset_x = number, -- screen-space offset of camera center (x)
offset_y = number, -- screen-space offset of camera center (y)
}
World-to-screen transform:
screen_x = (world_x - camera.target_x) * zoom + camera.offset_x
screen_y = (world_y - camera.target_y) * zoom + camera.offset_y
API
world_overlay.spawn(spec) -> handle
Syntax: world_overlay.spawn(spec: table) -> { id: number }
Validates spec, records anchor position (fixed) or defers to first update
(entity), and inserts overlay into the active list. Returns an opaque handle
for despawn. Loud-error on missing anchor, unknown kind, or missing
kind-required fields.
world_overlay.despawn(handle)
Syntax: world_overlay.despawn(handle: table) -> void
Removes the overlay identified by handle. Idempotent — calling with an
already-expired or non-existent handle is a no-op.
world_overlay.update(dt)
Syntax: world_overlay.update(dt: number) -> void
Must be called each game-update frame. Refreshes entity-anchor positions, expires overlays whose TTL has elapsed.
world_overlay.render(camera)
Syntax: world_overlay.render(camera: table) -> void
Must be called each render frame (inside the engine render phase). Converts
world positions to screen coordinates using the camera handle, computes
alpha + rise, and draws all active overlays. No-op if camera is nil.
world_overlay.list() -> spec[]
Syntax: world_overlay.list() -> table
Returns an array of spec tables for all currently active overlays. Intended for debug/introspection.
Glue-Pattern
Combat-style damage number
local world_overlay = require("lib-core.world-overlay")
-- In combat hit handler:
local function show_damage(entity, amount)
world_overlay.spawn{
kind = "text",
text = tostring(amount),
color = 0xFF3030FF,
font_size = 18,
anchor = entity,
ttl = 1.2,
rise_px = 32,
fade_out_pct = 0.4,
}
end
-- Module update/render wiring:
function M.update(dt)
world_overlay.update(dt)
end
function M.render()
-- draw world first
world_overlay.render(camera)
end
Pickup confirmation (fixed position + composite)
local world_overlay = require("lib-core.world-overlay")
local function show_pickup(world_x, world_y, icon_atlas, icon_uv, label)
world_overlay.spawn{
anchor = {x = world_x, y = world_y},
ttl = 1.5,
rise_px = 20,
children = {
{
kind = "icon",
atlas = icon_atlas,
uv = icon_uv,
w = 16, h = 16,
offset = {x = -8, y = -8},
},
{
kind = "text",
text = label,
color = 0xFFFFAAFF,
font_size = 14,
offset = {x = 12, y = -4},
},
},
}
end