/*
Game Scene: one small world that uses almost everything from the series.
@description The capstone: tilemap ground, patterns, sprites, camera, animation, and looping music in one scene.
Part of the BLIT386 demo series.
Prerequisites:
- Basics: https://demos.blit386.dev/basics
- Primitives: https://demos.blit386.dev/primitives
- Colors: https://demos.blit386.dev/colors
- Patterns: https://demos.blit386.dev/patterns
- Camera: https://demos.blit386.dev/camera
- Sprites: https://demos.blit386.dev/sprites
- Animation: https://demos.blit386.dev/animation
- Sprite Effects: https://demos.blit386.dev/sprite-effects
- Starfield: https://demos.blit386.dev/starfield
- Tilemap: https://demos.blit386.dev/tilemap
- Image Output: https://demos.blit386.dev/image-output
- Music: https://demos.blit386.dev/music
- Random Basics: https://demos.blit386.dev/random-basics
Live version: https://demos.blit386.dev/game-scene
Guides:
- Palette: https://blit386.dev/docs/guides/palette#per-draw-palette-offsets-zero-cost-color-variants
- Game Loop: https://blit386.dev/docs/guides/game-loop#smoothing-motion-between-updates
- Audio: https://blit386.dev/docs/guides/audio#playing-music
- Random: https://blit386.dev/docs/guides/random#deterministic-by-default
- Overlay: https://blit386.dev/docs/guides/overlay#custom-rows
Reference:
- Palette: https://blit386.dev/docs/api/palette#per-draw-offset
- Camera: https://blit386.dev/docs/api/camera
- Game Loop: https://blit386.dev/docs/api/game-loop#timer
- Audio: https://blit386.dev/docs/api/audio#loop-points
- Rendering: https://blit386.dev/docs/api/rendering#frame-capture
- Easing: https://blit386.dev/docs/api/easing#applyeasing
- Overlay: https://blit386.dev/docs/api/overlay#custom-rows
---
WHAT YOU WILL SEE
-----------------
A 640x240 world seen through a 320x240 window. From back to front:
- A sky of 20 horizontal bands, eased from a deep blue at the top to a pale blue at the
horizon, with five two-rectangle clouds that scroll at 22 percent of the camera speed
(the parallax idea from Starfield).
- Four buildings filled with a 4x4 checker pattern (Patterns) and framed with an outline,
standing on the far edge of the walkway.
- A grass strip from y = 188 down, a 3-pixel dark line on top of it, and above that the
walkway: one row of 16x16 tiles alternating two grass, two brown dirt, picked by tile
ID from an array (Tilemap).
- The 44x44 rock sprite from Sprites walking left and right along the walkway at one
pixel per tick, bobbing one pixel every 8 ticks, with a thin shadow under its feet. The
camera eases after it (Camera) and is clamped to the world.
- Three sparkles every 30 ticks around the rock: 3x3 squares that hue-shift and fade out
over 72 ticks (the particle idea from Animation).
- A 20-second day/night cycle: every world color dims toward a cool blue at night and
back (the palette tinting idea from Sprite Effects).
- Looping background music with a real intro-then-loop point (Music), a chime on every
day-phase change, and a blip after a successful PNG capture.
- Two small UI kit panels (src/shared/ui.js) along the top: Capstone in the top-left
corner with a Save PNG button (Image Output), and Status in the top-right corner with
the score and the time of day. Until sound is unlocked, the standard "enable sound"
hint sits on the grass in the bottom-left corner.
- Score, rock position, and day phase as engine overlay rows above the FPS bar.
WHAT YOU WILL LEARN
-------------------
- Palette-first lighting: every ambient-lit color is written into a dedicated palette
slot in update(); render() passes palette indices only (see below).
- Per-draw palette offsets: BT.drawSprite(sheet, rect, pos, spriteColorCount) shifts the
rock's pixel indices into a second "ambient" block of slots that update() re-lights
every tick.
- Color32.multiply(), Color32.lerp(), Color32.fromHSL(), and applyEasing(t, 'ease-in-out')
for the sky gradient.
- Fixed-step timing with Timer.fireIfElapsed(tick) for the walk bob, the score, and the
sparkle spawns.
- Smoothing the rock and the camera between ticks with BT.renderAlpha, with zero
per-frame allocation (reused Vector2i and Rect2i instances).
- BT.cameraSet(), BT.cameraReset(), and BT.cameraClamp(camera, worldSize, viewSize),
plus a second, slower camera for the sky layer.
- BT.musicPlay(clip, { loop, loopStart, loopEnd }), AudioClip.synth(),
BT.synthPreset.blip(), and BT.soundPlay(); music requested before the audio unlock
starts on the first click or key press.
- BT.downloadFrame(filename) from a UI kit button bound to Space, with a busy flag while
the promise is pending.
- BT.random.int(min, max) for the sparkle scatter.
- Overlay configuration: overlayRows(), overlayStyle, overlayTimingChartStyle, the palette
grid (isOverlayPaletteEnabled, 32 columns, 3 visible rows), and hiding the toggle hint
with isOverlayToggleHintVisible: false.
HOW THE DAY/NIGHT PALETTE WORKS
-------------------------------
Instead of computing base.multiply(ambient) in render() and passing a Color32 to draw
calls, update() pre-computes those multiplied colors and stores them in dedicated palette
slots. render() only ever uses palette index numbers.
Color32.multiply() scales each channel of the base color by the matching channel of the
ambient color and returns a new Color32 - like shining a colored light on a surface: a
blue light on a red wall gives a darker, purplish result.
Example: the grass fill has a base color (50, 140, 70) and a reserved slot C_GRASS. Every
tick update() computes grassBaseColor.multiply(ambient) and writes it to C_GRASS.
render() calls BT.drawRectFill(rect, C_GRASS) - no Color32 needed there.
The ambient color itself comes from a cosine over the 1200-tick cycle: bright and warm
(255, 255, 255) at the start, cool and dark (70, 75, 120) halfway through.
TRY THIS
--------
- Click or tap "Save PNG (Space)", or press Space, to download blit386-scene.png. The
label reads "Capturing..." while the browser writes the file, then "Saved: ..." for
3 seconds, and a blip plays.
- Click anywhere or press any key once to unlock audio; the music starts from its intro
and then loops between 1.5 s and 7.9 s. Listen for the chime every 5 seconds as the day
phase changes.
- Press Backquote (`) to open the engine overlay. The three custom rows show the score,
the rock's world position, and the day phase; the palette grid at the bottom shows the
scene slots re-lighting as the day turns.
*/
import { function applyEasing(t: number, easing: EasingFunction): numberApplies an easing curve to a normalized time value.applyEasing, class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip, function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap, const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT, class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, type Rect2i = Rect2i
class Rect2i
Integer rectangle for pixel-perfect bounds and regions.
Used throughout the engine for sprite regions, display-space bounds, and
zero-allocation geometry helpers. Both convenience getters and allocation-free
`*To()` helpers are provided so callers can choose between readability and
hot-path efficiency.Rect2i, type SpriteSheet = SpriteSheet
class SpriteSheet
Sprite-sheet wrapper around a loaded image asset.
The class keeps the original image available for CPU-side inspection while
lazily creating and caching a GPU texture for rendering. When possible,
`load()` also pre-decodes the source into an `ImageBitmap` so texture uploads
preserve pixel-art alpha and color values more reliably.
After calling `indexize()`, the sheet stores palette indices rather than RGBA
data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`.
The original RGBA bytes are retained so `reindexize()` can re-convert without
reloading the image.SpriteSheet, class TimerFixed-tick interval helper for
{@link
IBTDemo.update
}
loops.
Counts engine ticks (
{@link
BT.ticks
}
), which advance once per fixed update at
{@link
HardwareSettings.targetFPS
}
, not once per
{@link
IBTDemo.render
}
frame.
Convert ticks to seconds with `intervalTicks / BT.targetFPS`.
Tracks a "last fired" tick and reports when a configured interval has elapsed.
Useful for periodic events such as particle spawning, score ticks, or palette swaps.Timer, class Vector2iInteger 2D vector for pixel-perfect positioning.
Used for points, sizes, directions, and camera offsets throughout the engine.
The API includes both allocation-free `*To()` / `*InPlace()` variants and
convenience methods that return new vectors.Vector2i } from 'blit386';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').Palette} Palette */
/** @typedef {import('blit386').SpriteSheet} SpriteSheet */
/** @typedef {import('blit386').Rect2i} Rect2i */
// Internal game resolution width.
const const DISPLAY_W: 320DISPLAY_W = 320;
// The level is wider than the screen so the camera can scroll (Camera).
const const WORLD_W: 640WORLD_W = 640;
const const WORLD_H: 240WORLD_H = 240;
// Where the walkway ends and the grass starts.
const const GROUND_Y: 188GROUND_Y = 188;
// One row of 16 px tiles along the walkway (Tilemap: small tile IDs in an array).
const const GROUND_TILE_SIZE: 16GROUND_TILE_SIZE = 16;
const const TILE_GRASS_ID: 1TILE_GRASS_ID = 1;
const const TILE_DIRT_ID: 2TILE_DIRT_ID = 2;
// The buildings stand on the far edge of the walkway, and the rock walks along it 4
// pixels in from its near edge, so the scene reads front to back: grass, walkway with the
// rock, buildings, sky.
const const WALKWAY_Y: numberWALKWAY_Y = const GROUND_Y: 188GROUND_Y - const GROUND_TILE_SIZE: 16GROUND_TILE_SIZE;
const const WALK_LINE_Y: numberWALK_LINE_Y = const GROUND_Y: 188GROUND_Y - 4;
// test.png is 44x44, but its bottom 8 rows are transparent: the rock's lowest visible
// pixels are on row 35. Standing the rock on the walk line uses this, not the frame
// height, so its feet touch the ground instead of hovering 8 pixels above it.
const const HERO_ART_BOTTOM: 36HERO_ART_BOTTOM = 36;
// Checker squares inside buildings (Patterns: repeating blocks, no images).
const const BUILDING_PATTERN_CELL: 4BUILDING_PATTERN_CELL = 4;
// How far the rock moves along x each update tick.
const const HERO_SPEED: 1HERO_SPEED = 1;
// Walk bob timer: advance the step counter every 8 ticks.
const const WALK_FRAME_TICKS: 8WALK_FRAME_TICKS = 8;
// Score +1 every 60 ticks (one second at the default 60 ticks per second).
const const SCORE_INTERVAL_TICKS: 60SCORE_INTERVAL_TICKS = 60;
// A batch of sparkles every 30 ticks.
const const PARTICLE_SPAWN_INTERVAL: 30PARTICLE_SPAWN_INTERVAL = 30;
// Full day/night loop: 1200 ticks, 20 seconds at 60 ticks per second.
const const DAY_NIGHT_CYCLE_TICKS: 1200DAY_NIGHT_CYCLE_TICKS = 1200;
// Camera smoothing: each tick the camera steps 14 percent closer to its target.
const const CAMERA_LERP: 0.14CAMERA_LERP = 0.14;
// These two numbers come straight out of public/audio/music-intro-loop.loop.json,
// generated by scripts/generate-audio-loops.mjs. Music plays the same track in
// isolation.
const const MUSIC_LOOP_START_SECONDS: 1.5MUSIC_LOOP_START_SECONDS = 1.5;
const const MUSIC_LOOP_END_SECONDS: 7.9MUSIC_LOOP_END_SECONDS = 7.9;
// Parallax: clouds move at 22 percent of the real camera speed.
const const SKY_PARALLAX: 0.22SKY_PARALLAX = 0.22;
// How many sky bands cover the sky from y = 0 to GROUND_Y.
const const SKY_BANDS: 20SKY_BANDS = 20;
// Size of the rotating pool of particle palette slots.
const const MAX_PARTICLES: 20MAX_PARTICLES = 20;
// Static scene slot (never changes after init). UI text and panel colors come from the
// shared UI kit theme (slots 240 and up); the slots below belong to the scene itself.
const const C_BLACK: 2C_BLACK = 2; // black for BT.clear
// Dynamic world slots, rewritten every tick in update(). Sky bands: 10..10+SKY_BANDS-1
// (20 slots).
const const C_SKY_BASE: 10C_SKY_BASE = 10;
// Ground: 30..31, and the walkway's dirt tiles at 47.
const const C_GRASS: 30C_GRASS = 30;
const const C_DIRTLINE: 31C_DIRTLINE = 31;
const const C_DIRT: 47C_DIRT = 47;
// Buildings: 4 buildings x 2 slots (fill + outline) = 8 slots at 32..39.
const const C_BUILDING_BASE: 32C_BUILDING_BASE = 32;
// Clouds: one slot at 40.
const const C_CLOUD: 40C_CLOUD = 40;
// Overlay text and timing-chart colors: 41..44, dimmed with the scene at night.
const const C_HUD_TITLE: 41C_HUD_TITLE = 41; // timing-chart error markers only
const const C_HUD_SCORE: 42C_HUD_SCORE = 42;
const const C_HUD_POS: 43C_HUD_POS = 43;
const const C_HUD_PHASE: 44C_HUD_PHASE = 44; // colors the day-phase overlay row (Day / Night / ...)
// Hero shadow: 45.
const const C_HERO_SHADOW: 45C_HERO_SHADOW = 45;
// Overlay bar fill (text slots reuse C_HUD_SCORE / C_HUD_POS / C_HUD_PHASE).
const const C_OVERLAY_BAR: 46C_OVERLAY_BAR = 46;
// Particle slots: 50..69 (MAX_PARTICLES = 20).
const const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START = 50;
// Sprite base colors extracted from test.png: 70..70+N-1. The ambient (lit) version of
// the same colors: 70+N..70+2N-1.
const const SPRITE_BASE: 70SPRITE_BASE = 70;
// One self-running mini scene: walking rock, following camera, overlay rows, day/night,
// sparkles. All color computation happens in update(); render() uses palette indices
// only.
class class DemoDemo {
// The palette holds every color this demo uses.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Sprite sheet for the rock hero, loaded from /sprites/test.png.
/** @type {SpriteSheet | null} */
Demo.heroSheet: SpriteSheet | nullheroSheet = null;
// Looping background music with a distinct intro section.
/** @type {AudioClip | null} */
Demo.musicClip: AudioClip | nullmusicClip = null;
// Short chime played on every day/night phase change.
/** @type {AudioClip | null} */
Demo.dayPhaseChimeClip: AudioClip | nulldayPhaseChimeClip = null;
// Confirmation blip played after a successful PNG capture.
/** @type {AudioClip | null} */
Demo.captureBlipClip: AudioClip | nullcaptureBlipClip = null;
// Day phase label as of the previous tick, used to detect changes.
/** @type {string | null} */
Demo.lastDayPhaseLabel: string | nulllastDayPhaseLabel = null;
// The full source rectangle for the hero sprite.
/** @type {Rect2i | null} */
Demo.heroSprite: Rect2i | nullheroSprite = null;
// Hero sprite size in world pixels, read from the loaded sheet in init() (test.png is
// 44x44). Deriving it from the real image instead of a guessed number keeps movement
// bounds, camera centering, and particle spawns matching what is drawn.
/** @type {Vector2i} */
Demo.heroSize: Vector2iheroSize = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// How many unique colors the sprite has (N).
Demo.spriteColorCount: numberspriteColorCount = 0;
// Original Color32 objects for the sprite's colors, for the ambient multiplication.
Demo.spriteBaseColors: {}spriteBaseColors = [];
// Rock position in world pixels. The y here is a placeholder: init() sets the real y
// once the sprite has loaded and its height is known, so the rock stands exactly on
// the ground line.
Demo.heroPos: Vector2iheroPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(120, 0);
// Rock position at the start of the most recent update() tick, before this tick's
// walk step moved it. render() blends between heroPrevPos and heroPos using
// BT.renderAlpha so the rock glides between ticks instead of jumping (Game Loop
// guide, "Smoothing motion between updates"). init() snaps this to heroPos so the
// very first frame does not blend in from a stale position.
Demo.heroPrevPos: Vector2iheroPrevPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(120, 0);
// +1 = moving right, -1 = moving left.
Demo.heroFacing: numberheroFacing = 1;
// Walk step counter: not a frame index, it only bobs the rock one pixel.
Demo.walkStep: numberwalkStep = 0;
Demo.walkFrameTimer: TimerwalkFrameTimer = new new Timer(intervalTicks: number): TimerCreates a timer that fires once per fixed-tick interval.Timer(const WALK_FRAME_TICKS: 8WALK_FRAME_TICKS);
// Camera top-left in world coordinates.
Demo.cameraPos: Vector2icameraPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// Camera position at the start of the most recent update() tick, before this tick's
// follow-lerp moved it. render() blends between this and cameraPos using
// BT.renderAlpha for the same reason as heroPrevPos above.
Demo.cameraPrevPos: Vector2icameraPrevPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// Reused every render() call for the interpolated camera and hero positions, so no
// new Vector2i is allocated per frame.
Demo.cameraRenderPos: Vector2icameraRenderPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
Demo.heroRenderPos: Vector2iheroRenderPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// Float version of the camera x for a smooth lerp without pixel jitter.
Demo.cameraXFloat: numbercameraXFloat = 0;
// Simple score counter and its timer.
Demo.score: numberscore = 0;
Demo.scoreTimer: TimerscoreTimer = new new Timer(intervalTicks: number): TimerCreates a timer that fires once per fixed-tick interval.Timer(const SCORE_INTERVAL_TICKS: 60SCORE_INTERVAL_TICKS);
// Particle spawn timer.
Demo.particleSpawnTimer: TimerparticleSpawnTimer = new new Timer(intervalTicks: number): TimerCreates a timer that fires once per fixed-tick interval.Timer(const PARTICLE_SPAWN_INTERVAL: 30PARTICLE_SPAWN_INTERVAL);
// Active particle objects: { pos, spawnTick, paletteSlot }.
Demo.particles: {}particles = [];
// Rotating pool index for particle palette slots.
Demo.nextParticleSlot: numbernextParticleSlot = 0;
// World decoration: buildings and clouds, built once in init().
Demo.buildings: {}buildings = [];
Demo.clouds: {}clouds = [];
// Tile IDs for the walkway row (Tilemap): each entry is TILE_GRASS_ID or
// TILE_DIRT_ID.
Demo.groundTileIds: {}groundTileIds = [];
// PNG capture state (Image Output): the Capstone panel's Save button (click, tap, or
// Space) triggers BT.downloadFrame() once per press.
Demo.capturing: booleancapturing = false;
Demo.lastCaptureMessage: stringlastCaptureMessage = '';
Demo.messageTimer: numbermessageTimer = 0;
// Base colors for buildings, clouds, and ground, multiplied by the ambient in
// update().
Demo.buildingFills: {}buildingFills = [];
Demo.buildingOutlines: {}buildingOutlines = [];
Demo.cloudBaseColor: Color32cloudBaseColor = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(230, 240, 255, 200);
Demo.grassBaseColor: Color32grassBaseColor = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(50, 140, 70);
Demo.dirtBaseColor: Color32dirtBaseColor = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(40, 100, 55);
Demo.walkwayDirtBaseColor: Color32walkwayDirtBaseColor = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(130, 100, 70);
// Reused rectangle and vectors to avoid creating new objects every frame.
Demo.tempRect: Rect2itempRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 0, 0, 0);
Demo.tempVec: Vector2itempVec = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
Demo.worldSize: Vector2iworldSize = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const WORLD_W: 640WORLD_W, const WORLD_H: 240WORLD_H); // pre-allocated for cameraClamp calls
// Sky gradient: base colors at the top and at the horizon.
Demo.skyTop: Color32skyTop = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(40, 70, 140);
Demo.skyHorizon: Color32skyHorizon = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(120, 170, 220);
// Overlay text base colors, before the ambient is applied.
Demo.hudTitleBase: Color32hudTitleBase = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 230, 180);
Demo.hudScoreBase: Color32hudScoreBase = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(200, 220, 255);
Demo.hudPosBase: Color32hudPosBase = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(180, 200, 180);
Demo.hudFpsBase: Color32hudFpsBase = new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(150, 150, 160);
// Reused every frame for the overlay rows: score, rock position, day phase.
Demo.overlayRowData: {}overlayRowData = [
{ leftText: stringleftText: 'Score 0', textPaletteIndex: numbertextPaletteIndex: const C_HUD_SCORE: 42C_HUD_SCORE },
{ leftText: stringleftText: 'Rock (0, 0)', textPaletteIndex: numbertextPaletteIndex: const C_HUD_POS: 43C_HUD_POS },
{ leftText: stringleftText: 'Dawn/Day', textPaletteIndex: numbertextPaletteIndex: const C_HUD_PHASE: 44C_HUD_PHASE },
];
// Palette slots for the engine overlay bars (the FPS strip keeps the engine
// defaults). The live palette grid at the bottom of the overlay shows which slots
// this frame's draw calls use, which is handy for the day/night tinting and the
// sprite palette blocks: 32 swatches per row, 3 visible rows, scroll to browse all
// 256 slots.
Demo.configure(): {
isOverlayToggleHintVisible: boolean;
isCapturingKeyboardScroll: boolean;
isOverlayPaletteEnabled: boolean;
overlayPaletteColumns: number;
overlayPaletteRowsVisible: number;
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
};
isOverlayTimingChartEnabled: boolean;
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
};
}
configure() {
return {
// The engine normally draws a small toggle hint in the bottom-left corner so
// people know they can press Backquote (`) to open the stats overlay. This is
// an immersive game scene, so the hint is hidden to keep the picture clean.
// The overlay still works: ` reveals the full dev HUD (timing chart and
// palette grid) on demand, ` again hides it. Teaching demos leave the hint
// visible (the default) so newcomers can find it.
isOverlayToggleHintVisible: booleanisOverlayToggleHintVisible: false,
// Space is the Save PNG shortcut; keep the page from scrolling when it is
// pressed.
isCapturingKeyboardScroll: booleanisCapturingKeyboardScroll: true,
isOverlayPaletteEnabled: booleanisOverlayPaletteEnabled: true,
overlayPaletteColumns: numberoverlayPaletteColumns: 32,
overlayPaletteRowsVisible: numberoverlayPaletteRowsVisible: 3,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const C_OVERLAY_BAR: 46C_OVERLAY_BAR,
textPaletteIndex: numbertextPaletteIndex: const C_HUD_SCORE: 42C_HUD_SCORE,
gapPaletteIndex: numbergapPaletteIndex: const C_OVERLAY_BAR: 46C_OVERLAY_BAR,
},
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_HUD_POS: 43C_HUD_POS,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_HUD_SCORE: 42C_HUD_SCORE,
warningPaletteIndex: numberwarningPaletteIndex: const C_HUD_PHASE: 44C_HUD_PHASE,
errorPaletteIndex: numbererrorPaletteIndex: const C_HUD_TITLE: 41C_HUD_TITLE,
tagPaletteIndex: numbertagPaletteIndex: const C_HUD_POS: 43C_HUD_POS,
},
};
}
// Loads the hero sprite sheet, builds the palette, places buildings and clouds, and
// starts the audio.
async Demo.init(): unknowninit() {
console.log('[GameSceneDemo] Initializing...');
// Create the palette and set the static slots. The shared UI kit needs its
// twelve theme colors in the palette before any widget draws. applyTheme() writes
// them into high slots (240 and up), far above this demo's scene slots, which end
// at SPRITE_BASE + 2N - 1 (below slot 100). The returned slot map is not needed
// here, so the call is side effect only.
this.Demo.palette: Palette | nullpalette = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BLACK: 2C_BLACK, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0));
// Overlay row backgrounds.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_OVERLAY_BAR: 46C_OVERLAY_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0, 180));
// Pre-fill the particle slots as transparent.
for (let let i: numberi = 0; let i: numberi < const MAX_PARTICLES: 20MAX_PARTICLES; let i: numberi++) {
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START + let i: numberi, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0, 0));
}
// Build the world decoration and the walkway tile-ID row (Tilemap).
this.Demo.buildWorldDecor(): voidbuildWorldDecor();
this.Demo.buildGroundTileStrip(): voidbuildGroundTileStrip();
// Extract the sprite colors and register them in the palette: the engine scans
// the PNG and adds every unique color it finds, starting at SPRITE_BASE. The
// returned array holds the same colors in palette-write order (sorted darkest
// first by luminance). They are kept so updateWorldPalette() can multiply each
// base color by the current ambient tint and write the lit version into the
// higher "ambient block" (SPRITE_BASE+N..SPRITE_BASE+2N-1).
this.Demo.spriteBaseColors: {}spriteBaseColors = await class SpriteSheetSprite-sheet wrapper around a loaded image asset.
The class keeps the original image available for CPU-side inspection while
lazily creating and caching a GPU texture for rendering. When possible,
`load()` also pre-decodes the source into an `ImageBitmap` so texture uploads
preserve pixel-art alpha and color values more reliably.
After calling `indexize()`, the sheet stores palette indices rather than RGBA
data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`.
The original RGBA bytes are retained so `reindexize()` can re-convert without
reloading the image.SpriteSheet.SpriteSheet.loadColorsIntoPalette(url: string, palette: Palette, startSlot: number, options?: {
sort?: "luminance" | "none";
}): Promise<Color32[]>
Walks a PNG's pixels and registers every unique opaque color into the
supplied palette starting at `startSlot`.
Pixels with alpha 0 are skipped - they map to the engine's transparent
sentinel slot 0 at draw time. Opaque pixels are deduplicated on RGB and
stored with alpha forced to 255, matching the lookup performed by
`indexize()` so a subsequent `sheet.indexize(palette)` call resolves
without throwing on missing colors.
By default colors are sorted darkest-first by perceived luminance
(
{@link
Color32.luminance
}
); pass `{ sort: 'none' }` to keep the
row-major scan order of the source image.
Image loading goes through
{@link
AssetLoader.loadImage
}
, so the call
shares cache and in-flight deduplication with
{@link
SpriteSheet.load
}
.
The destination range is validated before any write, so the palette is
never left partially mutated: if the collected colors would not fit
(`startSlot < 1` or `startSlot + count > palette.size`), the method
throws without touching any slot.loadColorsIntoPalette('/sprites/test.png', this.Demo.palette: Palettepalette, const SPRITE_BASE: 70SPRITE_BASE);
const const colorCount: anycolorCount = this.Demo.spriteBaseColors: {}spriteBaseColors.length;
this.Demo.spriteColorCount: numberspriteColorCount = const colorCount: anycolorCount;
// Pre-fill the ambient sprite block (SPRITE_BASE+N..SPRITE_BASE+2N-1). update()
// recalculates these every tick from the current ambient light.
for (let let i: numberi = 0; let i: numberi < const colorCount: anycolorCount; let i: numberi++) {
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const SPRITE_BASE: 70SPRITE_BASE + const colorCount: anycolorCount + let i: numberi, this.Demo.spriteBaseColors: {}spriteBaseColors[let i: numberi]);
}
// Load the hero sprite against the slots registered above.
const const indexed: Promise<IndexedSpriteLoadResult>indexed = await class SpriteSheetSprite-sheet wrapper around a loaded image asset.
The class keeps the original image available for CPU-side inspection while
lazily creating and caching a GPU texture for rendering. When possible,
`load()` also pre-decodes the source into an `ImageBitmap` so texture uploads
preserve pixel-art alpha and color values more reliably.
After calling `indexize()`, the sheet stores palette indices rather than RGBA
data. The GPU texture becomes an `r8uint` format uploaded via `writeTexture`.
The original RGBA bytes are retained so `reindexize()` can re-convert without
reloading the image.SpriteSheet.SpriteSheet.loadIndexed(url: string, palette: Palette, startSlot: number, options?: {
sort?: "luminance" | "none";
tileSize?: number | Vector2i;
}): Promise<IndexedSpriteLoadResult>
Convenience one-call path for palette-indexed sprite setup.
This combines:
1)
{@link
SpriteSheet.loadColorsIntoPalette
}
2)
{@link
SpriteSheet.load
}
3)
{@link
SpriteSheet.indexize
}
It returns the indexized sheet plus a full-frame source rectangle and the
colors that were written into the palette. Callers still control when to
activate the palette via `BT.paletteSet(palette)`.loadIndexed('/sprites/test.png', this.Demo.palette: Palettepalette, const SPRITE_BASE: 70SPRITE_BASE, {
sort?: "luminance" | "none" | undefinedsort: 'none',
});
this.Demo.heroSheet: SpriteSheet | nullheroSheet = const indexed: Promise<IndexedSpriteLoadResult>indexed.sheet;
this.Demo.heroSprite: Rect2i | nullheroSprite = this.Demo.heroSheet: SpriteSheet | nullheroSheet.SpriteSheet.fullRect(): Rect2iReturns a source rectangle that covers the entire sprite sheet.fullRect();
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteSet: (palette: Palette) => voidStores the active engine palette.
Use this to swap the **entire palette** (e.g. switch between a day and night
theme). After this call the renderer uploads the new palette uniform on the
next frame.
**Palette-value swap (change what a slot looks like):** mutate the live
{@link
BT.palette
}
in place with `palette.set(slot, newColor)`. The renderer
uploads dirty slots on the next frame; no `paletteSet()` or
{@link
BT.spritesRefresh
}
needed.
**Palette-layout swap (same colors, different slot positions):** build a new
palette with the same colors at new indices, call `paletteSet()`, then call
{@link
BT.spritesRefresh
}
so every sprite sheet re-maps its original RGBA
pixels against the new slot layout.paletteSet(this.Demo.palette: Palettepalette);
console.log(`[GameSceneDemo] Loaded sprite: ${this.Demo.heroSprite: Rect2iheroSprite.Rect2i.width: numberWidth in pixels (defaults to 0).width}x${this.Demo.heroSprite: Rect2iheroSprite.Rect2i.height: numberHeight in pixels (defaults to 0).height}px`);
// Read the hero's real size from the loaded sheet (44x44 for test.png), the same
// way Basics Enhanced and Logo Low-Res do. Every bit of math below - movement
// bounds, camera centering, particle spawns - uses this size, so the logic always
// matches the picture on screen.
this.Demo.heroSize: Vector2iheroSize.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(this.Demo.heroSheet: SpriteSheet | nullheroSheet.SpriteSheet.size: Vector2iGets the sprite-sheet dimensions in pixels.size.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.heroSheet: SpriteSheet | nullheroSheet.SpriteSheet.size: Vector2iGets the sprite-sheet dimensions in pixels.size.Vector2i.y: numberVertical component (defaults to 0).y);
// Stand the rock on the walk line: the sprite's top-left y is the walk line minus
// the height of its visible pixels (HERO_ART_BOTTOM), so its lowest pixels sit
// right on WALK_LINE_Y.
this.Demo.heroPos: Vector2iheroPos.Vector2i.y: numberVertical component (defaults to 0).y = const WALK_LINE_Y: numberWALK_LINE_Y - const HERO_ART_BOTTOM: 36HERO_ART_BOTTOM;
// Snap heroPrevPos to match so the very first render does not blend in from the
// placeholder position the class fields started with.
this.Demo.heroPrevPos: Vector2iheroPrevPos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.heroPos: Vector2iheroPos.Vector2i.y: numberVertical component (defaults to 0).y);
// Place the camera on the hero to start.
this.Demo.cameraXFloat: numbercameraXFloat = this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x - const DISPLAY_W: 320DISPLAY_W / 2 + this.Demo.heroSize: Vector2iheroSize.Vector2i.x: numberHorizontal component (defaults to 0).x / 2;
this.Demo.cameraPos: Vector2icameraPos.Vector2i.x: numberHorizontal component (defaults to 0).x = Math.floor(this.Demo.cameraXFloat: numbercameraXFloat);
this.Demo.cameraPos: Vector2icameraPos.Vector2i.y: numberVertical component (defaults to 0).y = 0;
this.Demo.clampCamera(): voidclampCamera();
// Snap cameraPrevPos to match the starting camera position so the very first
// render does not blend in from (0, 0).
this.Demo.cameraPrevPos: Vector2icameraPrevPos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(this.Demo.cameraPos: Vector2icameraPos.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.cameraPos: Vector2icameraPos.Vector2i.y: numberVertical component (defaults to 0).y);
// Write every ambient-lit color once now. update() rewrites them each tick, but
// the first frame can be drawn before the first update() runs, and without this
// it would show a black world.
this.Demo.updateWorldPalette(): voidupdateWorldPalette();
// Load and start all sound: music, the day/night chime, and the capture blip.
await this.Demo.initAudio(): anyinitAudio();
console.log('[GameSceneDemo] Ready.');
return true;
}
// Loads the music track, synthesizes the two sound effects, and starts the music.
// Called once from init(); split out so the audio setup reads as one step.
async Demo.initAudio(): anyinitAudio() {
// Background music: a real intro-then-loop track, the same one Music plays in
// isolation. BT.musicPlay() called before the page is unlocked is remembered and
// starts for real the instant the player clicks or presses a key.
//
// Load and play are wrapped like in Snake Game: a missing or undecodable file
// must not abort the whole scene - the capstone still renders with SFX only.
try {
this.Demo.musicClip: AudioClip | nullmusicClip = await class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip.AudioClip.load(url: string | string[], options?: AudioClipLoadOptions): Promise<AudioClip>Loads an audio clip from a single URL, or from an ordered list of
candidate URLs.
A single URL runs the download+decode pipeline directly, sharing the
per-URL cache and in-flight dedup described on
{@link
AudioClip
}
. A URL
array tries each candidate in order and resolves with the first one
that downloads and decodes successfully - useful for offering a
browser-friendly fallback (for example `['music.ogg', 'music.mp3']`)
when a container or codec isn't universally supported. If every
candidate fails, the error from the last candidate is thrown.load('/audio/music-intro-loop.wav');
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.musicPlay: (clip: AudioClip, options?: MusicPlayOptions) => voidPlays a loaded audio clip through the music player, crossfading out whatever is currently
playing.
Silently does nothing when the clip hasn't finished loading yet (or was already unloaded
with `clip.unload()`), or before the engine has initialized. While the audio context is
still locked (before the first unlock gesture), the request is remembered instead of
dropped - it starts automatically the instant the context unlocks, unlike
{@link
BT.soundPlay
}
.musicPlay(this.Demo.musicClip: AudioClip | nullmusicClip, {
MusicPlayOptions.loop?: boolean | undefinedWhether the whole track loops. Ignored when `loopStart`/`loopEnd` are given. Defaults to `true`.loop: true,
MusicPlayOptions.loopStart?: number | undefinedLoop region start in seconds. Requires `loopEnd`; see
{@link
MusicPlayer.play
}
.loopStart: const MUSIC_LOOP_START_SECONDS: 1.5MUSIC_LOOP_START_SECONDS,
MusicPlayOptions.loopEnd?: number | undefinedLoop region end in seconds. Requires `loopStart`; see
{@link
MusicPlayer.play
}
.loopEnd: const MUSIC_LOOP_END_SECONDS: 7.9MUSIC_LOOP_END_SECONDS,
});
} catch (function (local var) error: unknownerror) {
console.warn('[GameSceneDemo] Failed to load background music, continuing without it.', function (local var) error: unknownerror);
this.Demo.musicClip: AudioClip | nullmusicClip = null;
}
// A soft sine chime for day/night transitions, and BT.synthPreset.blip() (the
// same UI blip Synth Toy uses) for a successful capture.
this.Demo.dayPhaseChimeClip: AudioClip | nulldayPhaseChimeClip = await class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip.AudioClip.synth(params: SynthParams): Promise<AudioClip>Synthesizes a clip from deterministic procedural parameters - no source file, no
`OfflineAudioContext`, and no audio graph involved.
Rendering happens entirely on the CPU via the pure
{@link
renderSynthSamples
}
function
against an `AudioBuffer` allocated from the registered decode context. The returned clip
flows through the same
{@link
buffer
}
getter and playback path as a loaded clip, but uses
a synthetic, non-cached identifier (`synth:<waveform>`) - it is never added to the
URL-keyed resolved cache and never deduplicated, so identical `params` still render a
fresh, independent `AudioBuffer` on every call. See
{@link
SynthParams
}
for the full
parameter set.synth({
SynthParams.waveform: anyOscillator waveform shape.waveform: 'sine',
SynthParams.frequency: numberBase carrier frequency in Hz at the start of the clip (before any pitch sweep or vibrato).frequency: 660,
SynthParams.duration: numberTotal clip duration in seconds. Must be greater than 0 and no more than
{@link
MAX_SYNTH_DURATION_SECONDS
}
.duration: 0.6,
SynthParams.volume?: number | undefinedOverall output amplitude in [0, 1] (unclamped on the high end; final output is always
clamped to avoid clipping).
Defaults to
{@link
DEFAULT_VOLUME
}
.volume: 0.5,
SynthParams.envelope?: SynthEnvelope | undefinedOptional attack/decay/sustain/release envelope. Defaults to a full ADSR envelope; see
{@link
SynthEnvelope
}
.envelope: { SynthEnvelope.attack?: number | undefinedTime in seconds to ramp from silence to full amplitude.
Defaults to
{@link
DEFAULT_ATTACK
}
.attack: 0.02, SynthEnvelope.decay?: number | undefinedTime in seconds to fall from full amplitude to the `sustain` level.
Defaults to
{@link
DEFAULT_DECAY
}
.decay: 0.15, SynthEnvelope.sustain?: number | undefinedGain level in [0, 1] held between the decay and release phases.
Defaults to
{@link
DEFAULT_SUSTAIN
}
.sustain: 0.3, SynthEnvelope.release?: number | undefinedTime in seconds to fall from the sustain level to silence at the end of the clip.
Defaults to
{@link
DEFAULT_RELEASE
}
.release: 0.4 },
SynthParams.seed: numberSeed for the deterministic PRNG driving noise generation - identical seeds render identical output.seed: 1,
});
this.Demo.captureBlipClip: AudioClip | nullcaptureBlipClip = await class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip.AudioClip.synth(params: SynthParams): Promise<AudioClip>Synthesizes a clip from deterministic procedural parameters - no source file, no
`OfflineAudioContext`, and no audio graph involved.
Rendering happens entirely on the CPU via the pure
{@link
renderSynthSamples
}
function
against an `AudioBuffer` allocated from the registered decode context. The returned clip
flows through the same
{@link
buffer
}
getter and playback path as a loaded clip, but uses
a synthetic, non-cached identifier (`synth:<waveform>`) - it is never added to the
URL-keyed resolved cache and never deduplicated, so identical `params` still render a
fresh, independent `AudioBuffer` on every call. See
{@link
SynthParams
}
for the full
parameter set.synth(const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.synthPreset: {
jump: (seed?: number) => SynthParams;
pickup: (seed?: number) => SynthParams;
explosion: (seed?: number) => SynthParams;
laser: (seed?: number) => SynthParams;
hit: (seed?: number) => SynthParams;
blip: (seed?: number) => SynthParams;
}
Pre-configured `SynthParams` presets for common sound effects ("jump", "pickup",
"explosion", "laser", "hit", "blip").
Each function returns a fresh
{@link
SynthParams
}
object; pass it to
{@link
AudioClip.synth
}
to render a clip, then play the result via
{@link
BT.soundPlay
}
.
An optional `seed` argument applies small, bounded, deterministic jitter to a few
hand-picked fields per preset, so repeated plays vary without losing reproducibility -
the same seed always renders the exact same variant.synthPreset.blip: (seed?: number) => SynthParamsUI blip / menu select: a very short, clean sine tone.
`seed` jitters only the base frequency, and only slightly (+/-3%) - UI feedback should stay
recognizably consistent rather than vary as much as a gameplay sound effect. Omit `seed` for
a fixed baseline variant.blip());
// Remember the starting day phase so updateDayPhaseSound() only chimes when the
// phase actually changes, not on the very first tick.
this.Demo.lastDayPhaseLabel: string | nulllastDayPhaseLabel = this.Demo.getDayPhaseLabel(tick?: number): "Day" | "Toward dusk" | "Night" | "Toward dawn"getDayPhaseLabel();
}
// Fixed-step logic: moves the rock, camera, score, and particles, then recalculates
// all ambient-lit palette colors.
Demo.update(): voidupdate() {
// Let the shared UI kit latch keyboard shortcuts and touch contacts for this
// tick. This must run before anything else so the Save button's Space binding
// works.
import uiui.tick();
const const tick: numbertick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
this.Demo.updateDayPhaseSound(tick: any): voidupdateDayPhaseSound(const tick: numbertick);
// The PNG capture itself starts from the kit's Save button in renderHud().
// Here only the "Saved: ..." message is counted down so it disappears after a
// while.
if (this.Demo.messageTimer: numbermessageTimer > 0) {
this.Demo.messageTimer: numbermessageTimer--;
}
this.Demo.updateHeroMovement(): voidupdateHeroMovement();
this.Demo.updateWalkStep(tick: any): voidupdateWalkStep(const tick: numbertick);
this.Demo.updateCameraFollow(): voidupdateCameraFollow();
this.Demo.updateScore(tick: any): voidupdateScore(const tick: numbertick);
this.Demo.updateParticlesSpawn(tick: any): voidupdateParticlesSpawn(const tick: numbertick);
this.Demo.cleanupParticles(tick: any): voidcleanupParticles(const tick: numbertick);
this.Demo.updateParticleColors(tick: any): voidupdateParticleColors(const tick: numbertick);
// Recalculate all ambient-lit palette colors: the core of the day/night system.
this.Demo.updateWorldPalette(): voidupdateWorldPalette();
}
// Plays a short chime the instant the day/night phase label changes (Day -> Toward
// dusk -> Night -> Toward dawn -> Day ...). Comparing this tick's label against last
// tick's label is the same edge-detection idea Keyboard Input uses for key presses:
// only the moment something changes matters, not every tick it stays the same.
Demo.updateDayPhaseSound(tick: any): voidupdateDayPhaseSound(tick: anytick) {
const const currentPhase: "Day" | "Toward dusk" | "Night" | "Toward dawn"currentPhase = this.Demo.getDayPhaseLabel(tick?: number): "Day" | "Toward dusk" | "Night" | "Toward dawn"getDayPhaseLabel(tick: anytick);
if (this.Demo.lastDayPhaseLabel: string | nulllastDayPhaseLabel !== null && const currentPhase: "Day" | "Toward dusk" | "Night" | "Toward dawn"currentPhase !== this.Demo.lastDayPhaseLabel: stringlastDayPhaseLabel) {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRefPlays a loaded audio clip through the SFX voice pool.
Returns an inert
{@link
SoundRef
}
without allocating a voice when the clip hasn't finished
loading yet (or was already unloaded), when the pool has no free or stealable voice at this
priority, or before the engine has unlocked audio playback.soundPlay(this.Demo.dayPhaseChimeClip: AudioClip | nulldayPhaseChimeClip);
}
this.Demo.lastDayPhaseLabel: string | nulllastDayPhaseLabel = const currentPhase: "Day" | "Toward dusk" | "Night" | "Toward dawn"currentPhase;
}
// Draws the world layers back to front. Score, position, and phase text are handled
// by the engine overlay. Every draw call uses palette index numbers only, no Color32.
Demo.render(): voidrender() {
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.clear: (paletteIndex: number) => voidSets the frame clear color using a palette index.
The renderer uses this color when clearing the full display at the start
of the next frame.clear(const C_BLACK: 2C_BLACK);
// Blend cameraPrevPos toward cameraPos by BT.renderAlpha - a fraction from 0 (a
// tick just finished) to just under 1 (the next tick is about to happen) - so the
// camera's on-screen position matches this exact render moment instead of only
// its last-tick position. Both the sky parallax and the full camera offset use
// this same smoothed value, so the layers never drift apart.
this.Demo.cameraRenderPos: Vector2icameraRenderPos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(
Math.floor(this.Demo.cameraPrevPos: Vector2icameraPrevPos.Vector2i.x: numberHorizontal component (defaults to 0).x + (this.Demo.cameraPos: Vector2icameraPos.Vector2i.x: numberHorizontal component (defaults to 0).x - this.Demo.cameraPrevPos: Vector2icameraPrevPos.Vector2i.x: numberHorizontal component (defaults to 0).x) * const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha),
Math.floor(this.Demo.cameraPrevPos: Vector2icameraPrevPos.Vector2i.y: numberVertical component (defaults to 0).y + (this.Demo.cameraPos: Vector2icameraPos.Vector2i.y: numberVertical component (defaults to 0).y - this.Demo.cameraPrevPos: Vector2icameraPrevPos.Vector2i.y: numberVertical component (defaults to 0).y) * const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha),
);
// Layer 1: sky and clouds with parallax.
this.Demo.renderSkyLayer(): voidrenderSkyLayer();
// Layer 2: world with the full camera offset.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.cameraSet: (offset: Vector2i) => voidSets the global camera offset applied to subsequent draw calls.cameraSet(this.Demo.cameraRenderPos: Vector2icameraRenderPos);
this.Demo.renderGroundAndBuildings(): voidrenderGroundAndBuildings();
this.Demo.renderParticles(): voidrenderParticles();
this.Demo.renderHero(): voidrenderHero();
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.cameraReset: () => voidResets the global camera offset to `(0, 0)`.cameraReset();
// HUD panels and capture controls, in screen space (not scrolled with the
// world).
this.Demo.renderHud(): voidrenderHud();
// Score, rock position, and day phase are drawn in overlayRows() above the FPS
// bar.
}
// Score, rock position, and day/night phase for the engine overlay. The text colors
// are updated each tick in updateWorldPalette() so they dim at night.
Demo.overlayRows(): {}overlayRows() {
this.Demo.overlayRowData: {}overlayRowData[0].leftText = `Score ${this.Demo.score: numberscore}`;
this.Demo.overlayRowData: {}overlayRowData[1].leftText = `Rock (${this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x}, ${this.Demo.heroPos: Vector2iheroPos.Vector2i.y: numberVertical component (defaults to 0).y})`;
this.Demo.overlayRowData: {}overlayRowData[2].leftText = this.Demo.getDayPhaseLabel(tick?: number): "Day" | "Toward dusk" | "Night" | "Toward dawn"getDayPhaseLabel();
return this.Demo.overlayRowData: {}overlayRowData;
}
// Fills groundTileIds with alternating grass/dirt tile IDs for one 16 px row
// (Tilemap).
Demo.buildGroundTileStrip(): voidbuildGroundTileStrip() {
const const cols: anycols = Math.ceil(const WORLD_W: 640WORLD_W / const GROUND_TILE_SIZE: 16GROUND_TILE_SIZE);
this.Demo.groundTileIds: {}groundTileIds = [];
for (let let col: numbercol = 0; let col: numbercol < const cols: anycols; let col: numbercol++) {
// Simple pattern: two grass tiles, then two dirt tiles, repeat.
this.Demo.groundTileIds: {}groundTileIds.push(let col: numbercol % 4 < 2 ? const TILE_GRASS_ID: 1TILE_GRASS_ID : const TILE_DIRT_ID: 2TILE_DIRT_ID);
}
}
// Places buildings and clouds once at startup, and stores the building base colors in
// separate arrays so update() can apply the ambient.
Demo.buildWorldDecor(): voidbuildWorldDecor() {
const const rawBuildings: {}rawBuildings = [
{
x: numberx: 40,
y: numbery: const WALKWAY_Y: numberWALKWAY_Y - 52,
w: numberw: 36,
h: numberh: 52,
fill: Color32fill: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(140, 90, 70),
outline: Color32outline: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(80, 50, 40),
},
{
x: numberx: 200,
y: numbery: const WALKWAY_Y: numberWALKWAY_Y - 70,
w: numberw: 44,
h: numberh: 70,
fill: Color32fill: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(110, 120, 140),
outline: Color32outline: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(60, 70, 90),
},
{
x: numberx: 380,
y: numbery: const WALKWAY_Y: numberWALKWAY_Y - 46,
w: numberw: 32,
h: numberh: 46,
fill: Color32fill: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(160, 100, 100),
outline: Color32outline: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(100, 50, 50),
},
{
x: numberx: 500,
y: numbery: const WALKWAY_Y: numberWALKWAY_Y - 60,
w: numberw: 40,
h: numberh: 60,
fill: Color32fill: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(120, 130, 100),
outline: Color32outline: new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(70, 80, 60),
},
];
// Fresh arrays every call, never push() onto the old ones: a second call would
// leave 8 buildings whose palette slots spill past slot 39 into the cloud,
// overlay, and walkway colors. Array.map returns a new array with one result per
// entry.
this.Demo.buildings: {}buildings = const rawBuildings: {}rawBuildings.map((b: anyb) => ({ x: anyx: b: anyb.x, y: anyy: b: anyb.y, w: anyw: b: anyb.w, h: anyh: b: anyb.h }));
this.Demo.buildingFills: {}buildingFills = const rawBuildings: {}rawBuildings.map((b: anyb) => b: anyb.fill);
this.Demo.buildingOutlines: {}buildingOutlines = const rawBuildings: {}rawBuildings.map((b: anyb) => b: anyb.outline);
this.Demo.clouds: {}clouds = [
{ x: numberx: 30, y: numbery: 24, w: numberw: 42, h: numberh: 14 },
{ x: numberx: 160, y: numbery: 40, w: numberw: 56, h: numberh: 18 },
{ x: numberx: 320, y: numbery: 18, w: numberw: 48, h: numberh: 16 },
{ x: numberx: 480, y: numbery: 36, w: numberw: 50, h: numberh: 15 },
{ x: numberx: 600, y: numbery: 22, w: numberw: 44, h: numberh: 17 },
];
}
// Computes the current ambient tint from the day/night cycle and returns it as a
// Color32, the "color of the light": bright white at midday, dark blue at midnight.
// Called from update() to drive the palette, never from render().
Demo.getAmbientTint(): Color32getAmbientTint() {
const const tick: numbertick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks;
const const cycle: numbercycle = (const tick: numbertick % const DAY_NIGHT_CYCLE_TICKS: 1200DAY_NIGHT_CYCLE_TICKS) / const DAY_NIGHT_CYCLE_TICKS: 1200DAY_NIGHT_CYCLE_TICKS;
// Math.cos returns -1..1; (cos + 1) / 2 gives 0..1 for "how bright is the sun".
const const dayAmount: numberdayAmount = (Math.cos(const cycle: numbercycle * Math.PI * 2) + 1) * 0.5;
// Interpolate from a cool night color to a warm day color.
const const r: anyr = Math.floor(70 + const dayAmount: numberdayAmount * 185);
const const g: anyg = Math.floor(75 + const dayAmount: numberdayAmount * 180);
const const b: anyb = Math.floor(120 + const dayAmount: numberdayAmount * 135);
return new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(const r: anyr, const g: anyg, const b: anyb);
}
// Recalculates every ambient-dependent palette entry. Called every tick in update()
// so render() always has up-to-date slot values.
Demo.updateWorldPalette(): voidupdateWorldPalette() {
const const ambient: Color32ambient = this.Demo.getAmbientTint(): Color32getAmbientTint();
// Sky gradient bands.
for (let let band: numberband = 0; let band: numberband < const SKY_BANDS: 20SKY_BANDS; let band: numberband++) {
// t goes from 0 at the top to 1 near the horizon.
const const t: numbert = let band: numberband / const SKY_BANDS: 20SKY_BANDS;
// applyEasing(t, 'ease-in-out') squashes the transition so it moves slowly
// near the top, speeds up through the middle, then slows again near the
// horizon. Real skies work the same way: a deep uniform color at the zenith,
// a quick shift through the middle bands, then a flatter wash near the
// horizon. lerp(other, t) blends between two Color32 values: t = 0 is all
// skyTop, t = 1 is all skyHorizon.
const const bandBase: Color32bandBase = this.Demo.skyTop: Color32skyTop.Color32.lerp(other: Color32, t: number): Color32Linearly interpolates between this color and another.
Useful for color transitions and gradients.lerp(this.Demo.skyHorizon: Color32skyHorizon, function applyEasing(t: number, easing: EasingFunction): numberApplies an easing curve to a normalized time value.applyEasing(const t: numbert, 'ease-in-out'));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_SKY_BASE: 10C_SKY_BASE + let band: numberband, const bandBase: Color32bandBase.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
}
// Ground.
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GRASS: 30C_GRASS, this.Demo.grassBaseColor: Color32grassBaseColor.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_DIRTLINE: 31C_DIRTLINE, this.Demo.dirtBaseColor: Color32dirtBaseColor.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_DIRT: 47C_DIRT, this.Demo.walkwayDirtBaseColor: Color32walkwayDirtBaseColor.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
// Buildings.
for (let let i: numberi = 0; let i: numberi < this.Demo.buildings: {}buildings.length; let i: numberi++) {
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BUILDING_BASE: 32C_BUILDING_BASE + let i: numberi * 2, this.Demo.buildingFills: {}buildingFills[let i: numberi].multiply(const ambient: Color32ambient));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_BUILDING_BASE: 32C_BUILDING_BASE + let i: numberi * 2 + 1, this.Demo.buildingOutlines: {}buildingOutlines[let i: numberi].multiply(const ambient: Color32ambient));
}
// Clouds.
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CLOUD: 40C_CLOUD, this.Demo.cloudBaseColor: Color32cloudBaseColor.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
// Engine overlay text: a subtle night tint on the score / position / phase rows.
// The kit HUD panels keep their fixed theme colors and does not dim at night.
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HUD_TITLE: 41C_HUD_TITLE, this.Demo.hudTitleBase: Color32hudTitleBase.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HUD_SCORE: 42C_HUD_SCORE, this.Demo.hudScoreBase: Color32hudScoreBase.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HUD_POS: 43C_HUD_POS, this.Demo.hudPosBase: Color32hudPosBase.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HUD_PHASE: 44C_HUD_PHASE, this.Demo.hudFpsBase: Color32hudFpsBase.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient));
// Hero shadow, softer at night.
const const shadowAlpha: anyshadowAlpha = Math.floor(60 + (const ambient: Color32ambient.Color32.r: numberRed channel (0-255).r / 255) * 60);
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HERO_SHADOW: 45C_HERO_SHADOW, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0, const shadowAlpha: anyshadowAlpha));
// Sprite ambient block: each base stone color is multiplied by the current
// ambient to get the lit version. drawSprite uses offset = spriteColorCount so it
// reads from this block.
for (let let i: numberi = 0; let i: numberi < this.Demo.spriteColorCount: numberspriteColorCount; let i: numberi++) {
const const base: anybase = this.Demo.spriteBaseColors: {}spriteBaseColors[let i: numberi];
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const SPRITE_BASE: 70SPRITE_BASE + this.Demo.spriteColorCount: numberspriteColorCount + let i: numberi, const base: anybase.multiply(const ambient: Color32ambient));
}
}
// Draws the sky gradient and clouds using a slower second camera for parallax depth.
Demo.renderSkyLayer(): voidrenderSkyLayer() {
// The sky camera x is only a fraction of the real one, so clouds drift slower
// than the ground. It uses the same interpolated camera position as the ground
// layer, so the two never drift apart from each other frame to frame.
const const paraX: anyparaX = Math.floor(this.Demo.cameraRenderPos: Vector2icameraRenderPos.Vector2i.x: numberHorizontal component (defaults to 0).x * const SKY_PARALLAX: 0.22SKY_PARALLAX);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.cameraSet: (offset: Vector2i) => voidSets the global camera offset applied to subsequent draw calls.cameraSet(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const paraX: anyparaX, 0));
// Each band is GROUND_Y / SKY_BANDS pixels tall.
const const bandH: anybandH = Math.ceil(const GROUND_Y: 188GROUND_Y / const SKY_BANDS: 20SKY_BANDS);
for (let let band: numberband = 0; let band: numberband < const SKY_BANDS: 20SKY_BANDS; let band: numberband++) {
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(0, let band: numberband * const bandH: anybandH, const WORLD_W: 640WORLD_W, const bandH: anybandH);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const C_SKY_BASE: 10C_SKY_BASE + let band: numberband);
}
// Clouds: two overlapping rectangles for a puffy look.
for (const const c: anyc of this.Demo.clouds: {}clouds) {
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const c: anyc.x, const c: anyc.y, const c: anyc.w, const c: anyc.h);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const C_CLOUD: 40C_CLOUD);
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const c: anyc.x + 8, const c: anyc.y - 6, const c: anyc.w - 16, const c: anyc.h - 4);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const C_CLOUD: 40C_CLOUD);
}
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.cameraReset: () => voidResets the global camera offset to `(0, 0)`.cameraReset();
}
// Draws the grass strip, the tile row, and the building blocks in world space.
Demo.renderGroundAndBuildings(): voidrenderGroundAndBuildings() {
// Grass.
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(0, const GROUND_Y: 188GROUND_Y, const WORLD_W: 640WORLD_W, const WORLD_H: 240WORLD_H - const GROUND_Y: 188GROUND_Y);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const C_GRASS: 30C_GRASS);
// Thin dark line at the top of the grass for depth.
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(0, const GROUND_Y: 188GROUND_Y, const WORLD_W: 640WORLD_W, 3);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const C_DIRTLINE: 31C_DIRTLINE);
// Walkway tile row: each cell is a tile ID mapped to a palette color (Tilemap).
this.Demo.renderGroundTileStrip(): voidrenderGroundTileStrip();
// Buildings: checker fill (Patterns) plus an outline frame.
for (let let i: numberi = 0; let i: numberi < this.Demo.buildings: {}buildings.length; let i: numberi++) {
const const b: anyb = this.Demo.buildings: {}buildings[let i: numberi];
const const fillIdx: numberfillIdx = const C_BUILDING_BASE: 32C_BUILDING_BASE + let i: numberi * 2;
const const outlineIdx: numberoutlineIdx = const fillIdx: numberfillIdx + 1;
this.Demo.renderBuildingChecker(building: any, fillIdx: any, outlineIdx: any): voidrenderBuildingChecker(const b: anyb, const fillIdx: numberfillIdx, const outlineIdx: numberoutlineIdx);
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const b: anyb.x, const b: anyb.y, const b: anyb.w, const b: anyb.h);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRect: (rect: Rect2i, paletteIndex: number) => voidDraws an unfilled rectangle outline.drawRect(this.Demo.tempRect: Rect2itempRect, const outlineIdx: numberoutlineIdx);
}
}
// Draws one 16 px tall row of tiles above the grass using the IDs in groundTileIds.
Demo.renderGroundTileStrip(): voidrenderGroundTileStrip() {
const const rowY: numberrowY = const GROUND_Y: 188GROUND_Y - const GROUND_TILE_SIZE: 16GROUND_TILE_SIZE;
for (let let col: numbercol = 0; let col: numbercol < this.Demo.groundTileIds: {}groundTileIds.length; let col: numbercol++) {
const const tileId: anytileId = this.Demo.groundTileIds: {}groundTileIds[let col: numbercol];
const const colorIndex: 30 | 47colorIndex = const tileId: anytileId === const TILE_GRASS_ID: 1TILE_GRASS_ID ? const C_GRASS: 30C_GRASS : const C_DIRT: 47C_DIRT;
const const worldX: numberworldX = let col: numbercol * const GROUND_TILE_SIZE: 16GROUND_TILE_SIZE;
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const worldX: numberworldX, const rowY: numberrowY, const GROUND_TILE_SIZE: 16GROUND_TILE_SIZE, const GROUND_TILE_SIZE: 16GROUND_TILE_SIZE);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const colorIndex: 30 | 47colorIndex);
}
}
// Fills a building with alternating 4x4 blocks (the checker pattern from Patterns).
// `fillIdx` is the palette index for the light squares, `outlineIdx` for the dark
// ones.
Demo.renderBuildingChecker(building: any, fillIdx: any, outlineIdx: any): voidrenderBuildingChecker(building: anybuilding, fillIdx: anyfillIdx, outlineIdx: anyoutlineIdx) {
const const cell: 4cell = const BUILDING_PATTERN_CELL: 4BUILDING_PATTERN_CELL;
for (let let py: anypy = building: anybuilding.y; let py: anypy < building: anybuilding.y + building: anybuilding.h; let py: anypy += const cell: 4cell) {
for (let let px: anypx = building: anybuilding.x; let px: anypx < building: anybuilding.x + building: anybuilding.w; let px: anypx += const cell: 4cell) {
// `>> 2` divides by 4 (the cell size); an even cell sum picks the fill.
const const useFill: booleanuseFill = ((let px: anypx >> 2) + (let py: anypy >> 2)) % 2 === 0;
const const colorIndex: anycolorIndex = const useFill: booleanuseFill ? fillIdx: anyfillIdx : outlineIdx: anyoutlineIdx;
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(let px: anypx, let py: anypy, const cell: 4cell, const cell: 4cell);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const colorIndex: anycolorIndex);
}
}
}
// Moves the rock left and right automatically, turning around at the world edges.
Demo.updateHeroMovement(): voidupdateHeroMovement() {
// Remember where the rock was before this tick moves it, so render() can draw a
// smooth in-between position instead of a pop.
this.Demo.heroPrevPos: Vector2iheroPrevPos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.heroPos: Vector2iheroPos.Vector2i.y: numberVertical component (defaults to 0).y);
let let nextX: numbernextX = this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x + const HERO_SPEED: 1HERO_SPEED * this.Demo.heroFacing: numberheroFacing;
const const margin: 2margin = 2;
if (let nextX: numbernextX <= const margin: 2margin) {
let nextX: numbernextX = const margin: 2margin;
this.Demo.heroFacing: numberheroFacing = 1;
} else if (let nextX: numbernextX + this.Demo.heroSize: Vector2iheroSize.Vector2i.x: numberHorizontal component (defaults to 0).x >= const WORLD_W: 640WORLD_W - const margin: 2margin) {
let nextX: numbernextX = const WORLD_W: 640WORLD_W - this.Demo.heroSize: Vector2iheroSize.Vector2i.x: numberHorizontal component (defaults to 0).x - const margin: 2margin;
this.Demo.heroFacing: numberheroFacing = -1;
}
this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x = let nextX: numbernextX;
}
// Advances the step counter every WALK_FRAME_TICKS ticks, for the subtle bob of the
// rock as it moves.
Demo.updateWalkStep(tick: any): voidupdateWalkStep(tick: anytick) {
if (this.Demo.walkFrameTimer: TimerwalkFrameTimer.Timer.fireIfElapsed(currentTick?: number): booleanReturns true once per interval and advances the internal last-fired tick.fireIfElapsed(tick: anytick)) {
this.Demo.walkStep: numberwalkStep = (this.Demo.walkStep: numberwalkStep + 1) % 4;
}
}
// Draws the rock sprite with a shadow underfoot. The sprite is drawn with
// paletteOffset = spriteColorCount, which shifts each pixel index into the ambient
// block (SPRITE_BASE+N..SPRITE_BASE+2N-1). Those slots are rewritten every tick in
// updateWorldPalette() for the current ambient light, so the rock dims at night.
Demo.renderHero(): voidrenderHero() {
if (!this.Demo.heroSheet: SpriteSheet | nullheroSheet || !this.Demo.heroSprite: Rect2i | nullheroSprite) {
return;
}
// Blend heroPrevPos toward heroPos by BT.renderAlpha so the rock's drawn position
// matches this exact render moment instead of only its last-tick position (same
// idea as the camera blend in render() above).
this.Demo.heroRenderPos: Vector2iheroRenderPos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(
Math.round(this.Demo.heroPrevPos: Vector2iheroPrevPos.Vector2i.x: numberHorizontal component (defaults to 0).x + (this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x - this.Demo.heroPrevPos: Vector2iheroPrevPos.Vector2i.x: numberHorizontal component (defaults to 0).x) * const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha),
Math.round(this.Demo.heroPrevPos: Vector2iheroPrevPos.Vector2i.y: numberVertical component (defaults to 0).y + (this.Demo.heroPos: Vector2iheroPos.Vector2i.y: numberVertical component (defaults to 0).y - this.Demo.heroPrevPos: Vector2iheroPrevPos.Vector2i.y: numberVertical component (defaults to 0).y) * const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.renderAlpha: numberFractional progress between the last completed fixed update and the next.
Intended for interpolating render state between fixed-update steps.renderAlpha),
);
// Tiny vertical bob based on walkStep: steps 0 and 2 are up, 1 and 3 at rest.
const const bob: 0 | -1bob = this.Demo.walkStep: numberwalkStep % 2 === 0 ? -1 : 0;
this.Demo.tempVec: Vector2itempVec.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(this.Demo.heroRenderPos: Vector2iheroRenderPos.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.heroRenderPos: Vector2iheroRenderPos.Vector2i.y: numberVertical component (defaults to 0).y + const bob: 0 | -1bob);
// The ambient offset shifts all pixel indices into the pre-lit block.
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => void (+1 overload)drawSprite(this.Demo.heroSheet: SpriteSheetheroSheet, this.Demo.heroSprite: Rect2iheroSprite, this.Demo.tempVec: Vector2itempVec, this.Demo.spriteColorCount: numberspriteColorCount);
// Shadow underfoot: a 2-pixel strip on the walk line, as wide as the rock's
// visible pixels (x = 4 to 38 in the frame) minus a little at each end. It uses
// heroRenderPos, not tempVec, so it stays on the ground while the rock bobs.
// The engine draws rectangles before sprites, so the rock covers its top row.
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(this.Demo.heroRenderPos: Vector2iheroRenderPos.Vector2i.x: numberHorizontal component (defaults to 0).x + 6, const WALK_LINE_Y: numberWALK_LINE_Y - 1, 30, 2);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const C_HERO_SHADOW: 45C_HERO_SHADOW);
}
// Smoothly follows the hero, then clamps so the view never leaves the world.
Demo.updateCameraFollow(): voidupdateCameraFollow() {
// Remember where the camera was before this tick's follow-lerp moves it.
this.Demo.cameraPrevPos: Vector2icameraPrevPos.Vector2i.set(x: number, y: number): Vector2iSets both components of this vector.
Modifies this vector directly for maximum performance.
WARNING: Mutates this vector. Don't use on frozen/cached singletons.set(this.Demo.cameraPos: Vector2icameraPos.Vector2i.x: numberHorizontal component (defaults to 0).x, this.Demo.cameraPos: Vector2icameraPos.Vector2i.y: numberVertical component (defaults to 0).y);
const const targetCamX: numbertargetCamX = this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x - const DISPLAY_W: 320DISPLAY_W / 2 + this.Demo.heroSize: Vector2iheroSize.Vector2i.x: numberHorizontal component (defaults to 0).x / 2;
this.Demo.cameraXFloat: numbercameraXFloat += (const targetCamX: numbertargetCamX - this.Demo.cameraXFloat: numbercameraXFloat) * const CAMERA_LERP: 0.14CAMERA_LERP;
this.Demo.cameraPos: Vector2icameraPos.Vector2i.x: numberHorizontal component (defaults to 0).x = Math.floor(this.Demo.cameraXFloat: numbercameraXFloat);
this.Demo.clampCamera(): voidclampCamera();
}
// Keeps cameraPos.x between 0 and WORLD_W - DISPLAY_W.
Demo.clampCamera(): voidclampCamera() {
const const clamped: Vector2iclamped = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.cameraClamp: (camera: Vector2i, worldSize: Vector2i, viewSize?: Vector2i) => Vector2iClamps a camera origin so the viewport stays within world bounds.
Uses integer clamping per axis: `[0, worldSize - viewSize]`.
If `viewSize` is omitted, the active
{@link
BT.displaySize
}
is used.cameraClamp(this.Demo.cameraPos: Vector2icameraPos, this.Demo.worldSize: Vector2iworldSize, const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.displaySize: Vector2iActive logical render resolution in pixels.
This is the game/simulation coordinate space configured by the demo, not
the canvas element's CSS size. Each read returns a clone.displaySize);
this.Demo.cameraPos: Vector2icameraPos.Vector2i.x: numberHorizontal component (defaults to 0).x = const clamped: Vector2iclamped.Vector2i.x: numberHorizontal component (defaults to 0).x;
this.Demo.cameraPos: Vector2icameraPos.Vector2i.y: numberVertical component (defaults to 0).y = const clamped: Vector2iclamped.Vector2i.y: numberVertical component (defaults to 0).y;
this.Demo.cameraXFloat: numbercameraXFloat = this.Demo.cameraPos: Vector2icameraPos.Vector2i.x: numberHorizontal component (defaults to 0).x;
}
// +1 score every SCORE_INTERVAL_TICKS.
Demo.updateScore(tick: any): voidupdateScore(tick: anytick) {
if (this.Demo.scoreTimer: TimerscoreTimer.Timer.fireIfElapsed(currentTick?: number): booleanReturns true once per interval and advances the internal last-fired tick.fireIfElapsed(tick: anytick)) {
this.Demo.score: numberscore += 1;
}
}
// Spawns three sparkles near the rock on a fixed schedule.
Demo.updateParticlesSpawn(tick: any): voidupdateParticlesSpawn(tick: anytick) {
if (this.Demo.particleSpawnTimer: TimerparticleSpawnTimer.Timer.fireIfElapsed(currentTick?: number): booleanReturns true once per interval and advances the internal last-fired tick.fireIfElapsed(tick: anytick)) {
for (let let i: numberi = 0; let i: numberi < 3; let i: numberi++) {
const const slot: numberslot = const PARTICLE_SLOT_START: 50PARTICLE_SLOT_START + (this.Demo.nextParticleSlot: numbernextParticleSlot % const MAX_PARTICLES: 20MAX_PARTICLES);
this.Demo.nextParticleSlot: numbernextParticleSlot++;
// Scatter each sparkle around the rock. BT.random.int(min, max) returns a
// whole number from min up to but not including max, so int(-10, 10)
// gives -10 to 9. The y range is lopsided on purpose: most offsets are
// negative, which on screen means upward, so the sparkles gather above
// the rock.
const const ox: numberox = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(-10, 10);
const const oy: numberoy = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run, or open the page with `?seed=N` - applied in `BT.init()`
just before the demo's own `init()`, so a `randomSeed` call there still wins.
Mutating the instance (for example `BT.random.int(10)`) advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(-12, 4);
this.Demo.particles: {}particles.push({
// Spawn each sparkle near the center of the rock (half its real width
// and height in from the top-left corner), plus the random offset
// picked above.
pos: Vector2ipos: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(
this.Demo.heroPos: Vector2iheroPos.Vector2i.x: numberHorizontal component (defaults to 0).x + this.Demo.heroSize: Vector2iheroSize.Vector2i.x: numberHorizontal component (defaults to 0).x / 2 + const ox: numberox,
this.Demo.heroPos: Vector2iheroPos.Vector2i.y: numberVertical component (defaults to 0).y + this.Demo.heroSize: Vector2iheroSize.Vector2i.y: numberVertical component (defaults to 0).y / 2 + const oy: numberoy,
),
spawnTick: anyspawnTick: tick: anytick,
paletteSlot: numberpaletteSlot: const slot: numberslot,
});
}
}
}
// Removes particles older than 72 ticks (1.2 seconds).
Demo.cleanupParticles(tick: any): voidcleanupParticles(tick: anytick) {
this.Demo.particles: {}particles = this.Demo.particles: {}particles.filter((p: anyp) => tick: anytick - p: anyp.spawnTick < 72);
}
// Updates each particle's palette slot with its current color. The hue comes from
// spawnTick and age; the alpha fades out as the particle ages. The ambient tint is
// applied too, so sparkles dim at night.
Demo.updateParticleColors(tick: any): voidupdateParticleColors(tick: anytick) {
const const ambient: Color32ambient = this.Demo.getAmbientTint(): Color32getAmbientTint();
for (const const p: anyp of this.Demo.particles: {}particles) {
const const age: numberage = tick: anytick - const p: anyp.spawnTick;
const const fade: numberfade = 1 - const age: numberage / 72;
const const alpha: anyalpha = Math.floor(200 * const fade: numberfade);
const const hue: numberhue = (const p: anyp.spawnTick * 7 + const age: numberage * 4) % 360;
const const base: Color32base = class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32.Color32.fromHSL(h: number, s: number, l: number, a?: number): Color32Creates a color from HSL values.fromHSL(const hue: numberhue, 90, 65);
const const lit: Color32lit = const base: Color32base.Color32.multiply(other: Color32): Color32Multiplies this color by another color component-wise.
Useful for tinting and color modulation.multiply(const ambient: Color32ambient);
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const p: anyp.paletteSlot, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(const lit: Color32lit.Color32.r: numberRed channel (0-255).r, const lit: Color32lit.Color32.g: numberGreen channel (0-255).g, const lit: Color32lit.Color32.b: numberBlue channel (0-255).b, const alpha: anyalpha));
}
}
// Draws the active particles as 3x3 colored squares. The colors were computed in
// update(); render() only reads the slot.
Demo.renderParticles(): voidrenderParticles() {
for (const const p: anyp of this.Demo.particles: {}particles) {
this.Demo.tempRect: Rect2itempRect.Rect2i.set(x: number, y: number, width: number, height: number): Rect2iSets all components of this rectangle.
Modifies this rectangle directly for maximum performance.
WARNING: Mutates this rectangle. Don't use on frozen/cached singletons.set(const p: anyp.pos.x - 1, const p: anyp.pos.y - 1, 3, 3);
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.tempRect: Rect2itempRect, const p: anyp.paletteSlot);
}
}
// The HUD in screen space, built with the shared UI kit: a small Capstone panel with
// the Save button in the top-left corner, a Score and time-of-day panel in the
// top-right corner, and the "enable sound" hint down on the grass. The two panels
// stay small so most of the sky and clouds stay visible. The Save button also listens
// for the Space key, so keyboard, mouse, and touch all trigger the same PNG capture
// (Image Output).
Demo.renderHud(): voidrenderHud() {
// Anchor the group to the top-left corner; the kit sizes the panel to fit its
// rows. panel() gives the group a background, border, and an amber title line.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT);
import uiui.panel('Capstone');
// The Save button returns true only on the frame it is clicked, tapped, or its
// bound key (Space) is pressed. The `capturing` flag stops a second capture from
// starting while the browser is still writing the first PNG.
if (import uiui.button('Save PNG (Space)', { key: stringkey: 'Space' }) && !this.Demo.capturing: booleancapturing) {
this.Demo.startCapture(): voidstartCapture();
}
// Progress / result line for the capture, in the kit's accent color.
if (this.Demo.capturing: booleancapturing) {
import uiui.label('Capturing...', { color: stringcolor: 'accent' });
} else if (this.Demo.messageTimer: numbermessageTimer > 0) {
import uiui.label(this.Demo.lastCaptureMessage: stringlastCaptureMessage, { color: stringcolor: 'accent' });
}
import uiui.end();
// Score and time of day, the two things a player of this scene would want to
// see. kv() rows line their values up in a column; kvCols: 6 makes the key
// column wide enough for "Score" plus a space.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_RIGHT, { kvCols: numberkvCols: 6 });
import uiui.panel('Status');
import uiui.kv('Score', this.Demo.score: numberscore);
import uiui.kv('Time', this.Demo.getDayPhaseLabel(tick?: number): "Day" | "Toward dusk" | "Night" | "Toward dawn"getDayPhaseLabel());
import uiui.end();
// Browsers keep all sound muted until the player clicks or presses a key. This
// kit row shows the standard warm "enable sound" hint only while the audio is
// still locked, then disappears on its own. It sits on the plain grass in the
// bottom-left corner, out of the way of the sky and the walking rock.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.BOTTOM_LEFT);
import uiui.audioUnlockHint();
import uiui.end();
}
// Starts one PNG download (Image Output). BT.downloadFrame() is asynchronous - it
// hands the browser a file and resolves later - so `capturing` flips on now and the
// result message (shown for 3 seconds via messageTimer) is set when the promise
// settles.
Demo.startCapture(): voidstartCapture() {
this.Demo.capturing: booleancapturing = true;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.downloadFrame: (filename?: string, options?: FrameCaptureOptions) => Promise<void>Captures the next rendered frame and downloads it from the browser.
Convenience wrapper around
{@link
BT.captureFrame
}
that creates a temporary
object URL and clicks a synthetic anchor element. Takes the same `options` as
{@link
BT.captureFrame
}
.downloadFrame('blit386-scene.png')
.then(() => {
this.Demo.lastCaptureMessage: stringlastCaptureMessage = 'Saved: blit386-scene.png';
this.Demo.messageTimer: numbermessageTimer = 180;
this.Demo.capturing: booleancapturing = false;
const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRefPlays a loaded audio clip through the SFX voice pool.
Returns an inert
{@link
SoundRef
}
without allocating a voice when the clip hasn't finished
loading yet (or was already unloaded), when the pool has no free or stealable voice at this
priority, or before the engine has unlocked audio playback.soundPlay(this.Demo.captureBlipClip: AudioClip | nullcaptureBlipClip);
return null;
})
.catch((err: anyerr) => {
this.Demo.lastCaptureMessage: stringlastCaptureMessage = `Error: ${err: anyerr.message}`;
this.Demo.messageTimer: numbermessageTimer = 180;
this.Demo.capturing: booleancapturing = false;
console.error('[GameSceneDemo] Capture failed:', err: anyerr);
});
}
// Human-readable label for the current point in the day/night cycle. `tick` defaults
// to BT.ticks.
Demo.getDayPhaseLabel(tick?: number): "Day" | "Toward dusk" | "Night" | "Toward dawn"getDayPhaseLabel(tick: numbertick = const BT: {
FLIP_H: number;
FLIP_V: number;
ROT_90_CW: number;
ROT_180_CW: number;
ROT_270_CW: number;
BTN_UP: number;
BTN_DOWN: number;
BTN_LEFT: number;
BTN_RIGHT: number;
BTN_A: number;
BTN_B: number;
BTN_X: number;
BTN_Y: number;
BTN_L: number;
BTN_R: number;
BTN_START: number;
BTN_SELECT: number;
BTN_POINTER_A: number;
BTN_POINTER_B: number;
BTN_POINTER_C: number;
BTN_POINTER_D: number;
PLAYER_ONE: number;
PLAYER_TWO: number;
PLAYER_THREE: number;
PLAYER_FOUR: number;
AXIS_LEFT_X: number;
AXIS_LEFT_Y: number;
AXIS_RIGHT_X: number;
AXIS_RIGHT_Y: number;
AXIS_TRIGGER_L: number;
... 124 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.ticks: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks) {
const const phaseTick: numberphaseTick = tick: numbertick % const DAY_NIGHT_CYCLE_TICKS: 1200DAY_NIGHT_CYCLE_TICKS;
if (const phaseTick: numberphaseTick < const DAY_NIGHT_CYCLE_TICKS: 1200DAY_NIGHT_CYCLE_TICKS * 0.25) {
return 'Day';
}
if (const phaseTick: numberphaseTick < const DAY_NIGHT_CYCLE_TICKS: 1200DAY_NIGHT_CYCLE_TICKS * 0.5) {
return 'Toward dusk';
}
if (const phaseTick: numberphaseTick < const DAY_NIGHT_CYCLE_TICKS: 1200DAY_NIGHT_CYCLE_TICKS * 0.75) {
return 'Night';
}
return 'Toward dawn';
}
}
function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>One-liner bootstrap function for BLIT386 demos.
Handles canvas retrieval and engine initialization. Backend selection
(WebGPU or software fallback) is managed internally by BTAPI.
This function provides a streamlined way to start a demo with sensible defaults
while allowing customization through options.bootstrap(class DemoDemo);