hud
Everything the player reads, and every button they press that is not the hero's own controls. The HUD reads match state and calls `Rules.send`; it never writes gold, lives or income, and it never touches another slice's entities.
game.hud — the Emberfront screen
Everything the player reads, and every button they press that is not the
hero's own controls. The HUD reads match state and calls Rules.send; it
never writes gold, lives or income, and it never touches another slice's
entities.
local Hud = require("game.hud")
Hud.start() -- register + show both screens, subscribe to the bus
Hud.update(dt) -- called every frame by the scene entrypoint
Hud.stop() -- remove the screens, unsubscribe, release the callback env
The two screens
| Screen | Layer | Carries |
|---|---|---|
emberfront_hud | 40 | both nexuses' lives, the treasury, the wave countdown, the SEND panel, the hero bar and ability chips, the battle log, the affordability toast |
emberfront_hud_cards | 45 | the how-to-play card and the victory / defeat screen — shown only when one of them applies |
Controls it owns
| Input | Effect |
|---|---|
1 2 3 4 | Rules.send("ember", <tier>) — the tier in that row of Rules.K.creeps |
| click a SEND row | the same send; unaffordable rows are dimmed and carry no click |
F1 | toggle the how-to-play card (Esc closes it) |
Enter | on the end card only — asks the match director for a new match |
The end card also carries a PLAY AGAIN button on the same path. Both are a
request, not an action: the HUD calls game.economy's restart() and the
director decides what a fresh match means. Either is ignored while the match is
still running, and pressing both several times still produces exactly one new
match.
| click TO THE LANE | close the card |
A send that cannot be paid for raises a two-second toast naming the shortfall rather than failing in silence.
What it reads from the other slices
Both are read through a guarded require, so the HUD degrades to defaults
instead of taking the match down when a neighbour is mid-edit.
| Source | Field | Meaning |
|---|---|---|
game.rules | state.lanes.<lane>.lives / .gold / .income / .sent | the two lane records |
game.rules | state.wave .phase .winner | match progress |
game.economy | waveClock | seconds remaining until the next wave tick (falls back to Rules.state.waveClock) |
game.hero | health maxHealth alive | the health bar |
game.hero | respawnIn | seconds until respawn, shown while alive is false |
game.hero | cooldowns.cleave .surge | seconds remaining, 0 = ready — the chip fills back up as this counts down |
Bus events it listens to
creepKilled creepLeaked sendQueued waveTick heroDied heroRespawned
feed the battle log. matchStarted opens the how-to card, matchOver fills the
screen with victory or defeat.
How it stays cheap
update builds one short signature string per frame from the values that would
change a pixel — lives, gold, income, wave, phase, the countdown to a tenth, hero
health to the point, each cooldown to a tenth, the length of the feed — and only
rebuilds the tree when that string moves. A still HUD costs a string compare.
The cards screen has its own signature and is hidden outright when neither card applies, so the full-screen overlay never sits invisible over the game.
Surviving a hot reload
A hot reload swaps every local in init.luau for a fresh one while the scene
entrypoint keeps ticking the same module table, so update would find itself
un-mounted and go dark for the rest of the session. It re-mounts instead. The
bus subscription list is parked on Rules.bus — the one table a reload of this
file leaves alone — so the new incarnation can unsubscribe the handlers the old
one registered rather than leaving two generations listening.
Scoped to this part · feeds back into the world's score.