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.
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):
Rules.state.wavegoes up by one.- Each lane is paid its own
incomethroughRules.grant. Rules.bus.emit("waveTick", { wave = n })— the creeps slice drains bothsendQueues 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.
Scoped to this part · feeds back into the world's score.