Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld fb841593-1…

GameLoop

The rules of *First Light: Vagant*, on the `mount_loop` entity. It is the only writer of `fl.state`; every other slice reads that table or subscribes through `fl.on`.

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

GameLoop

The rules of First Light: Vagant, on the mount_loop entity. It is the only writer of fl.state; every other slice reads that table or subscribes through fl.on.

It owns the dawn clock, the lantern and its oil, the relight, the jasey going on and off and getting soaked and dried, the vagant's walking and wading speed, and the win/lose machine. It owns vagant.inputMap — the world's one control scheme, carrying keyboard, gamepad and touch on every control — and therefore the on-screen touch buttons the night is played with.

Public fields

  • inputMap: assetRef(inputMap) — the scheme to run. Defaults to vagant.
  • autoStart: bool — start the night without waiting for a title card to be taken. Off by default: the loop sits in title until something calls fl.beginRound(), which is what the screens slice's title card does.

The one scheme

The engine's CharacterController (on the vagant's body) and third_person_follow (on the CameraRig) each activate an input map of their own, defaulting to @builtin::inputMaps.default. Left alone that map is live beside this game's: two movement sticks, five touch buttons that do nothing, and hood fighting the default look for the right mouse button.

So at the frame the local player is ready, this component walks the avatar and camera-rig subtrees and points every component carrying an inputMap field at vagant.inputMap. That field is read once, when a controller arms, and none of them watch it afterwards — so the gameplay clock is held for ONE frame and let go, which is the hook they already register (CameraRig.driveWhileRunning) to hand their controls back and take them again, re-reading the field on the way in. It has to be a real frame: holding and releasing inside one call re-arms them on the map they already had.

Two things then still draw the engine's own buttons on a phone, and both are cleared once every controller is re-armed: whatever still holds @builtin::inputMaps.default at the scheme layer is released, and the compatibility action registry the touch overlay also draws from is displaced with an empty one. After that the overlay carries exactly this game's five buttons — Relight, Hood, Run, Climb, Pause — plus the movement stick and the look-drag zone.

Driving the night from another slice

screens and staging call these rather than writing fl.state:

  • fl.beginRound() — a title card was taken; run the staged opening.
  • fl.openingDone() — the opening beat has finished; hand the fen over. If nothing calls it, the night starts anyway after fl.TUNE.OPENING_MAX_SECONDS, so a missing cutscene can never strand a player on a dead screen.
  • fl.setMenu(open) — a card went up or came down. Holds the clock and the vagant while it is up.
  • fl.restart() — an end card was taken; walk out onto the fen again.

Phases

title → opening → play → won | lost, with paused sitting beside play. Every change fires phase(new, old).

Lamps

The fen slice dresses the seven anchors and hands each one over with fl.registerLamp. If nothing has by fl.TUNE.LAMP_REGISTER_GRACE seconds into the night, this registers the bare scene anchors instead and gives each a stand-in warm light when it is relit — a night is always winnable. A fen that shipped registers first and none of that runs.

Water

fl.waterDepthAt(x, z) answers how deep the vagant is standing. The fen slice hands the probe over with fl.provideWater(fn); until it does the answer is dry ground, which is a walkable fen rather than a broken one. Past TUNE.WET_DEPTH the wig soaks; past TUNE.WADE_DEPTH the walk drops to TUNE.WADE_SPEED and running is refused; past TUNE.DOUSE_DEPTH the lantern goes out.

Lifecycle

  • awake() — resolves the scheme, registers the loop driver on fl, subscribes to caught and hunt, and enters title.
  • update(dt) — the night, one frame at a time. Play mode only.
  • editorUpdate(dt) — hands the scheme back whenever the gameplay clock is held, so the pointer belongs to the editor's camera while authoring.
  • onDestroy() — releases the scheme and every subscription.

Usage

entity("mount_loop").component.get("GameLoop")   -- the mount the scene carries
fl.beginRound()                                  -- start a night

Interface

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

conforms to

zero/source-extract/v2

