Skip to documentation
CranberryClock.Ideas / Systems / Design

GAME TEMPLATE / v0.1.0

AfterglowIntegration guide

Install Afterglow, connect it to your project, and understand the integration boundaries.

Run the download

Extract afterglow-template-0.1.0.tgz, open a terminal in the extracted folder, and run:

bash
node server.mjs

Open the localhost address printed by the server. Node.js 22 or later is recommended. npm start runs the same server. Use PORT=4180 node server.mjs to change the port on a POSIX shell. You can also place the whole extracted folder on an ordinary static web host. Keep its relative paths intact. Use HTTP; directly opening index.html as a file: URL does not reliably support browser module loading.

The source repository assembles the portable runtime during its root npm run build. The authored template omits generated vendor/, packages/, and lib/ copies. The build copies official Three.js, Signal, Flux and Cranberry Forge's shared createStage helper. Those copies and the authored portable server are included in the downloadable archive. No other Cranberry Forge package is required.

How the template is divided

File Responsibility
game.js Renderer-free game state, encounter schedule, movement, dash, exact impact checks, lifecycle and score
scene.js Original Three.js temple and keeper, Signal warning instances, Flux dash trail, visual cleanup
app.js DOM HUD, input, camera-relative movement mapping, dialogs, optional best score and lifecycle
game.css / index.html Responsive presentation, touch controls, accessible labels, import map
lib/scene.js Shared native Three.js renderer, lighting pipeline, resize handling and frame loop
packages/signal / packages/flux Independent MIT toolkits with their own API documentation and declarations
server.mjs Small local static web server; not an application backend

There are no HTTP game endpoints. The public API is a direct JavaScript controller, suitable for an existing Three.js game loop, a unit test or a separate frontend. A backend would need its own authoritative simulation and protocol.

Controller tutorial

javascript
import { AfterglowGame, attackContains } from "./game.js";

const game = new AfterglowGame();
game.start();

// World coordinates: +X is right, +Z is forward on the XZ floor.
// The shipped app converts camera-relative keyboard/touch input to these axes.
const events = game.step(1 / 60, { x: 1, z: 0, dash: false });
const view = game.view;

for (const event of events) {
  if (event.type === "impact" && event.damaged) {
    console.log("Light remaining:", view.health);
  }
}

// Use this exact shared query when adding another gameplay reaction.
const warning = view.attacks[0];
if (warning) console.log(attackContains(warning, view.position));

game.pause(); // step() now leaves all gameplay state unchanged
game.resume();
game.restart(); // returns a fresh READY view; call start() to begin again

game.view is a detached structured clone. Editing it does not move the player or change the controller. This API does not accept arbitrary state restoration; it avoids silently trusting imported combat state. Restarting is explicit. The optional best score is the only persisted value.

Public API

API Result
new AfterglowGame(options?) New controller in ready phase; validates configuration
.options Frozen normalized configuration
.phase ready, playing, paused, won or lost
.view Detached current state, including attacks, position, score and clocks
.start() true only when transitioning from ready to playing
.pause() / .resume() true only when performing the requested valid transition
.restart() Resets all attacks, timers, position, score and lifecycle; returns the ready view
.step(dt, input?) Advances playing state and returns the events created during this call
attackContains(attack, {x,z}) Inclusive point-in-footprint check with the same Y rotation and XZ shape semantics as Signal

dt must be finite in [0, 0.25] seconds. input.x and input.z default to zero and must be finite in [-1,1]. Diagonal input is normalized. input.dash defaults to false and must be a boolean. Send it as a one-frame press request. A sustained true intentionally requests another dash as soon as the cooldown ends; the shipped keyboard adapter ignores key-repeat and the touch adapter uses button activation.

The controller advances in at most 1/120-second movement slices and splits at warning starts, impacts, and dash ends. All movement occurs before checking an impact at the new time. Protection is exclusive at its end: a dash that has just reached zero no longer blocks that instant's impact. A call with dt = 0, a paused controller or a terminal phase performs no gameplay work. A frame hitch is capped by the supplied stage helper rather than fast-forwarding the game while a tab is hidden.

