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.
270 lines
7.8 KiB
Markdown
270 lines
7.8 KiB
Markdown
# 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
|
|
|
|
<!-- topology:start (auto-generated; do not edit) -->
|
|
```mermaid
|
|
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
|
|
```
|
|
<!-- topology:end -->
|
|
|
|
## 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
|
|
|
|
```lua
|
|
{
|
|
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
|
|
|
|
```lua
|
|
{
|
|
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.
|
|
|
|
```lua
|
|
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.
|
|
|
|
```lua
|
|
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:
|
|
|
|
```lua
|
|
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
|
|
|
|
```lua
|
|
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)
|
|
|
|
```lua
|
|
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
|
|
```
|