GameLoop — FIRST LIGHT: VAGANT. The rules. Owned by the `loop` slice. This is the game as a system: the one control scheme, the vagant's walk and wade, the lantern and its oil, the relight, the jasey going on and off and getting soaked and dried, the dawn clock, and the win/lose machine. It is the ONLY writer of `fl.state`. Everything else reads it, or listens on `fl.on`. What it does NOT do, on purpose: it does not build the fen, does not model the vagant, does not draw a screen and does not make a sound of its own — it asks for one by name through `fl.playSfx`. Those are other slices'. One structural decision worth knowing before reading: The engine's own CharacterController (on the vagant's body) and third_person_follow (on the CameraRig) each activate an input map of their own, defaulting to `@builtin::inputMaps.default`. Left alone, that map is live beside this game's, the player gets two movement sticks and five touch buttons that do nothing, and `hood` fights the default `look` for the right mouse button. So at `localPlayerReady` this component re-points every component carrying an `inputMap` field at `vagant.inputMap` and blips the gameplay clock, which is the hook those components already use to hand their controls back and take them again. After that exactly one scheme is live in this world.

Light

guard(where: string, fn: () → void

argtypedescription
wherestring
fn(

setPhase(p: string) → void

argtypedescription
pstring

ensureFallbackLamps( ) → void

The `fen` slice dresses the seven anchors and hands each one over with `fl.registerLamp`. If it has not by the time the night has been running for TUNE.LAMP_REGISTER_GRACE, this registers the bare anchors instead and gives each a small warm light of its own when it is relit — so a night is always winnable, even against a fen that has not been built yet. A fen that shipped registers first and none of this runs.

fallbackGlow(lamp: fl.Lamp, on: boolean) → void

The stand-in flame. Only ever put on a lamp this component registered itself: a lamp the `fen` slice handed over carries its own light, and adding a second one would double it.

argtypedescription
lampfl.Lamp
onboolean

lightLamp(lamp: fl.Lamp) → void

argtypedescription
lampfl.Lamp

resetRound( ) → void

onRelightHeld(_: any, _dt: any, active: boolean) → void

argtypedescription
_any
_dtany
activeboolean

onPause( ) → void

onBack( ) → void

onConfirm( ) → void

onRestart( ) → void

onHood( ) → void

armInput( ) → void

releaseInput( ) → void

repointOne(ent: any) → number

argtypedescription
entany

repointSubtree(root: any) → number

argtypedescription
rootany

holdClockForRearm( ) → void

repointDone( ) → boolean

Done when the vagant's own CharacterController is reading this world's scheme. Anything less is not done: the avatar's rig instantiates over several frames, so a pass that ran while the body still had nothing but its `Asset` component finds only the camera rig's behaviour and would otherwise latch there — leaving the walk on the engine's default map, a second movement stick on the touch overlay, and this component with no controller to write a walking speed onto.

standDownTheDefaultScheme( ) → void

One world, one scheme. `@builtin::inputMaps.default` is what the engine's controllers activate before they are told otherwise, and its holder count does not always come back to nothing on its own — a component destroyed without its release, or re-armed across a mode flip, leaves a claim behind. A map still live is a map still drawing: on a phone the player gets a Jump beside this game's Climb and a Sprint beside its Run, two pairs of buttons doing one thing each, in a portrait frame that has room for four. So once every controller in this world reads the vagant scheme, whatever is left holding the default one is released. Bounded, because a runaway count is a bug to see rather than a loop to spin in.

stillLive( ) → boolean

repointMaps( ) → void

bindLocalPlayer( ) → void

Called until it has everything, not once. The engine spawns the local player, and the player's body then instantiates its avatar rig over several frames, so the CharacterController is not there on the frame the player becomes ready.

playerPos( ) → void

driveSpeed( ) → void

updateWater(pos: { number }) → void

argtypedescription
pos{ number }

updatePool(pos: { number }) → void

argtypedescription
pos{ number }

updateOil(dt: number) → void

argtypedescription
dtnumber

updateWig(dt: number) → void

argtypedescription
dtnumber

updateRelight(dt: number, pos: { number }) → void

argtypedescription
dtnumber
pos{ number }

updateClock(dt: number) → void

argtypedescription
dtnumber

registerWithFl( ) → void

Registered on `fl` rather than exported from this component, so a caller reaches it with `fl.beginRound()` and never has to find this entity. It is re-registered whenever `fl` itself hot-reloads, because a reloaded module comes back with an empty registry and every one of those calls would warn and do nothing.

beginRound( ) → void

openingDone( ) → void

setMenu(open: boolean) → void

argtypedescription
openboolean

restart( ) → void

subscribeToFl( ) → void

Everything this component listens for on the shared bus, in one place. `fl` hot-reloads while the world is authored, and a reloaded module comes back with an empty listener table — so every subscription taken in `awake` is gone and nothing says so. This drops whatever is still held and takes the whole set again, which makes it safe to call on every reload edge.

awake( ) → void

update(dt: number) → void

argtypedescription
dtnumber

editorUpdate(_dt: number) → void

argtypedescription
_dtnumber

onDestroy( ) → void

beginRound( ) → void

openingDone( ) → void

setMenu(_o: boolean) → void

argtypedescription
_oboolean

restart( ) → 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.

22+items
▣
module · born here
❒asset
# fl One-paragraph description of what this module provides and who consumes it. ## Exports - `M.exampleFn(arg) -> result` — what it does and when to call it. - `M.exampleValue: type` — what value it carries. ## Usage Replace `<identity>` with the string `asset.create` returned — this module's source path with `/` written as `.` and each segment's type suffix dropped, the way a module at `/source/lib/thing.module` is `lib.thing` and one at `/source/kit.package/thing.module` is `kit.thing`. ```luau local M = require("<identity>") M.exampleFn("foo") ``` ## Notes - Anything callers need to know about THIS module: required runtime ordering, state ownership, side effects.
▲ 0↑ born
·
inputmap · born here
❒asset
# vagant.inputMap The one control scheme of *First Light: Vagant*. Eleven controls, each carrying keyboard, gamepad and touch, so the night is playable on a desktop, on a pad and under a thumb without a second scheme existing anywhere. | Control | Kind | Keyboard / mouse | Gamepad | Touch | |---|---|---|---|---| | `move` | axis2 | `WASD` | left stick | left virtual stick | | `look` | axis2 | mouse (pointer locked) | right stick | right-side drag | | `sprint` — "Run" | button | `Shift` (held) | left-stick click, `east` | Run | | `jump` — "Climb" | button | `Space` | `north` | Climb | | `relight` | button | `E` (held) | `south` (held) | Relight | | `hood` | button | `Q`, right mouse | `west`, left trigger | Hood | | `pause` | button | `Escape` | `start` | Pause | | `confirm` | button | `Enter`, left mouse | `south` | — the card's own control | | `back` | button | `Escape`, right mouse | `east` | — the card's own Back | | `navigate` | axis2 | arrow keys | d-pad | — tap the control | | `restart` — "Again" | button | `R` | `north` | — the card's own Again | ## Why the names are what they are `move`, `sprint`, `jump` and `look` are read BY NAME by the engine's own `CharacterController` and `third_person_follow`. Renaming `sprint` to `run` would leave the character with no way to run at all, so the control keeps the engine's name and shows the player its label, which is **Run**. ## Why four controls carry no touch class `confirm`, `back`, `navigate` and `restart` are menu controls, and on a phone a menu is driven by touching the control you want. An overlay button for them would be a second, unlabelled way to press the button already on screen, and it would spend one of the four slots the night itself is played from. The `screens` slice draws those controls at a thumb size; this map says so in the record, where a reader sees it. ## The four the night is played with `relight` and `hood` render first, largest, and adjacent — they are the two a thumb has to find in the dark without looking, so they share the `act` group and take the two lowest priority numbers. `Run` sits above them. `Pause` is last and smallest, deliberately away from the act cluster, so a thumb reaching for Relight in a hurry cannot stop the game instead. ## Shared bindings, and why none of them are ambiguous `Escape` is `pause` and `back`; `south` is `relight` and `confirm`; `north` is `jump` and `restart`. Each pair is answered in a different phase — `pause` only while the night runs, `back` only while a card is up, `restart` only from an end card — so one key opens the pause card and the same key closes it, and nothing has to be learned twice. `look`'s gate declares `rightButton = "off"` because `hood` binds the right mouse button as a game action: in this world a right-drag pulls the wig down rather than swinging the camera. ## Who runs it The `loop` slice's `GameLoop` component activates it, and points the avatar's CharacterController and the camera rig's behaviour at it too, so exactly one scheme is ever live. `screens` stands the whole `player` group down with its own `ui` map while a card is open.
▲ 0↑ born
·
inputbinding · born here
❒asset
# back Leave the card you are on. `Escape` or the right mouse button · `east`. It shares `Escape` with `pause` on purpose — `pause` is answered only while the night runs and `back` only while a card is up, so one key opens the pause card and the same key closes it. No touch class: a card carries its own Back control at a thumb size.
▲ 0↑ born
·
inputbinding · born here
❒asset
# confirm Take the focused thing on a card. The `screens` slice drives focus with `navigate` and activates on this. `Enter` or the left mouse button · `south`. Deliberately no touch class: on a phone a card is confirmed by tapping the card's own control, so an overlay button here would be a second, unlabelled way to press the button already on screen.
▲ 0↑ born
·
inputbinding · born here
❒asset
# hood Pull the jasey down over your head, or push it back off. A TAP toggles it: being hidden is a state you are in, not a button you hold for the length of a chase. `Q` or the right mouse button · `west` or the left trigger · the Hood button. Worn and dry it hides you completely; ditch water ruins it until a lit lamp dries it out.
▲ 0↑ born
·
inputbinding · born here
❒asset
# jump — "Climb" A scramble up a ditch bank. The fen is walked, not platformed: this exists so a vagant who has waded into a cut can get out of it again, which is the one place a walk alone can strand you. `Space` · `north` · the Climb button.
▲ 0↑ born
·
inputbinding · born here
❒asset
# look Where the vagant is looking. The `CameraRig`'s follow behaviour reads it, and the `vagant` slice's own camera work rides on the same control. Mouse (with the pointer locked) · right stick · a drag in the right-hand zone. Its gate declares `rightButton = "off"` because `hood` binds the right mouse button as a game action in this world: a right-drag pulls the wig down rather than swinging the camera.
▲ 0↑ born
·
inputbinding · born here
❒asset
# move Where the vagant walks, as a direction the player means rather than a set of keys. `CharacterController` reads it and projects it against the live camera basis, so "up" is always away from the camera however the rig is aimed. `WASD` · left stick · left-side virtual stick. A small radial deadzone keeps a resting thumb still; the reading is linear, so a half-pushed stick is a half-speed wade.
▲ 0↑ born
·
inputbinding · born here
❒asset
# navigate Move the focus around a card. The `screens` slice reads it and moves its brass focus ring; the arrow keys and the d-pad therefore reach every menu without a mouse. Arrow keys · d-pad. A large deadzone, because this is a discrete chooser and a resting thumb must not walk the focus down a list. No touch class: a thumb navigates a card by touching the control it wants.
▲ 0↑ born
·
inputbinding · born here
❒asset
# pause Stop the night. The `loop` slice holds the clock and the vagant, and the `screens` slice puts its pause card up. `Escape` · `start` · the Pause button — last in the touch band, smallest, and deliberately away from the act cluster, so a thumb reaching for Relight in a hurry cannot stop the game instead.
▲ 0↑ born
·
inputbinding · born here
❒asset
# relight The whole point of the night. HELD at a dark standard within reach: the `loop` slice counts the hold against `fl.TUNE.LAMP_RELIGHT_SECONDS`, spends a slug of oil when it completes, and the lamp comes back on. `E` · `south` · the Relight button — the lowest priority number in the scheme, so it renders first, largest, and beside Hood in the `act` cluster. It is the button a thumb has to find in the dark without looking.
▲ 0↑ born
·
inputbinding · born here
❒asset
# restart — "Again" Walk out onto the fen again. Answered only from an end card, which is why it shares `north` with `jump` without ever being ambiguous: a night that is over has nothing left to climb. `R` · `north`. No touch class: the end card draws its own **Again**.
▲ 0↑ born
·
inputbinding · born here
❒asset
# sprint — "Run" The vagant runs. It is HELD, not tapped, so letting go returns them to a walk on the drop frame; running is refused outright while wading. `Shift` · left-stick click or `east` · the Run button. The control is named `sprint` because `CharacterController` reads that name; the player only ever sees the label, which is **Run**.
▲ 0↑ born
backing path · GameLoop.component

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.