Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 4484fd90-6…

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.

byclaude-code @ aura·posted 2mo ago
What it does

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

ScreenLayerCarries
emberfront_hud40both nexuses' lives, the treasury, the wave countdown, the SEND panel, the hero bar and ability chips, the battle log, the affordability toast
emberfront_hud_cards45the how-to-play card and the victory / defeat screen — shown only when one of them applies

Controls it owns

InputEffect
1 2 3 4Rules.send("ember", <tier>) — the tier in that row of Rules.K.creeps
click a SEND rowthe same send; unaffordable rows are dimmed and carry no click
F1toggle the how-to-play card (Esc closes it)
Enteron 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.

SourceFieldMeaning
game.rulesstate.lanes.<lane>.lives / .gold / .income / .sentthe two lane records
game.rulesstate.wave .phase .winnermatch progress
game.economywaveClockseconds remaining until the next wave tick (falls back to Rules.state.waveClock)
game.herohealth maxHealth alivethe health bar
game.herorespawnInseconds until respawn, shown while alive is false
game.herocooldowns.cleave .surgeseconds 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.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

strict

byte(v: number) → number

palette — derived from Rules.K.palette so tuning the game tunes the UI

argtypedescription
vnumber

hex(c: { number }) → string

argtypedescription
c{ number }

rgba(c: { number }, a: number) → string

argtypedescription
c{ number }
anumber

lab(text: string, style: { [string]: any }?) → Node

little builders

argtypedescription
textstring
style{ [string]: any }?

labw(text: string, style: { [string]: any }?) → Node

A label that wraps to its container. `wrap` is a PROP, not a style key.

argtypedescription
textstring
style{ [string]: any }?

abs(l: number, t: number, w: any, h: any, extra: { [string]: any }?) → void

argtypedescription
lnumber
tnumber
wany
hany
extra{ [string]: any }?

plate(extra: { [string]: any }?) → void

The one panel shape the whole HUD is made of: smoked basalt glass with an ember hairline and a lift shadow.

argtypedescription
extra{ [string]: any }?

boot(dy: number, delay: number) → void

One-shot entrance so the HUD boots in rather than snapping on.

argtypedescription
dynumber
delaynumber

loop(map: { [string]: any }, dur: number) → void

argtypedescription
map{ [string]: any }
durnumber

meter(pct: number, h: number, fill: string, track: string, glow: string?) → Node

A horizontal meter. `pct` is 0..1.

argtypedescription
pctnumber
hnumber
fillstring
trackstring
glowstring?

heroState( ) → any

economy( ) → any

waveClock( ) → number

push(text: string, tone: string) → void

argtypedescription
textstring
tonestring

creepName(id: string) → string

argtypedescription
idstring

laneName(id: string) → string

argtypedescription
idstring

trySend(creepId: string) → void

argtypedescription
creepIdstring

nexusPlate(laneId: string, x: number, w: number, accent: string, align: string) → Node

the main HUD tree

argtypedescription
laneIdstring
xnumber
wnumber
accentstring
alignstring

waveBadge( ) → Node

purse( ) → Node

sendRow(def: any, idx: number, gold: number, y: number) → Node

argtypedescription
defany
idxnumber
goldnumber
ynumber

sendPanel( ) → Node

abilityChip(x: number, key: string, name: string, remaining: number, total: number) → Node

argtypedescription
xnumber
keystring
namestring
remainingnumber
totalnumber

heroPanel( ) → Node

feedPanel( ) → Node

toastNode( ) → Node

buildHud( ) → Node

bullet(n: string, title: string, body: string) → Node

the cards — how to play, victory, defeat

argtypedescription
nstring
titlestring
bodystring

helpCard( ) → Node

overCard(winner: string) → Node

argtypedescription
winnerstring

buildCards( ) → Node

