KetylCore
The core slice's own shared surface, and nobody else's. `Conductor` and `KetylPilot` are the two halves of the rules of THE EIGHTH KETYL, and this is where they agree about what the controls read, where the ketyl is, which way it faces and whether it may move.
KetylCore
The core slice's own shared surface, and nobody else's. Conductor and
KetylPilot are the two halves of the rules of THE EIGHTH KETYL, and this is
where they agree about what the controls read, where the ketyl is, which way it
faces and whether it may move.
Other slices talk to core through the Still event bus, which is the contract
the whole game shares. Nothing outside the core slice reads this module.
local Core = require("game.KetylCore")
Why it exists at all
Editing a component hot-reloads it, and a hot-reload re-executes the chunk:
anything a component held in a file-scope local comes back at its default while
the instance keeps running, and anything it subscribed to in awake() gets
subscribed a second time. So the run's clocks and the registrations live here,
and the components register plain functions that this module calls.
Re-registering replaces; it never stacks.
Editing this file is the other direction, and it behaves differently than
the shape above suggests: the module table stays the same object, its functions
are replaced, and every local behind them starts over — so the run's clocks, the
body, and the record of who holds the controls all come back empty. The
dependent components' awake() does not run again to notice. Each of them
therefore watches for the one reading that says it happened — nobody holding the
controls, or an empty handler table — and re-arms or re-installs from update.
The same problem applies one level up — this file can be reloaded too, and the
run that subscribed is the only one holding the unsubscriber. modules.onUnload
is the answer: it hands the outgoing run a moment, with its own locals still in
scope, to drop everything it put on the Still bus and on the controls, just
before the next run replaces it. Within a run, wire() and arm() release
before they register, so calling either twice is a replacement too.
The surfaces
| Surface | Written by | Read by |
|---|---|---|
Core.run | Conductor | Conductor |
Core.body | KetylPilot | Conductor |
Core.input | the armed input map | KetylPilot |
Core.press | Conductor | the armed input map |
Core.handlers | Conductor | the bus forwarders |
Core.body.placeAt is the one write that goes the other way: the Conductor asks
for a teleport by putting a {x, y, z} there, and the pilot performs it and
clears it.
The numbers
Everything about the still, the ketyl's speed, its charge and its wetness is in
Still.module and read from there. What is here is the shape of the run —
the parts of the loop that are rules rather than fiction, which no other slice
needs to agree with: INTRO_SECONDS, GROWTH_GRACE, ASCEND_RADIUS,
ASCEND_TIME, PLATE_MARGIN, FACE_TURN, the hover bob, ENDING_LOCKOUT, and
the two warning thresholds.
The rules, as functions
approach · hoverY(n) · clampToPlate(x, z) · riserDistance(x, z, n) ·
arrivalPoint(n) · dischargeFalloff(dist, cosAngle) · controllable(phase) ·
invulnerable(now). No state, no side effects — a test can call them without a
running game.
dischargeFalloff is the shape of a discharge's arc written once. Core does not
apply it; the blennoma slice decides what was hit. It is here so that both
answers come from one function rather than two that drift.
Scoped to this part · feeds back into the world's score.