view includes phase, time, duration, round, health, position, normalized facing, dashRemaining, cooldown, invulnerability, attacks, resolved, avoided, hits and score. Positions use world XZ units; clocks use seconds. attacks contain id, round, label, world x/z, Y rotation in radians, armedAt, impactAt, duration, and Signal options.

Returned events

Type Extra fields Meaning
armed attack A warning locked its location and facing
dash position A valid dash request began
impact attack, inside, damaged, dashed One footprint resolved exactly once; dashed means the point was inside while dash protection was active
round round The next round began
won / lost A terminal transition occurred once

Events are returned as plain detached data. There are no callbacks inside the controller, so a HUD or scene cannot re-enter an unfinished damage update. Once terminal, subsequent steps return an empty array and preserve the final state.

Put the scene in an existing Three.js stage

javascript
import * as THREE from "three";
import { createAfterglowScene } from "./scene.js";
import { AfterglowGame } from "./game.js";

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(38, width / height, 0.1, 200);
const world = createAfterglowScene({ scene, camera });
const game = new AfterglowGame();
game.start();

function update(dt, input) {
  const events = game.step(Math.min(dt, 0.25), input);
  world.setState(game.view, events);
  world.update(dt);
  // Your renderer.render(scene, camera) goes here.
}

// On teardown:
world.dispose();

createAfterglowScene(stage, { reducedMotion = false }?) creates no renderer, canvas or DOM. stage.scene and stage.camera are required; an optional stage.bloom is tuned if present. The factory changes the supplied scene's background and fog and positions the supplied camera. It returns root, ground, player, telegraphs, trail, setState(view, events = []), update(dt = 0), setReducedMotion(boolean) and idempotent dispose().

Pass successive real controller views to setState; event arrays add impact flashes. The scene detects a backward game clock on restart and clears all previous Signal/Flux state. Its warning records expose the actual signal and a companion native geometry fill. That static fill and edge geometry improve legibility and allow honest offline illustrations; they are generated from the same options and do not replace the live Signal shader. The factory also works in a Node process for geometry checks or Three.js SVGRenderer illustrations. Such illustrations are not browser screenshots or proof of WebGL rendering.

Customize an encounter

Start by changing a small controller option:

javascript
const game = new AfterglowGame({
  rounds: 3,
  roundDuration: 24,
  health: 4,
  dashCooldown: 1.6,
});
Option Default Accepted range
rounds 3 Integer 1–5
roundDuration 18 seconds 12–60 seconds
health 3 Integer 1–10
arenaRadius 8.8 units 7–12 units
moveSpeed 4.8 units/second 1–12
dashSpeed 18 units/second 8–30
dashDuration 0.22 seconds 0.1–0.5
dashCooldown 1.25 seconds 0.5–5

The shipped HTML introduction, help text and three round indicators describe the default 3×18-second game. Update that copy and indicator count when tuning rounds, duration, health or dash timing. The temple geometry and mint movement ring are authored for arenaRadius: 8.8; changing the controller radius does not resize the island or props. Update the geometry in scene.js alongside any radius change. Other option combinations are validated numerically but are not guaranteed to preserve the default difficulty or solvability.

The encounter schedule is built in restart() using six warning times per round. #arm() chooses the circle, fan, lane or ring and snapshots the player's location/facing at warning start. Later rounds shorten the warning duration and add a twin circle. To create a new pattern, change that method, keep its Signal options and transform as the single source of truth, and keep its impact before the round's end. Add a test that drives the resulting encounter to completion.

Colors, column arrangement, halo, banners and character geometry live in scene.js. Flux's constructor near the keeper controls ribbon capacity, life, color and width. Signal's normalizer documents shape limits in packages/signal/README.md. A native Three.js Telegraph uses the same local -Z direction as attackContains; do not change one orientation convention without the other.

View versioned source on GitHub ↗