Skip to main content

Lifecycle hooks

The script has no event bus. Each "when X happens" has one place that already decides it, and that is where a reaction goes. This page is the map.

I want to run code when…Hook intoNotes
a run startsbuildRun in index.tsAfter ts exists and before the scheduler starts. Reset per-run state here (gFever.reset(), lorcanaReset() are the models).
a run endsendRun in index.tsThe only place that dismantles the world; runs on the thread that owns it.
a stop is requestedts.isRunningNot a hook: long loops poll it, sleep returns early on it, the touch wrappers refuse on it. A task body that wants the run to end calls requestStop().
a round is about to startopenRound in play.tsCalled from the pre-round screen with the items set; round.start goes out here with the frozen roundSettings.
the whistle — the last moment a setting can shape this roundquickBarApplyPending in quickbar.tsHeld-back settings land; the walk to the board follows.
the round is overwatchRoundEnd in play.tsStamps roundEndedAt and emits round.over.
the tally has been readafter finishRoundStats in taskPlayGameQuickround.end goes out with the figures.
a screen is recognisedgPages.subscribe({...}) in pageHandlers.tsHandle a page. Every reaction to a page is one of these; pick the band carefully.
a fever starts or endsgFever.subscribe({...})Below.
the Lorcana transformation happensgLorcana in lorcana.tsThe watcher is modelled on fever.ts; the skill file reads gLorcana.transformed.
a skill is about to fire / has firedbeforeActivate / afterActivate on the handlerAdd a skill. orderPaths is the one hook that runs while the skill is not activating.
a chain was just linkedpopBubblesAfterChain on the handlerOnly for a skill that claims the bubbles.
a setting changes on a running scripta case in quickBarApplyOneAdd a setting. Both pages go through it.
the run is paused from the striponPause() in quickbar.tsEvaluated by the host after it parks the engine. The only hook allowed to tap while paused.
the game app has restartedawaitAppUp in appLifecycle.ts, then observe.startupPhaseThe root warning is the first page after a restart; the observe handler flips isStartupPhase.
a scheduled interval elapsesa row in runTaskTableAdd a scheduled task.
a failure should collect an issue reportReportTriggers in report.tsKeyed by the Log event the failure already writes. Logging and events.

Run start and end

app.gap.Tsum/src/index.ts
loading...

Anything created per run — a watcher's state, a counter, a cached reading — is reset in buildRun, not lazily on first use, so the round that just ended has nothing to say about the next run. Anything that must be undone goes in endRun, in the order the parts can safely go: flags, then ts.isRunning, then the scheduler, then the router, then ts itself.

Stop

requestStop() raises every flag and returns; stop() calls it and then waits for the run to hand back. A task body may call requestStop() — the Max Round Duration cap does — and must not call stop(), which would wait for itself.

app.gap.Tsum/src/index.ts
loading...

Round start and end

app.gap.Tsum/src/play.ts
loading...

openRound is gated so that one round is announced once: the pre-round screen's handler calls it with the items in frame, and taskPlayGameQuick calls it again once the board is up for the round that never passed that screen. If you need per-round state, initialise it just after openRound() in taskPlayGameQuick (the loop's locals are the model) and read it off roundSettings if it depends on a setting — that copy is frozen at the whistle, so a mid-round Quick Bar change is the next round's news.

Fever

fever.ts reads the fever off its own probes, debounces two agreeing readings before the state flips, and refuses an unreadable frame as evidence for up to two seconds (a burst animation covers the HUD; a fever underneath runs on). Subscribe with an id, a what and a handler:

gFever.subscribe({
id: 'mySkill.feverEdge',
what: 'Re-arm the choreography when a fever ends.',
handler: function(event) {
// event.active: true at a start, false at an end
// event.lastedMs: how long the state it replaced had been up
// event.page: the page the change was seen on
// this: the Tsum
}
});
app.gap.Tsum/src/globals.d.ts
loading...

Subscriptions are registrable at load time (the watcher is a global like gPages), run one at a time in registration order, and a subscriber that throws does not cost the others their turn. Readings arrive from record.feverTime in pageHandlers.ts on every look the router makes, which is what makes the events fire on their own during a round.

For a one-off look, ts.isFeverTime() answers with no memory; pass it a frame you already hold and it costs only the probe read.

Skill activation

useSkill in skillCore.ts is the one caller: gauge read, fever hold-off, the activation tap(s), then the handler's afterActivate. The play loop runs it in a while after each link batch, so a skill that is ready is never made to wait behind a blind sweep. maybeAutoTapSkill is the cheaper path inside a link batch for skills that declare bareTapActivates or overloadProbe.

Adding a new kind of watcher

For a mode of the board rather than a screen — fever is the worked example, Formal Beast's gauge the second — the pattern is: a probe table in data.ts, a watcher modelled on fever.ts, and whatever feeds it readings: a record subscription in pageHandlers.ts when everything has to know, or the one caller when only one does. Types go in globals.d.ts. Then prove it offline by driving the built bundle over a real frame under the tools/runtime/ shim.