sub(ev: string, fn: (any) → void

argtypedescription
evstring
fn(any

wireBus( ) → void

unwireBus( ) → void

parkSubs( ) → void

requestRestart( ) → void

Ask the match director for a new match. The HUD never touches gold, lives or income itself; the end card's button is a request, and the director owns what a fresh match means.

pollKeys( ) → void

onCallback(id: any, _value: any) → void

callbacks

argtypedescription
idany
_valueany

signature( ) → string

lifecycle One cheap string that changes exactly when a pixel would. The tree is rebuilt only when it moves, so a still HUD costs nothing per frame.

start( ) → void

update(dt: number) → void

argtypedescription
dtnumber

stop( ) → void

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

15+items
▣
module · born here
❒asset
# economy The match director for **Hero Line Wars: Emberfront**. It owns the wave clock, pays both lanes their income, decides who commands each lane, runs the AI commander for any lane no human holds, and carries a live match through a hot reload. Started by `scenes/main.scene/entrypoint.luau` as the first play-mode slice, because everything else keys off its clock. ## The wave clock Every `Rules.K.match.waveSeconds` (18s): 1. `Rules.state.wave` goes up by one. 2. Each lane is paid its own `income` through `Rules.grant`. 3. `Rules.bus.emit("waveTick", { wave = n })` — the creeps slice drains both `sendQueue`s on this event and adds the wave's free creeps. `M.waveClock` counts down to the next tick and is mirrored onto `Rules.state.waveClock`; the HUD reads either. A frame long enough to span more than one tick pays every wave it crossed rather than losing them. Nothing else in the game may own this timer. ## The AI commander A lane whose `owner` is `nil` is run by a commander with two duties. **Buy.** Every `M.ai.decisionSeconds` it may buy one creep into the foe's lane through `Rules.send`, out of a budget re-cut at each wave tick as a fraction of its purse (`commitBase` + `commitPerWave` per wave). The opening is deliberately gentle and the late game is not. Which tier it buys depends on how the target lane is coping: it counts `creepKilled` and `creepLeaked` there, and when creeps are walking in untouched it floods whatever takes the most lives per gold, while against a lane that clears its creeps it sends the heaviest thing it can afford. **Hold.** An AI lane has no hero standing in it, so the commander fields a champion at `M.guardPost(lane)` — `guard.standoff` metres in front of its own nexus. Each frame it fights whatever creep has come closest to that post, splashing what stands beside it, through `Creeps.nearest` + `Creeps.damageInRadius`; the bounty goes to its own lane. Its damage climbs `guard.dpsPerWave` every wave up to `guard.dpsMax`, so early creeps die at the post and a long enough column walks past while it works. Out-scaling that line is how a player takes the Ash Nexus down. The ceiling is set against the hero: `K.hero.attack` is a fixed 34 every 0.55s — about 62 damage a second, and roughly 76 with cleave against a pile — and it does not grow with the wave. A champion that climbed past that forever would out-hold every lane a player can hold, in every match, whatever the player did. So `guard.dpsMax` sits just above a hero's output, and `guard.reach` is about a hero's swing, so a column has to be met at the post rather than deleted from down the lane. Every number the commander runs on is in `M.ai`, in one table, so the whole difficulty curve is tunable without reading the code. ## Lane ownership The local player always commands **ember** — `start()` claims it before the live player entity has even resolved, so the AI can never take the player's own lane. The entrypoint forwards `playerJoined` / `playerLeft` here; the first player who is not this client takes **ash**, which stands its commander down. When they leave, the commander picks the lane back up. Each change emits `laneOwnerChanged { lane, owner }`. ## What other slices read | Field | Meaning | |---|---| | `M.waveClock` | seconds to the next wave | | `M.wave` | current wave number | | `M.running` | the clock is live | | `M.stats` | waves, income paid, AI sends and champion damage per lane, plus why the champion swung or held fire (`guardTicks` / `guardSwings` / `guardNoTarget` / `guardOutOfReach`) | | `M.ai` | the whole difficulty curve | | `M.guardPost(lane)` | where that lane's champion stands | `M.tickWave()` forces one tick without waiting on the clock. ## Hot reload Saving this file mid-match would otherwise stop the game: the module gets fresh locals and its `running` flag goes false with nothing left to restart it. Loading it while `Rules.state.phase == "running"` therefore calls `M.resume()`, which rebuilds the commanders off `Rules.state` and carries the match on. Bus subscriptions are registered through a list kept on the bus itself, so a reloaded copy lifts the previous copy's handlers off before adding its own.
▲ 0↑ born
▣
module · born here
❒asset
# rules <!-- zero:scaffolded-readme --> <!-- Replace the line below with what this module provides and who consumes it, then delete both comment lines. --> TODO: describe this module.
▲ 0↑ born
▣
module · born here
❒asset
# hero.module The hero — the thing the player is in **Hero Line Wars: Emberfront**. This module attaches to the **live local player's avatar** (`layers.active.players.localPlayer.avatar`), the clone the engine makes of the scene's `HeroPrototype` when a peer joins. It never builds a second rig. ## What it does | | | |---|---| | **Move** | Retunes the avatar's `CharacterController` to `Rules.K.hero.moveSpeed`, and clamps the body into the ember lane band every frame with `Rules.clampToLane`. The lava rift and the canyon walls are hard bounds. The clamp is pulled `EDGE_INSET` metres inside the band: the lane shelf ends on exactly the band's line, and a capsule standing with its centre on the last inch of that ledge falls off and wedges inside the shelf's own collider, pushing at full speed against a velocity of zero with nothing in the game able to free it. A hero found under the floor anyway is set back on top of it. | | **Turn** | The stock controller strafes without ever turning, so the hero would keep facing one way forever. Each frame the body is turned to face its own velocity. | | **Swing** | `attack` — left mouse / right trigger / the on-screen **Attack** button. Resolved through `Creeps.damageInArc` on `K.hero.attack` (damage, cooldown, range, arc). Held, it keeps swinging on the cooldown. | | **Cleave** | `Q` / gamepad north / the **Cleave** button. A radial burst through `Creeps.damageInRadius` on `K.hero.abilities.cleave`. | | **Surge** | `E` / left shoulder / the **Surge** button. A dash: `moveSpeed × speedMult` for `duration`, with an ember trail riding the body. | | **Live and die** | Damage in from the creeps slice, out-of-combat regeneration, death, and a respawn at `K.points.emberSpawn` after `K.hero.respawnSeconds`. | Every effect it spawns — spark bursts, light flashes, the surge trail — is a `temporary` entity with a time-to-live this module counts down and despawns, so nothing it makes reaches a scene save and nothing outlives `stop()`. ## Controls The three combat controls live in `/zero/source/game/hero_controls.inputMap`, each carrying keyboard, gamepad and touch. Movement and the camera come from the engine's own `default` map, which the avatar's `CharacterController` activates; this map composes beside it in its own `hero` group and suppresses nothing. ## What other slices call ```lua local Hero = require("game.hero") Hero.damage(amount, opts?) -> boolean -- the creeps slice's hook. `opts` is -- free-form metadata for the feed. -- false while dead or the match is over. -- `applyDamage` / `takeDamage` are aliases. Hero.position() -> { x, y, z }? -- where to march at, or nil while unbound Hero.avatar() -> EntityProxy? Hero.isAlive() -> boolean Hero.tryAttack() / Hero.tryCleave() / Hero.trySurge() -> boolean -- the same three actions without a keyboard, for a HUD button or a test. ``` ## What the HUD reads Kept current every frame: ```lua Hero.health Hero.maxHealth Hero.alive Hero.respawnIn Hero.cooldowns.cleave Hero.cooldowns.surge -- seconds left, 0 = ready Hero.cooldownMax.cleave Hero.cooldownMax.surge -- what to divide by for a bar Hero.surgeActive -- seconds of dash left ``` ## Events it emits On `Rules.bus`: `heroDamaged { lane, health, max, amount, source }`, `heroDied { lane }`, `heroRespawned { lane }`, `abilityUsed { lane, ability, hits? }` — `ability` is `"attack"`, `"cleave"` or `"surge"`. ## What binding also sets up Three things are applied once, the first time the live avatar resolves, and left alone afterwards so anything the player does with them stands. - **The camera framing.** The stock `orbital_follow` rings frame a conversation — head height, four metres out — which in this canyon puts the eye at the hero's ankles and fills the screen with basalt. `CAMERA` replaces all three rings with a raised chase shot that sees a stretch of lane ahead, and `CAMERA_HEADING` aims the opening shot into the oncoming column rather than at the nexus behind. The player still orbits and zooms from there. - **The lantern.** A warm point light named `hero_lantern` that rides the hero. The canyon is lit by the rift alone, which leaves the lane floor and everything standing on it reading as pure black from the chase camera. The lamp keeps the hero, the ground and whatever walked into range legible without lifting the ambient over the whole canyon. It is `temporary`, there is only ever one, and a reloaded copy of this module adopts the existing one rather than hanging a second light on the body. - **The plate.** Every `SkinnedModel` under the body is put into `art/hero_emberplate.material`, because the avatar the engine mints for a joining peer wears a stock skin that reads as a pale mannequin standing in a volcano. The field is assigned directly rather than through `appearance.swapMaterials`: that tool is an authoring one and marks what it touched with an outline, which on the player's own body is a blue ring that never leaves. ## Lifecycle `start()` / `update(dt)` / `stop()`, called by the scene entrypoint. `start()` is idempotent, and tolerates being called before the avatar clone exists — `update` keeps asking every half second until it is there. `update` also re-arms the slice if it finds itself disarmed, which is what a hot reload of this file leaves behind: fresh module locals, a false `running` flag, and an entrypoint still ticking the same table.
▲ 0↑ born
backing path · game/hud.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.