Files
sporel-lib-core.world-overlay/README.md
Calic 0ba22230ba initial: world-overlay v0.1.0 — transient world-space UI primitives
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.
2026-06-14 01:49:24 +02:00

7.8 KiB

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.y from 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