// Game Scene (CAPSTONE): 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.
//
// This demo brings together everything you have learned!
//
// Prerequisites (do these first):
// Basics https://demos.blit386.dev/basics
// Primitives https://demos.blit386.dev/primitives
// Colors https://demos.blit386.dev/colors
// Fonts https://demos.blit386.dev/fonts
// Pixel Art https://demos.blit386.dev/pixel-art
// 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
//
// Guide: https://blit386.dev/docs
//
// WHAT YOU SEE (how the pieces connect):
// - Sky gradient and slow-moving clouds = colors + parallax idea from starfield.
// - Scrolling ground, tile-ID sidewalk strip (tilemap), checker buildings (patterns) = camera.
// - Moving rock hero = sprites and timing (animation).
// - Sparkles near the rock = small fading squares, like the particles in animation.
// - Day and night = palette-based ambient lighting (sprite-effects); the world dims at night.
// - Background music with a real intro-then-loop point (music), plus a chime on every
// day/night phase change and a blip on a successful PNG capture.
// - Score, rock position, and day phase = engine overlay rows (fonts + built-in FPS bar).
// - A legend panel built with the shared UI kit (src/shared/ui.js) explains the mix and
// holds a Save PNG button (image-output): click it, tap it on a touchscreen, or press Space.
//
// HOW THE DAY/NIGHT PALETTE WORKS:
//
// Instead of computing `base.multiply(ambient)` in render() and passing a Color32 to draw
// calls, we pre-compute those multiplied colors in update() and store them in dedicated
// palette slots. render() only ever uses palette index numbers.
//
// `Color32.multiply()` is a built-in engine method: it scales each channel of `base` by
// the matching channel of `ambient` and returns a new Color32. Think of it as shining a
// colored flashlight on a surface - a blue light on a red wall gives you a darker,
// purple-ish result.
//
// Example: the grass fill has a base color (50, 140, 70) and a reserved slot C_GRASS.
// Every tick in update(), we compute `grassBase.multiply(ambient)` and write it to C_GRASS.
// render() calls `BT.drawRectFill(rect, C_GRASS)` - no Color32 needed there.
//
// Think of it as updating the paint cans before the painter starts working.
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;
... 106 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').IBTDemo} IBTDemo */
/** @typedef {import('blit386').HardwareSettings} HardwareSettings */
/** @typedef {import('blit386').Palette} Palette */
/** @typedef {import('blit386').SpriteSheet} SpriteSheet */
/** @typedef {import('blit386').Rect2i} Rect2i */
// Internal game resolution.
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 sidewalk / grass starts.
const const GROUND_Y: 188GROUND_Y = 188;
// One row of 16 px tiles along the sidewalk (Tilemap idea: 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;
// Checker squares inside buildings (Patterns idea: repeating blocks, no images).
const const BUILDING_PATTERN_CELL: 4BUILDING_PATTERN_CELL = 4;
// How fast the rock moves along X each update tick.
const const HERO_SPEED: 1HERO_SPEED = 1;
// Walk frame timer: advance the "step" counter every 8 ticks.
const const WALK_FRAME_TICKS: 8WALK_FRAME_TICKS = 8;
// Score +1 every 60 ticks (~1 second).
const const SCORE_INTERVAL_TICKS: 60SCORE_INTERVAL_TICKS = 60;
// Sparkle particle batch every 30 ticks.
const const PARTICLE_SPAWN_INTERVAL: 30PARTICLE_SPAWN_INTERVAL = 30;
// Full day/night loop = 1200 ticks (~20 seconds at 60 FPS).
const const DAY_NIGHT_CYCLE_TICKS: 1200DAY_NIGHT_CYCLE_TICKS = 1200;
// Camera smooth factor: each tick we step 14% closer to the 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 (see Music for the same track used 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% of the real camera speed (parallax illusion).
const const SKY_PARALLAX: 0.22SKY_PARALLAX = 0.22;
// How many sky bands cover the height of the sky (from 0 to GROUND_Y).
const const SKY_BANDS: 20SKY_BANDS = 20;
// Maximum live particles at once.
const const MAX_PARTICLES: 20MAX_PARTICLES = 20;
// Static scene slots (never change after init).
// UI text and panel colors now 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 (updated 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.
const const C_GRASS: 30C_GRASS = 30;
const const C_DIRTLINE: 31C_DIRTLINE = 31;
// Buildings: 4 buildings × 2 slots (fill + outline) = 8 slots at 32..39.
const const C_BUILDING_BASE: 32C_BUILDING_BASE = 32;
// Clouds: 1 slot at 40.
const const C_CLOUD: 40C_CLOUD = 40;
// HUD text: 41..44.
const const C_HUD_TITLE: 41C_HUD_TITLE = 41;
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 below).
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 sprite colors: 70+N..70+2N-1.
const const SPRITE_BASE: 70SPRITE_BASE = 70;
/**
* One self-running mini scene: walking rock, following camera, HUD, day/night, sparkles.
* All color computation happens in update(); render() uses only palette indices.
*
* @implements {IBTDemo}
*/
class class DemoOne self-running mini scene: walking rock, following camera, HUD, day/night, sparkles.
All color computation happens in update(); render() uses only palette indices.Demo {
// The palette holds all colors used in this demo.
/** @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;
/** @type {AudioClip | null} Looping background music with a distinct intro section. */
Demo.musicClip: AudioClip | nullmusicClip = null;
/** @type {AudioClip | null} Short chime played on every day/night phase change. */
Demo.dayPhaseChimeClip: AudioClip | nulldayPhaseChimeClip = null;
/** @type {AudioClip | null} Confirmation blip played after a successful PNG capture. */
Demo.captureBlipClip: AudioClip | nullcaptureBlipClip = null;
/** @type {string | null} Day phase label as of the previous tick, used to detect changes. */
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 guessing a
// number here - keeps movement bounds, camera centering, and particle spawns
// matching what is actually drawn on screen.
/** @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 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 true 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 smoothly between physics ticks instead of
// jumping - see "Interpolating render state with renderAlpha" in the engine's
// docs/api-game-loop.md. init() snaps this to match 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 - just bobs the rock position slightly).
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 render-time (interpolated) camera and hero
// positions, so we do not allocate new Vector2i instances every 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 for smooth lerp without pixel jitter.
Demo.cameraXFloat: numbercameraXFloat = 0;
// Simple score counter.
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 one sidewalk row (tilemap): each entry is TILE_GRASS_ID or TILE_DIRT_ID.
Demo.groundTileIds: {}groundTileIds = [];
// PNG capture state (image-output): the legend'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 and clouds (used in update() for ambient multiplication).
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);
// Reused rectangle and vector 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 band colors: top and horizon base values for the gradient.
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);
// HUD text colors (base values before 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 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 (FPS strip uses the engine defaults).
*
* The live palette grid at the bottom shows which slots this frame's draw calls
* use (helpful for day/night tinting and sprite palette blocks). Thirty-two swatches
* per row, three visible rows; scroll to browse the full 256-slot palette.
*
* @returns {Partial<HardwareSettings>}
*/
Demo.configure(): Partial<HardwareSettings>Palette slots for the engine overlay bars (FPS strip uses the engine defaults).
The live palette grid at the bottom shows which slots this frame's draw calls
use (helpful for day/night tinting and sprite palette blocks). Thirty-two swatches
per row, three visible rows; scroll to browse the full 256-slot palette.configure() {
return {
// The engine normally shows a tiny "~" toggle hint in the bottom-left
// corner so people know they can press the Backquote key (`) to open the
// stats overlay. This is an immersive game scene, so we hide that hint to
// keep the picture clean. The overlay still works: press ` to reveal the
// full dev HUD (timing chart and palette grid) on demand, then ` again to
// hide it. Teaching demos leave this hint visible (the default) so newcomers
// can find it.
isOverlayToggleHintVisible: booleanisOverlayToggleHintVisible: false,
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, and places buildings and clouds.
*
* @returns {Promise<boolean>}
*/
async Demo.init(): Promise<boolean>Loads the hero sprite sheet, builds the palette, and places buildings and clouds.init() {
console.log('[GameSceneDemo] Initializing...');
// Create palette and set 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 top out around slot 110 at the sprite ambient block).
// This demo never needs the returned slot map, 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;
... 106 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));
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)); // overlay row backgrounds
// Pre-fill 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 world decoration and the sidewalk tile-ID row (Tilemap demo).
this.Demo.buildWorldDecor(): voidPlaces buildings and clouds once at startup.
Stores their base colors in separate arrays so update() can apply ambient.buildWorldDecor();
this.Demo.buildGroundTileStrip(): voidFills groundTileIds with alternating grass/dirt tile IDs for one 16 px row (Tilemap demo).buildGroundTileStrip();
// Extract sprite colors and register in palette
// Ask the engine to scan the PNG and add every unique color it finds into our palette,
// starting at SPRITE_BASE. The returned array is the same colors in palette-write order
// (sorted darkest-first by brightness). We keep them 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() will recalculate these every tick based on 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 hero sprite
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";
}): 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;
... 106 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-lowres 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 ground line: the sprite's top-left Y is the ground
// minus the sprite's height, so its bottom edge touches GROUND_Y exactly.
this.Demo.heroPos: Vector2iheroPos.Vector2i.y: numberVertical component (defaults to 0).y = const GROUND_Y: 188GROUND_Y - this.Demo.heroSize: Vector2iheroSize.Vector2i.y: numberVertical component (defaults to 0).y;
// 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 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(): voidKeeps cameraPos.x between 0 and WORLD_W - DISPLAY_W.clampCamera();
// 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);
// Load and start all sound: music, the day/night chime, and the capture blip.
await this.Demo.initAudio(): anyLoads 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 clear step.initAudio();
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 clear step.
*/
async Demo.initAudio(): anyLoads 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 clear step.initAudio() {
// Background music: a real intro-then-loop track, the same one Music
// demonstrates 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.
//
// Wrap load + play like 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;
... 106 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 rising 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;
... 106 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): stringHuman-readable label for where we are in the day/night cycle.getDayPhaseLabel();
}
/**
* Fixed-step logic: moves the rock, camera, score, and particles,
* then recalculates all ambient-lit palette colors.
*/
Demo.update(): voidFixed-step logic: moves the rock, camera, score, and particles,
then recalculates all ambient-lit palette colors.update() {
// 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;
... 106 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: number): voidPlays 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 - we
only care about the moment something changes, not every tick it stays the same.updateDayPhaseSound(const tick: numbertick);
// The PNG capture itself now starts from the kit's Save button in renderLegend().
// Here we only count down the "Saved: ..." message so it disappears after a while.
if (this.Demo.messageTimer: numbermessageTimer > 0) {
this.Demo.messageTimer: numbermessageTimer--;
}
this.Demo.updateHeroMovement(): voidMoves the rock left/right automatically, bouncing off world edges.updateHeroMovement();
this.Demo.updateWalkStep(tick: number): voidAdvances a step counter every WALK_FRAME_TICKS ticks.
Used to add a subtle bob to the rock as it moves.updateWalkStep(const tick: numbertick);
this.Demo.updateCameraFollow(): voidSmoothly follows the hero, then clamps so the view never leaves the world.updateCameraFollow();
this.Demo.updateScore(tick: number): void+1 score every SCORE_INTERVAL_TICKS.updateScore(const tick: numbertick);
this.Demo.updateParticlesSpawn(tick: number): voidSpawns a handful of sparkles near the rock on a fixed schedule.updateParticlesSpawn(const tick: numbertick);
this.Demo.cleanupParticles(tick: number): voidRemoves particles older than 72 ticks (~1.2 seconds).cleanupParticles(const tick: numbertick);
this.Demo.updateParticleColors(tick: number): voidUpdates each particle's palette slot with its current color.
Hue comes from spawnTick; alpha fades out as the particle ages.
The ambient tint is also applied so sparkles dim at night.updateParticleColors(const tick: numbertick);
// Recalculate all ambient-lit palette colors.
// This is the core of the day/night system.
this.Demo.updateWorldPalette(): voidRecalculates all ambient-dependent palette entries.
Called every tick in update() so render() always has up-to-date slot values.updateWorldPalette();
}
/**
* 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 - we
* only care about the moment something changes, not every tick it stays the same.
*
* @param {number} tick - Current engine tick (BT.ticks).
*/
Demo.updateDayPhaseSound(tick: number): voidPlays 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 - we
only care about the moment something changes, not every tick it stays the same.updateDayPhaseSound(tick: number- Current engine tick (BT.ticks).tick) {
const const currentPhase: stringcurrentPhase = this.Demo.getDayPhaseLabel(tick?: number): stringHuman-readable label for where we are in the day/night cycle.getDayPhaseLabel(tick: number- Current engine tick (BT.ticks).tick);
if (this.Demo.lastDayPhaseLabel: string | nulllastDayPhaseLabel !== null && const currentPhase: stringcurrentPhase !== 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;
... 106 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: stringcurrentPhase;
}
/**
* Draws world layers back-to-front. HUD text is handled by the engine overlay.
* Every draw call uses only palette index numbers - no Color32 objects.
*/
Demo.render(): voidDraws world layers back-to-front. HUD text is handled by the engine overlay.
Every draw call uses only palette index numbers - no Color32 objects.render() {
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;
... 106 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 below 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;
... 106 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;
... 106 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(): voidDraws the sky gradient and clouds using a slower fake camera for parallax depth.renderSkyLayer();
// Layer 2: world with 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;
... 106 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(): voidDraws the grass strip and building blocks in world space.renderGroundAndBuildings();
this.Demo.renderParticles(): voidDraws active particles as 3x3 colored squares.
Colors were already computed in update() - render() just reads the slot.renderParticles();
this.Demo.renderHero(): voidDraws 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 updated every tick in updateWorldPalette() to reflect the current
ambient light, so the rock automatically dims at night.renderHero();
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.cameraReset: () => voidResets the global camera offset to `(0, 0)`.cameraReset();
// First-time legend and capture hint (screen space, not scrolled with the world).
this.Demo.renderLegend(): voidShort legend for first-time viewers (screen space, top-left), built with the shared
UI kit. The Save button also listens for the Space key, so keyboard, mouse, and
touch all trigger the same PNG capture (Image Output demo).renderLegend();
// 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.
* Text colors are updated each tick in updateWorldPalette() so they dim at night.
*
* @returns {readonly { leftText: string, rightText?: string }[]}
*/
Demo.overlayRows(): readonly {
leftText: string;
rightText?: string;
}[]
Score, rock position, and day/night phase for the engine overlay.
Text colors are updated each tick in updateWorldPalette() so they dim at night.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): stringHuman-readable label for where we are in the day/night cycle.getDayPhaseLabel();
return this.Demo.overlayRowData: {}overlayRowData;
}
/**
* Fills groundTileIds with alternating grass/dirt tile IDs for one 16 px row (Tilemap demo).
*/
Demo.buildGroundTileStrip(): voidFills groundTileIds with alternating grass/dirt tile IDs for one 16 px row (Tilemap demo).buildGroundTileStrip() {
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.
* Stores their base colors in separate arrays so update() can apply ambient.
*/
Demo.buildWorldDecor(): voidPlaces buildings and clouds once at startup.
Stores their base colors in separate arrays so update() can apply ambient.buildWorldDecor() {
const const rawBuildings: {}rawBuildings = [
{ x: numberx: 40, y: numbery: const GROUND_Y: 188GROUND_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 GROUND_Y: 188GROUND_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 GROUND_Y: 188GROUND_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 GROUND_Y: 188GROUND_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),
},
];
for (const const b: anyb of const rawBuildings: {}rawBuildings) {
this.Demo.buildings: {}buildings.push({ x: anyx: const b: anyb.x, y: anyy: const b: anyb.y, w: anyw: const b: anyb.w, h: anyh: const b: anyb.h });
this.Demo.buildingFills: {}buildingFills.push(const b: anyb.fill);
this.Demo.buildingOutlines: {}buildingOutlines.push(const 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 based on the day/night cycle.
* Returns a Color32 that represents the current "color of the light".
* Bright white = midday; dark blue = midnight.
*
* This is called in update() to drive the palette, not in render().
*
* @returns {Color32} The ambient light color.
*/
Demo.getAmbientTint(): Color32Computes the current ambient tint based on the day/night cycle.
Returns a Color32 that represents the current "color of the light".
Bright white = midday; dark blue = midnight.
This is called in update() to drive the palette, not in render().getAmbientTint() {
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;
... 106 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 all ambient-dependent palette entries.
* Called every tick in update() so render() always has up-to-date slot values.
*/
Demo.updateWorldPalette(): voidRecalculates all ambient-dependent palette entries.
Called every tick in update() so render() always has up-to-date slot values.updateWorldPalette() {
const const ambient: Color32ambient = this.Demo.getAmbientTint(): Color32Computes the current ambient tint based on the day/night cycle.
Returns a Color32 that represents the current "color of the light".
Bright white = midday; dark blue = midnight.
This is called in update() to drive the palette, not in render().getAmbientTint();
// Sky gradient bands
for (let let band: numberband = 0; let band: numberband < const SKY_BANDS: 20SKY_BANDS; let band: numberband++) {
// t goes 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') squishes 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
// color shift through the middle bands, then a flatter wash near the horizon.
// lerp(other, t) blends smoothly between two Color32 values at position t,
// where t=0 is all skyTop and 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));
// 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 (subtle night tint on the score / position / phase rows).
// The kit legend panel keeps its 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
const const shadowAlpha: anyshadowAlpha = Math.floor(60 + (const ambient: Color32ambient.Color32.r: numberRed channel (0-255).r / 255) * 60); // Softer at night.
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 "ambient 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 fake camera for parallax depth.
*/
Demo.renderSkyLayer(): voidDraws the sky gradient and clouds using a slower fake camera for parallax depth.renderSkyLayer() {
// Fake camera X is only a fraction of the real one: clouds drift slower than ground.
// Uses the same render-time (interpolated) camera position as the ground layer
// below, 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;
... 106 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;
... 106 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;
... 106 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;
... 106 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;
... 106 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 and building blocks in world space.
*/
Demo.renderGroundAndBuildings(): voidDraws the grass strip and building blocks in world space.renderGroundAndBuildings() {
// 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;
... 106 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;
... 106 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);
// Sidewalk tile row: each cell is a tile ID mapped to a palette color (Tilemap demo).
this.Demo.renderGroundTileStrip(): voidDraws one 16 px tall row of tiles above the grass using tile IDs from groundTileIds.renderGroundTileStrip();
// Buildings: checker fill (Patterns demo) plus 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: {
x: number;
y: number;
w: number;
h: number;
}, fillIdx: number, outlineIdx: number): void
Fills a building with alternating 4x4 blocks (checker pattern from Patterns demo).renderBuildingChecker(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;
... 106 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 tile IDs from groundTileIds.
*/
Demo.renderGroundTileStrip(): voidDraws one 16 px tall row of tiles above the grass using tile IDs from groundTileIds.renderGroundTileStrip() {
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 | 31colorIndex = const tileId: anytileId === const TILE_GRASS_ID: 1TILE_GRASS_ID ? const C_GRASS: 30C_GRASS : const C_DIRTLINE: 31C_DIRTLINE;
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;
... 106 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 | 31colorIndex);
}
}
/**
* Fills a building with alternating 4x4 blocks (checker pattern from Patterns demo).
*
* @param {{ x: number, y: number, w: number, h: number }} building
* @param {number} fillIdx palette index for "light" squares
* @param {number} outlineIdx palette index for "dark" squares
*/
Demo.renderBuildingChecker(building: {
x: number;
y: number;
w: number;
h: number;
}, fillIdx: number, outlineIdx: number): void
Fills a building with alternating 4x4 blocks (checker pattern from Patterns demo).renderBuildingChecker(building: {
x: number;
y: number;
w: number;
h: number;
}
building, fillIdx: numberpalette index for "light" squaresfillIdx, outlineIdx: numberpalette index for "dark" squaresoutlineIdx) {
const const cell: 4cell = const BUILDING_PATTERN_CELL: 4BUILDING_PATTERN_CELL;
for (let let py: numberpy = building: {
x: number;
y: number;
w: number;
h: number;
}
building.y: numbery; let py: numberpy < building: {
x: number;
y: number;
w: number;
h: number;
}
building.y: numbery + building: {
x: number;
y: number;
w: number;
h: number;
}
building.h: numberh; let py: numberpy += const cell: 4cell) {
for (let let px: numberpx = building: {
x: number;
y: number;
w: number;
h: number;
}
building.x: numberx; let px: numberpx < building: {
x: number;
y: number;
w: number;
h: number;
}
building.x: numberx + building: {
x: number;
y: number;
w: number;
h: number;
}
building.w: numberw; let px: numberpx += const cell: 4cell) {
const const useFill: booleanuseFill = ((let px: numberpx >> 2) + (let py: numberpy >> 2)) % 2 === 0;
const const colorIndex: numbercolorIndex = const useFill: booleanuseFill ? fillIdx: numberpalette index for "light" squaresfillIdx : outlineIdx: numberpalette index for "dark" squaresoutlineIdx;
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: numberpx, let py: numberpy, 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;
... 106 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: numbercolorIndex);
}
}
}
/**
* Moves the rock left/right automatically, bouncing off world edges.
*/
Demo.updateHeroMovement(): voidMoves the rock left/right automatically, bouncing off world edges.updateHeroMovement() {
// 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 a step counter every WALK_FRAME_TICKS ticks.
* Used to add a subtle bob to the rock as it moves.
*
* @param {number} tick - Current tick.
*/
Demo.updateWalkStep(tick: number): voidAdvances a step counter every WALK_FRAME_TICKS ticks.
Used to add a subtle bob to the rock as it moves.updateWalkStep(tick: number- Current tick.tick) {
if (this.Demo.walkFrameTimer: TimerwalkFrameTimer.Timer.fireIfElapsed(currentTick?: number): booleanReturns true once per interval and advances the internal last-fired tick.fireIfElapsed(tick: number- Current tick.tick)) {
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 updated every tick in updateWorldPalette() to reflect the current
* ambient light, so the rock automatically dims at night.
*/
Demo.renderHero(): voidDraws 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 updated every tick in updateWorldPalette() to reflect the current
ambient light, so the rock automatically dims at night.renderHero() {
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;
... 106 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;
... 106 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,2 are up; 1,3 are 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawSprite: (spriteSheet: SpriteSheet, srcRect: Rect2i, destPos: Vector2i, paletteOffset?: number) => voidDraws a sprite region from an indexed sprite sheet.
Sprite draws are batched internally. Grouping draws from the same
{@link
SpriteSheet
}
minimizes batch flushes and reduces GPU state changes.
The sprite sheet must have been converted to palette indices via
`spriteSheet.indexize(palette)` before the first draw call. Prefer
`SpriteSheet.loadIndexed(...)` for one-call setup.
**Palette offset semantics:** Sprite pixels are stored as palette indices starting at 1.
Index 0 is always transparent and is discarded by the fragment shader. The final palette
lookup is `storedIndex + paletteOffset`, so:
- `paletteOffset = 0` (default): a sprite pixel stored at index 1 renders as `palette[1]`.
`palette[0]` is never reachable because stored indices start at 1.
- `paletteOffset = N`: shifts the entire sprite's color range up by N slots. A pixel stored
at index 1 renders as `palette[1 + N]`, a pixel at index 2 renders as `palette[2 + N]`,
and so on. Use this for palette-swap effects such as team colors or damage flashes.
**Out-of-range behavior:** No CPU-side validation is performed. `paletteOffset` is passed to
the GPU as a `u32`. If `storedIndex + paletteOffset` exceeds the last palette index, WebGPU's
robust buffer access returns 0 for every component; because the fragment shader forces alpha
to 1.0, the affected pixels render as opaque black. Negative values are forbidden - a negative
JS number written into a `u32` vertex attribute wraps to a large unsigned integer, which also
produces out-of-bounds black pixels.drawSprite(this.Demo.heroSheet: SpriteSheetheroSheet, this.Demo.heroSprite: Rect2iheroSprite, this.Demo.tempVec: Vector2itempVec, this.Demo.spriteColorCount: numberspriteColorCount);
// Shadow underfoot.
const const shadowY: numbershadowY = this.Demo.heroRenderPos: Vector2iheroRenderPos.Vector2i.y: numberVertical component (defaults to 0).y + this.Demo.heroSprite: Rect2iheroSprite.Rect2i.height: numberHeight in pixels (defaults to 0).height - 2;
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 + 2, const shadowY: numbershadowY, this.Demo.heroSprite: Rect2iheroSprite.Rect2i.width: numberWidth in pixels (defaults to 0).width - 4, 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;
... 106 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(): voidSmoothly follows the hero, then clamps so the view never leaves the world.updateCameraFollow() {
// 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(): voidKeeps cameraPos.x between 0 and WORLD_W - DISPLAY_W.clampCamera();
}
/**
* Keeps cameraPos.x between 0 and WORLD_W - DISPLAY_W.
*/
Demo.clampCamera(): voidKeeps cameraPos.x between 0 and WORLD_W - DISPLAY_W.clampCamera() {
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;
... 106 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;
... 106 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.
*
* @param {number} tick - Current tick.
*/
Demo.updateScore(tick: number): void+1 score every SCORE_INTERVAL_TICKS.updateScore(tick: number- Current tick.tick) {
if (this.Demo.scoreTimer: TimerscoreTimer.Timer.fireIfElapsed(currentTick?: number): booleanReturns true once per interval and advances the internal last-fired tick.fireIfElapsed(tick: number- Current tick.tick)) {
this.Demo.score: numberscore += 1;
}
}
/**
* Spawns a handful of sparkles near the rock on a fixed schedule.
*
* @param {number} tick - Current tick.
*/
Demo.updateParticlesSpawn(tick: number): voidSpawns a handful of sparkles near the rock on a fixed schedule.updateParticlesSpawn(tick: number- Current tick.tick) {
if (this.Demo.particleSpawnTimer: TimerparticleSpawnTimer.Timer.fireIfElapsed(currentTick?: number): booleanReturns true once per interval and advances the internal last-fired tick.fireIfElapsed(tick: number- Current tick.tick)) {
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 is the engine's shared random number generator, and int() returns a whole number starting
// at the first value and stopping just before the second - so int(-10, 10) gives anything from -10 to
// 9, and 10 itself never comes up.
// Left and right come up about equally often. The y range is lopsided on purpose: most of those
// 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;
... 106 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. 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;
... 106 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. 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: numberspawnTick: tick: number- Current tick.tick,
paletteSlot: numberpaletteSlot: const slot: numberslot,
});
}
}
}
/**
* Removes particles older than 72 ticks (~1.2 seconds).
*
* @param {number} tick - Current tick.
*/
Demo.cleanupParticles(tick: number): voidRemoves particles older than 72 ticks (~1.2 seconds).cleanupParticles(tick: number- Current tick.tick) {
this.Demo.particles: {}particles = this.Demo.particles: {}particles.filter((p: anyp) => tick: number- Current tick.tick - p: anyp.spawnTick < 72);
}
/**
* Updates each particle's palette slot with its current color.
* Hue comes from spawnTick; alpha fades out as the particle ages.
* The ambient tint is also applied so sparkles dim at night.
*
* @param {number} tick - Current tick.
*/
Demo.updateParticleColors(tick: number): voidUpdates each particle's palette slot with its current color.
Hue comes from spawnTick; alpha fades out as the particle ages.
The ambient tint is also applied so sparkles dim at night.updateParticleColors(tick: number- Current tick.tick) {
const const ambient: Color32ambient = this.Demo.getAmbientTint(): Color32Computes the current ambient tint based on the day/night cycle.
Returns a Color32 that represents the current "color of the light".
Bright white = midday; dark blue = midnight.
This is called in update() to drive the palette, not in render().getAmbientTint();
for (const const p: anyp of this.Demo.particles: {}particles) {
const const age: numberage = tick: number- Current tick.tick - 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 active particles as 3x3 colored squares.
* Colors were already computed in update() - render() just reads the slot.
*/
Demo.renderParticles(): voidDraws active particles as 3x3 colored squares.
Colors were already computed in update() - render() just reads the slot.renderParticles() {
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;
... 106 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);
}
}
/**
* Short legend for first-time viewers (screen space, top-left), built with the shared
* UI kit. The Save button also listens for the Space key, so keyboard, mouse, and
* touch all trigger the same PNG capture (Image Output demo).
*/
Demo.renderLegend(): voidShort legend for first-time viewers (screen space, top-left), built with the shared
UI kit. The Save button also listens for the Space key, so keyboard, mouse, and
touch all trigger the same PNG capture (Image Output demo).renderLegend() {
// Anchor the group to the top-left corner; the kit sizes the panel to fit its rows.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT);
// panel() gives the group a background, border, and an amber title line.
import uiui.panel('Capstone: scroll, tiles, sprite, day/night');
// A dim (secondary) line pointing back at the demos these visuals come from.
import uiui.label('Tiles + checker = tilemap + patterns', { color: stringcolor: 'dim' });
// 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(): voidStarts one PNG download (Image Output demo). BT.downloadFrame() is asynchronous - it hands
the browser a file and resolves later - so we flip `capturing` on now and set the
result message (shown for ~3 seconds via messageTimer) when the promise settles.startCapture();
}
// Progress / result line for the capture, in the kit's green 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' });
}
// 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.
import uiui.audioUnlockHint();
import uiui.end();
}
/**
* Starts one PNG download (Image Output demo). BT.downloadFrame() is asynchronous - it hands
* the browser a file and resolves later - so we flip `capturing` on now and set the
* result message (shown for ~3 seconds via messageTimer) when the promise settles.
*/
Demo.startCapture(): voidStarts one PNG download (Image Output demo). BT.downloadFrame() is asynchronous - it hands
the browser a file and resolves later - so we flip `capturing` on now and set the
result message (shown for ~3 seconds via messageTimer) when the promise settles.startCapture() {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.downloadFrame: (filename?: string) => 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.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;
... 106 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 where we are in the day/night cycle.
*
* @param {number} [tick] - Tick to evaluate (defaults to {@link BT.ticks}).
* @returns {string}
*/
Demo.getDayPhaseLabel(tick?: number): stringHuman-readable label for where we are in the day/night cycle.getDayPhaseLabel(tick: number | undefined- Tick to evaluate (defaults to
{@link
BT.ticks
}
).tick = 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;
... 106 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: number- Tick to evaluate (defaults to
{@link
BT.ticks
}
).tick % 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 DemoOne self-running mini scene: walking rock, following camera, HUD, day/night, sparkles.
All color computation happens in update(); render() uses only palette indices.Demo);