// Tilemap: build a grid world from a 2D array and scroll the camera across it.
// @description Build a grid world from a two-dimensional array and scroll a camera across it, drawing visible tiles.
//
// Prerequisites: Basics (https://demos.blit386.dev/basics),
// Primitives (https://demos.blit386.dev/primitives),
// Camera (https://demos.blit386.dev/camera).
// Optional background: Sprites (https://demos.blit386.dev/sprites) also
// places art on a grid, but this demo uses colored rectangles instead of a PNG sheet.
//
// Guide: https://blit386.dev/docs/api/camera
//
// A "tilemap" is like a floor made of same-sized square tiles. Each cell in a 2D array
// (a list of rows, each row a list of columns) stores a small number that means "which
// kind of tile goes here" - like a Lego instruction sheet that says which brick color
// fits each stud. The computer walks the grid with nested loops (one loop for rows,
// one loop for columns) and draws only the tiles the camera can see, which is faster
// than drawing thousands of off-screen tiles nobody would see.
//
// This demo uses a 30 by 20 tile world (480 by 320 pixels at 16 pixels per tile). The
// visible screen is only 320 by 240, so the camera slowly pans so you can explore the
// whole map. A mini-map in the corner shows the full world and the yellow box is the
// part you are looking at right now. Pond water on the main map shimmers (animated
// palette slot C_WATER); the mini-map uses a separate still blue (C_MINIMAP_WATER)
// so the overview stays calm and easy to read. The caption panel in the top-left
// corner is drawn with the shared UI kit (src/shared/ui.js), so it matches the
// panels in every other demo.
import { 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, class Rect2iInteger 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, 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 */
// These numbers are the "tile IDs" stored inside the 2D array.
// Using named constants helps you remember what each number means when you read the map.
const const TILE_SKY: 0TILE_SKY = 0; // Empty air - we skip drawing and let the sky clear color show through.
const const TILE_GRASS: 1TILE_GRASS = 1; // Green ground.
const const TILE_DIRT: 2TILE_DIRT = 2; // Brown earth below or beside grass.
const const TILE_STONE: 3TILE_STONE = 3; // Gray rocks.
const const TILE_WATER: 4TILE_WATER = 4; // Blue water (we animate the shade a little each frame).
const const TILE_TREE_TOP: 5TILE_TREE_TOP = 5; // Dark green tree canopy.
// How many tiles wide and tall the world is. Multiply by TILE_SIZE to get pixel size.
const const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES = 30;
const const MAP_HEIGHT_TILES: 20MAP_HEIGHT_TILES = 20;
// Every tile is a small square on a pixel grid. 16 is a common retro size.
const const TILE_SIZE: 16TILE_SIZE = 16;
// World size in pixels: 30 * 16 = 480 wide, 20 * 16 = 320 tall.
const const WORLD_WIDTH_PX: numberWORLD_WIDTH_PX = const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES * const TILE_SIZE: 16TILE_SIZE;
const const WORLD_HEIGHT_PX: numberWORLD_HEIGHT_PX = const MAP_HEIGHT_TILES: 20MAP_HEIGHT_TILES * const TILE_SIZE: 16TILE_SIZE;
// Each color in this demo has a reserved palette slot (a number from 1 upward).
// Index 0 is always transparent. Giving each slot a name makes the drawing code
// easier to read - "draw in C_GRASS" is clearer than "draw in index 5."
// The caption panel and the mini-map frame use the shared UI kit theme instead,
// which applyTheme() installs into high palette slots (240 and up).
const const C_SKY: 2C_SKY = 2; // Soft sky blue: fills the screen background
const const C_HUD_BAR: 3C_HUD_BAR = 3; // Semi-transparent black: the engine overlay bar color
const const C_TEXT_DIM: 4C_TEXT_DIM = 4; // Dimmed white: the engine overlay text color
const const C_GRASS: 5C_GRASS = 5; // Green: grass tiles
const const C_DIRT: 6C_DIRT = 6; // Brown: dirt tiles
const const C_STONE: 7C_STONE = 7; // Gray: stone/rock tiles
const const C_TREE_TOP: 8C_TREE_TOP = 8; // Dark green: tree canopy tiles
// C_MINIMAP_WATER is a calm, fixed blue for the corner mini-map only. The main view
// uses C_WATER, which update() animates every tick so the pond shimmers on screen.
const const C_MINIMAP_WATER: 9C_MINIMAP_WATER = 9; // Static blue: water on the mini-map (never animated)
const const C_CHART_WARNING: 11C_CHART_WARNING = 11; // Near-white: overlay timing chart warning color
const const C_VIEWPORT: 12C_VIEWPORT = 12; // Yellow: rectangle showing the camera view on the mini-map
const const C_CHART_TAG: 13C_CHART_TAG = 13; // Dim gray: overlay timing chart tag labels
const const C_WATER: 14C_WATER = 14; // DYNAMIC: the animated water tile color, updated every tick in update()
/**
* Shows a scrolling tile-based landscape with a mini-map and animated water.
*
* @implements {IBTDemo}
*/
class class DemoShows a scrolling tile-based landscape with a mini-map and animated water.Demo {
// tilemap is an array of rows. tilemap[row][column] is one cell.
// row 0 is the top of the world; column 0 is the left edge.
// Think of it like a spreadsheet: first index is how far down, second is how far right.
Demo.map: {}map = [];
// cameraPos is the top-left corner of the world (in pixels) that appears at the
// top-left of the screen. When this moves right, the world seems to slide left.
Demo.cameraPos: Vector2icameraPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// Where the camera was at the START of the most recent update() tick, before this
// tick's sine math moved it. render() blends between cameraPrevPos and cameraPos
// using BT.renderAlpha so the camera pans smoothly between physics ticks instead
// of jumping - see "Interpolating render state with renderAlpha" in the engine's
// docs/api-game-loop.md.
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 position,
// so we do not allocate a new Vector2i every frame. Both the actual camera offset
// and the visible-tile culling window use this same value, so they never disagree
// about which pixel row/column the camera is looking at.
Demo.cameraRenderPos: Vector2icameraRenderPos = new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(0, 0);
// palette holds all the colors this demo uses. We fill it in init()
// so the engine knows every color before drawing begins.
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
// Slot map for the shared UI kit theme, filled in init() by applyTheme().
// It tells us which palette slots hold the kit's panel, border, and text colors.
Demo.theme: nulltheme = null;
// One rectangle object we rewrite each time we draw a tile. Reusing it avoids
// making a new Rect2i for every single tile, which would stress the garbage collector.
Demo.tileRect: Rect2itileRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 0, const TILE_SIZE: 16TILE_SIZE, const TILE_SIZE: 16TILE_SIZE);
/**
* @returns {Partial<HardwareSettings>}
*/
Demo.configure(): Partial<HardwareSettings>Optional hook to declare display size, optional output drawing-buffer size,
upscale filter, target fixed-update rate, rendering backend, and overlay.
When omitted, the engine uses
{@link
defaultConfig
}
(`320x240` logical,
`640x480` drawing buffer, `60` FPS, overlay enabled).
When present, you may return only the fields you want to change; the
engine merges them with
{@link
defaultConfig
}
via
{@link
mergeHardwareSettings
}
. Omit `displaySize` to inherit the full
default resolution and output buffer. Include `displaySize` when you
want a custom logical size; optional fields you omit then stay unset
(for example no `drawingBufferSize` means a 1:1 drawing buffer).configure() {
return {
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartDiagnostics: stringoverlayTimingChartDiagnostics: 'rich',
isOverlayRendererDiagnosticsBarEnabled: booleanisOverlayRendererDiagnosticsBarEnabled: true,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const C_HUD_BAR: 3C_HUD_BAR,
textPaletteIndex: numbertextPaletteIndex: const C_TEXT_DIM: 4C_TEXT_DIM,
gapPaletteIndex: numbergapPaletteIndex: const C_HUD_BAR: 3C_HUD_BAR,
},
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_VIEWPORT: 12C_VIEWPORT,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_GRASS: 5C_GRASS,
warningPaletteIndex: numberwarningPaletteIndex: const C_CHART_WARNING: 11C_CHART_WARNING,
errorPaletteIndex: numbererrorPaletteIndex: const C_STONE: 7C_STONE,
tagPaletteIndex: numbertagPaletteIndex: const C_CHART_TAG: 13C_CHART_TAG,
},
};
}
/**
* Runs once at startup: builds the palette and fills the 2D tilemap array.
*
* @returns {Promise<boolean>} True when the demo is ready to run.
*/
async Demo.init(): Promise<boolean>Runs once at startup: builds the palette and fills the 2D tilemap array.init() {
// Set up the color palette
// A palette is like an artist's paint tray - we choose all our colors BEFORE
// drawing anything. Each color gets a number (an "index") that we use in draw calls.
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);
// Static (fixed) colors: these never change from frame to frame.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_SKY: 2C_SKY, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(135, 206, 250)); // soft sky blue
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HUD_BAR: 3C_HUD_BAR, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(0, 0, 0, 185)); // black with some transparency (alpha 185)
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_TEXT_DIM: 4C_TEXT_DIM, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(200, 200, 200)); // dimmed white for the engine overlay
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_GRASS: 5C_GRASS, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(50, 160, 60)); // medium green grass
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_DIRT: 6C_DIRT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(130, 90, 55)); // earthy brown dirt
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_STONE: 7C_STONE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(120, 120, 130)); // cool gray stone
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_TREE_TOP: 8C_TREE_TOP, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(15, 90, 30)); // very dark green canopy
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_MINIMAP_WATER: 9C_MINIMAP_WATER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(30, 110, 200)); // solid blue for mini-map water
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CHART_WARNING: 11C_CHART_WARNING, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(240, 240, 240)); // near-white chart warnings
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_VIEWPORT: 12C_VIEWPORT, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(255, 230, 60)); // yellow camera-viewport box
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_CHART_TAG: 13C_CHART_TAG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(160, 160, 160)); // medium gray chart tag labels
// Dynamic color: the animated water tile. We give it a starting value here so
// there is no empty slot on the very first frame before update() runs.
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_WATER: 14C_WATER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(30, 110, 210)); // initial water blue (updated each tick)
// Install the shared UI kit theme. applyTheme() writes twelve UI colors into
// high palette slots (240 and up), far above this demo's scene slots (1..14),
// and returns a map of slot numbers we can draw with (panel, border, text...).
this.Demo.theme: nulltheme = import applyThemeapplyTheme(this.Demo.palette: Palettepalette);
// Tell the engine to use this palette for all drawing from now on.
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);
// Fill tilemap with a simple outdoor scene: sky, grass, dirt, trees, water, rocks.
this.Demo.buildLandscape(): voidCreates the 2D array and paints a simple slice of nature: sky, ground strip, trees,
a pond at the bottom, and a few stone patches in the water.buildLandscape();
return true;
}
/**
* Runs at a fixed rate (60 Hz) for game logic. Moves the camera and updates the
* animated water color in the palette so render() can use C_WATER as a plain index.
*/
Demo.update(): voidRuns at a fixed rate (60 Hz) for game logic. Moves the camera and updates the
animated water color in the palette so render() can use C_WATER as a plain index.update() {
// Remember where the camera was before this tick's math moves it, so render()
// has an "old" and "new" position to blend between.
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);
// BT.ticks counts how many fixed updates have happened since the demo started.
// Multiplying by a small number makes the wave change slowly over time.
const const t: numbert = 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 * 0.028;
// Math.sin(t) wiggles forever between -1 and +1, like a gentle wave on water.
// We scale and shift it so the camera slides horizontally across most of the map.
// Floor turns the float into a whole pixel position (BLIT386 uses integer pixels).
const const viewSize: Vector2iviewSize = 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;
const const maxCamX: numbermaxCamX = const WORLD_WIDTH_PX: numberWORLD_WIDTH_PX - const viewSize: Vector2iviewSize.Vector2i.x: numberHorizontal component (defaults to 0).x;
const const maxCamY: numbermaxCamY = const WORLD_HEIGHT_PX: numberWORLD_HEIGHT_PX - const viewSize: Vector2iviewSize.Vector2i.y: numberVertical component (defaults to 0).y;
const const centerX: numbercenterX = const maxCamX: numbermaxCamX / 2;
const const centerY: numbercenterY = const maxCamY: numbermaxCamY / 2;
const const amplitudeX: anyamplitudeX = Math.max(0, const maxCamX: numbermaxCamX / 2);
const const amplitudeY: anyamplitudeY = Math.max(0, const maxCamY: numbermaxCamY / 2);
this.Demo.cameraPos: Vector2icameraPos.Vector2i.x: numberHorizontal component (defaults to 0).x = Math.floor(const centerX: numbercenterX + Math.sin(const t: numbert) * const amplitudeX: anyamplitudeX);
this.Demo.cameraPos: Vector2icameraPos.Vector2i.y: numberVertical component (defaults to 0).y = Math.floor(const centerY: numbercenterY + Math.sin(const t: numbert * 0.65) * const amplitudeY: anyamplitudeY);
// Clamp keeps the camera inside the world so you never see empty void past the edge.
this.Demo.cameraPos: Vector2icameraPos = 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, new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const WORLD_WIDTH_PX: numberWORLD_WIDTH_PX, const WORLD_HEIGHT_PX: numberWORLD_HEIGHT_PX), const viewSize: Vector2iviewSize);
// BT.cameraSet() now happens in render(), using a position blended between
// cameraPrevPos and cameraPos - see render() below.
// Update the animated water color in the palette
// Instead of computing a new Color32 inside render() every frame, we compute it
// here in update() and store it in the reserved C_WATER palette slot.
// render() can then just write C_WATER as a plain number - no Color32 needed there.
// This "palette animation" technique is how retro hardware made water shimmer!
const const waterPulse: anywaterPulse = Math.sin(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 * 0.12); // a slow gentle wave between -1 and +1
const const waterBlue: anywaterBlue = Math.floor(210 + const waterPulse: anywaterPulse * 28); // shifts the blue channel 28 units up and down
this.Demo.palette: Palette | nullpalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_WATER: 14C_WATER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(30, 110, const waterBlue: anywaterBlue));
}
/**
* Runs once per monitor refresh. Clears the sky, draws visible tiles, then draws HUD
* in screen space after resetting the camera.
*/
Demo.render(): voidRuns once per monitor refresh. Clears the sky, draws visible tiles, then draws HUD
in screen space after resetting the camera.render() {
// Soft sky blue behind everything. Tiles with ID 0 (sky) are not drawn, so this
// color shows through in "empty" cells.
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_SKY: 2C_SKY);
// 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 camera offset and the visible-tile
// culling window below use this same smoothed value.
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),
);
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);
// Draw the chunk of the world that might be visible right now.
this.Demo.renderVisibleTiles(): voidFigures out which tile rows and columns overlap the screen and draws only those.
That is a simple "culling" optimization: work scales with visible tiles, not the
whole 30x20 map (though for this small map either way would be fine).renderVisibleTiles();
// HUD and mini-map should stick to the screen, not scroll away with the world.
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();
this.Demo.renderHud(): voidDraws labels and the mini-map after the camera is reset so they stay on the screen.renderHud();
}
/**
* Creates the 2D array and paints a simple slice of nature: sky, ground strip, trees,
* a pond at the bottom, and a few stone patches in the water.
*/
Demo.buildLandscape(): voidCreates the 2D array and paints a simple slice of nature: sky, ground strip, trees,
a pond at the bottom, and a few stone patches in the water.buildLandscape() {
// Start with a fresh empty array we will push rows into.
this.Demo.map: {}map = [];
// Outer loop: each row is one horizontal line of tiles from left to right.
for (let let row: numberrow = 0; let row: numberrow < const MAP_HEIGHT_TILES: 20MAP_HEIGHT_TILES; let row: numberrow++) {
// Inner collection for this row's tile IDs.
const const line: {}line = [];
for (let let col: numbercol = 0; let col: numbercol < const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES; let col: numbercol++) {
// Default every cell to sky until we overwrite it below.
const line: {}line.push(const TILE_SKY: 0TILE_SKY);
}
// Attach the finished row to the map (rows stack from top to bottom).
this.Demo.map: {}map.push(const line: {}line);
}
// Ground band: a few rows of grass and dirt in the middle-lower area.
const const grassRow: 9grassRow = 9;
for (let let col: numbercol = 0; let col: numbercol < const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES; let col: numbercol++) {
this.Demo.map: {}map[const grassRow: 9grassRow][let col: numbercol] = const TILE_GRASS: 1TILE_GRASS;
this.Demo.map: {}map[const grassRow: 9grassRow + 1][let col: numbercol] = const TILE_DIRT: 2TILE_DIRT;
this.Demo.map: {}map[const grassRow: 9grassRow + 2][let col: numbercol] = const TILE_DIRT: 2TILE_DIRT;
}
// Small hills of dirt sticking up into the sky on the left and right.
for (let let col: numbercol = 2; let col: numbercol < 8; let col: numbercol++) {
this.Demo.map: {}map[const grassRow: 9grassRow - 1][let col: numbercol] = const TILE_DIRT: 2TILE_DIRT;
}
for (let let col: numbercol = 22; let col: numbercol < 28; let col: numbercol++) {
this.Demo.map: {}map[const grassRow: 9grassRow - 1][let col: numbercol] = const TILE_DIRT: 2TILE_DIRT;
}
// Tree tops sit on top of grass like broccoli on a plate.
const const treeCols: {}treeCols = [4, 5, 14, 15, 16, 25, 26];
for (const const col: anycol of const treeCols: {}treeCols) {
this.Demo.map: {}map[const grassRow: 9grassRow - 1][const col: anycol] = const TILE_TREE_TOP: 5TILE_TREE_TOP;
this.Demo.map: {}map[const grassRow: 9grassRow - 2][const col: anycol] = const TILE_TREE_TOP: 5TILE_TREE_TOP;
}
// Wider tree: two tiles side by side on the second canopy row only.
this.Demo.map: {}map[const grassRow: 9grassRow - 2][6] = const TILE_TREE_TOP: 5TILE_TREE_TOP;
this.Demo.map: {}map[const grassRow: 9grassRow - 2][7] = const TILE_TREE_TOP: 5TILE_TREE_TOP;
// Lower area: more dirt, then water rows filling the bottom of the map.
const const waterTopRow: 14waterTopRow = 14;
for (let let row: numberrow = const grassRow: 9grassRow + 3; let row: numberrow < const waterTopRow: 14waterTopRow; let row: numberrow++) {
for (let let col: numbercol = 0; let col: numbercol < const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES; let col: numbercol++) {
this.Demo.map: {}map[let row: numberrow][let col: numbercol] = const TILE_DIRT: 2TILE_DIRT;
}
}
// Pond: water across the bottom rows.
for (let let row: numberrow = const waterTopRow: 14waterTopRow; let row: numberrow < const MAP_HEIGHT_TILES: 20MAP_HEIGHT_TILES; let row: numberrow++) {
for (let let col: numbercol = 0; let col: numbercol < const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES; let col: numbercol++) {
this.Demo.map: {}map[let row: numberrow][let col: numbercol] = const TILE_WATER: 4TILE_WATER;
}
}
// Stepping stones and little islands (stone replaces water in those cells).
const const stoneSpots: {}stoneSpots = [
[15, 16],
[15, 17],
[16, 16],
[17, 8],
[17, 9],
[18, 20],
[18, 21],
[19, 12],
[19, 13],
[19, 14],
];
for (const [const row: anyrow, const col: anycol] of const stoneSpots: {}stoneSpots) {
this.Demo.map: {}map[const row: anyrow][const col: anycol] = const TILE_STONE: 3TILE_STONE;
}
// One stone tile peeking at the shoreline.
this.Demo.map: {}map[const waterTopRow: 14waterTopRow - 1][10] = const TILE_STONE: 3TILE_STONE;
this.Demo.map: {}map[const waterTopRow: 14waterTopRow - 1][11] = const TILE_STONE: 3TILE_STONE;
}
/**
* Figures out which tile rows and columns overlap the screen and draws only those.
* That is a simple "culling" optimization: work scales with visible tiles, not the
* whole 30x20 map (though for this small map either way would be fine).
*/
Demo.renderVisibleTiles(): voidFigures out which tile rows and columns overlap the screen and draws only those.
That is a simple "culling" optimization: work scales with visible tiles, not the
whole 30x20 map (though for this small map either way would be fine).renderVisibleTiles() {
// How big is the virtual screen in pixels?
const const viewW: numberviewW = 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.Vector2i.x: numberHorizontal component (defaults to 0).x;
const const viewH: numberviewH = 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.Vector2i.y: numberVertical component (defaults to 0).y;
// Camera position is the world pixel at the top-left of the view. Use the same
// render-time (interpolated) position render() just set with BT.cameraSet(),
// so the culling window always agrees with what actually got drawn.
const const camX: numbercamX = this.Demo.cameraRenderPos: Vector2icameraRenderPos.Vector2i.x: numberHorizontal component (defaults to 0).x;
const const camY: numbercamY = this.Demo.cameraRenderPos: Vector2icameraRenderPos.Vector2i.y: numberVertical component (defaults to 0).y;
// Convert pixel edges to tile indices. Math.floor for the left/top tile,
// Math.ceil for the pixel just past the right/bottom edge so we include partial tiles.
const const startCol: anystartCol = Math.max(0, Math.floor(const camX: numbercamX / const TILE_SIZE: 16TILE_SIZE));
const const startRow: anystartRow = Math.max(0, Math.floor(const camY: numbercamY / const TILE_SIZE: 16TILE_SIZE));
const const endCol: anyendCol = Math.min(const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES - 1, Math.ceil((const camX: numbercamX + const viewW: numberviewW) / const TILE_SIZE: 16TILE_SIZE) - 1);
const const endRow: anyendRow = Math.min(const MAP_HEIGHT_TILES: 20MAP_HEIGHT_TILES - 1, Math.ceil((const camY: numbercamY + const viewH: numberviewH) / const TILE_SIZE: 16TILE_SIZE) - 1);
// Nested loops: outer walks down the rows, inner walks across columns.
// This visits every visible cell exactly once.
for (let let row: anyrow = const startRow: anystartRow; let row: anyrow <= const endRow: anyendRow; let row: anyrow++) {
for (let let col: anycol = const startCol: anystartCol; let col: anycol <= const endCol: anyendCol; let col: anycol++) {
// Read which tile kind lives in this grid cell.
const const id: anyid = this.Demo.map: {}map[let row: anyrow][let col: anycol];
// Sky tiles are invisible rectangles; the clear color already painted the sky.
if (const id: anyid === const TILE_SKY: 0TILE_SKY) {
continue;
}
// World pixel position of this tile's top-left corner.
const const worldX: numberworldX = let col: anycol * const TILE_SIZE: 16TILE_SIZE;
const const worldY: numberworldY = let row: anyrow * const TILE_SIZE: 16TILE_SIZE;
// Reuse the same Rect2i: set(x, y, width, height).
this.Demo.tileRect: Rect2itileRect.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 worldY: numberworldY, const TILE_SIZE: 16TILE_SIZE, const TILE_SIZE: 16TILE_SIZE);
// Pick the palette index for this tile ID. if / else if is like a menu: first match wins.
// We pass just a number (C_GRASS, C_DIRT, etc.) - the palette knows the actual color.
if (const id: anyid === const TILE_GRASS: 1TILE_GRASS) {
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.tileRect: Rect2itileRect, const C_GRASS: 5C_GRASS);
} else if (const id: anyid === const TILE_DIRT: 2TILE_DIRT) {
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.tileRect: Rect2itileRect, const C_DIRT: 6C_DIRT);
} else if (const id: anyid === const TILE_STONE: 3TILE_STONE) {
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.tileRect: Rect2itileRect, const C_STONE: 7C_STONE);
} else if (const id: anyid === const TILE_WATER: 4TILE_WATER) {
// C_WATER is the animated water slot - update() already set its color this frame.
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.tileRect: Rect2itileRect, const C_WATER: 14C_WATER);
} else if (const id: anyid === const TILE_TREE_TOP: 5TILE_TREE_TOP) {
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.tileRect: Rect2itileRect, const C_TREE_TOP: 8C_TREE_TOP);
}
}
}
}
/**
* Draws labels and the mini-map after the camera is reset so they stay on the screen.
*/
Demo.renderHud(): voidDraws labels and the mini-map after the camera is reset so they stay on the screen.renderHud() {
// The caption panel is built with the shared UI kit. Widgets declared between
// ui.begin() and ui.end() stack into one anchored group; the kit measures the
// rows, draws the panel background, and places the group for us.
// The kit draws in whatever camera space is active, so this must run AFTER
// BT.cameraReset() - otherwise the panel would scroll away with the world.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT);
import uiui.panel('Tilemap');
import uiui.label('30x20 tiles, 16px each', { color: stringcolor: 'dim' });
import uiui.label('Camera scrolls automatically', { color: stringcolor: 'dim' });
import uiui.end();
// Mini-map sits in the bottom-right, like a treasure map corner-fold.
this.Demo.renderMiniMap(): voidScales the whole tilemap down so each tile is a 3x3 pixel square on the HUD.
Draws a yellow rectangle around the area the main camera is showing.renderMiniMap();
}
/**
* Scales the whole tilemap down so each tile is a 3x3 pixel square on the HUD.
* Draws a yellow rectangle around the area the main camera is showing.
*/
Demo.renderMiniMap(): voidScales the whole tilemap down so each tile is a 3x3 pixel square on the HUD.
Draws a yellow rectangle around the area the main camera is showing.renderMiniMap() {
const const mapX: 218mapX = 218;
const const mapY: 158mapY = 158;
const const scale: 3scale = 3; // Each world tile becomes a 3 by 3 block on the mini-map.
const const mapPixelW: numbermapPixelW = const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES * const scale: 3scale;
const const mapPixelH: numbermapPixelH = const MAP_HEIGHT_TILES: 20MAP_HEIGHT_TILES * const scale: 3scale;
// Backing panel from the shared UI kit (same look as every other demo panel).
// spacer() reserves the tile area; the custom tile overlay draws on top after end().
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { x: numberx: const mapX: 218mapX - 2, y: numbery: const mapY: 158mapY - 2, width: numberwidth: const mapPixelW: numbermapPixelW + 4, margin: numbermargin: 0, pad: numberpad: 2 });
import uiui.panel();
import uiui.spacer(const mapPixelH: numbermapPixelH);
import uiui.end();
// Walk every tile in the entire world (the map is small, so this is cheap).
for (let let row: numberrow = 0; let row: numberrow < const MAP_HEIGHT_TILES: 20MAP_HEIGHT_TILES; let row: numberrow++) {
for (let let col: numbercol = 0; let col: numbercol < const MAP_WIDTH_TILES: 30MAP_WIDTH_TILES; let col: numbercol++) {
const const id: anyid = this.Demo.map: {}map[let row: numberrow][let col: numbercol];
const const px: numberpx = const mapX: 218mapX + let col: numbercol * const scale: 3scale;
const const py: numberpy = const mapY: 158mapY + let row: numberrow * const scale: 3scale;
// Pick a palette index for each tile type.
// Using C_SKY for sky tiles shows the background color as the map background.
// We use C_MINIMAP_WATER for water on the mini-map - this is a static shade,
// not the animated C_WATER, so the mini-map stays calm even as the tiles shimmer.
let let c: numberc = const C_SKY: 2C_SKY;
if (const id: anyid === const TILE_GRASS: 1TILE_GRASS) {
let c: numberc = const C_GRASS: 5C_GRASS;
} else if (const id: anyid === const TILE_DIRT: 2TILE_DIRT) {
let c: numberc = const C_DIRT: 6C_DIRT;
} else if (const id: anyid === const TILE_STONE: 3TILE_STONE) {
let c: numberc = const C_STONE: 7C_STONE;
} else if (const id: anyid === const TILE_WATER: 4TILE_WATER) {
let c: numberc = const C_MINIMAP_WATER: 9C_MINIMAP_WATER;
} else if (const id: anyid === const TILE_TREE_TOP: 5TILE_TREE_TOP) {
let c: numberc = const C_TREE_TOP: 8C_TREE_TOP;
}
this.Demo.tileRect: Rect2itileRect.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 px: numberpx, const py: numberpy, const scale: 3scale, const scale: 3scale);
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.tileRect: Rect2itileRect, let c: numberc);
}
}
// Viewport indicator: where is the 320x240 window inside the 480x320 world?
// Use this.cameraPos, not BT.camera: render() already called BT.cameraReset(),
// which zeroes BT.camera - our own field still remembers the real position.
const const cam: Vector2icam = this.Demo.cameraPos: Vector2icameraPos;
const const disp: Vector2idisp = 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;
const const vx: anyvx = const mapX: 218mapX + Math.floor((const cam: Vector2icam.Vector2i.x: numberHorizontal component (defaults to 0).x / const WORLD_WIDTH_PX: numberWORLD_WIDTH_PX) * const mapPixelW: numbermapPixelW);
const const vy: anyvy = const mapY: 158mapY + Math.floor((const cam: Vector2icam.Vector2i.y: numberVertical component (defaults to 0).y / const WORLD_HEIGHT_PX: numberWORLD_HEIGHT_PX) * const mapPixelH: numbermapPixelH);
const const vw: anyvw = Math.max(1, Math.floor((const disp: Vector2idisp.Vector2i.x: numberHorizontal component (defaults to 0).x / const WORLD_WIDTH_PX: numberWORLD_WIDTH_PX) * const mapPixelW: numbermapPixelW));
const const vh: anyvh = Math.max(1, Math.floor((const disp: Vector2idisp.Vector2i.y: numberVertical component (defaults to 0).y / const WORLD_HEIGHT_PX: numberWORLD_HEIGHT_PX) * const mapPixelH: numbermapPixelH));
this.Demo.tileRect: Rect2itileRect.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 vx: anyvx, const vy: anyvy, const vw: anyvw, const vh: anyvh);
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.tileRect: Rect2itileRect, const C_VIEWPORT: 12C_VIEWPORT);
}
}
// bootstrap() wires this class into the engine: it creates an instance and runs the loop.
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 DemoShows a scrolling tile-based landscape with a mini-map and animated water.Demo);