Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
8.7 kB · 253 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254// ---------------------------------------------------------------------------// Engine data contract — the render-ready *MapExport* shape and its primitives.//// This is the seam between map authoring (the editor's `buildMapExport`, which// bakes the live stores into this shape) and map rendering (the engine's// `buildScene`, plus any consuming game). It is deliberately a dependency leaf:// it imports nothing, so a separate game project can copy `src/engine/` verbatim// and depend on this contract without dragging in React, the editor stores, the// lexicon, or atproto.//// A `MapExport` is self-contained: the offset-grid corner-Wang resolution and// the source tilesets are baked away into ONE per-map sprite sheet (the atlas)// plus a flat list of blits, object defs, and region nodes — all in `cell` px// (world scale), so a renderer can draw it with a single uniform zoom and no// auto-tiling logic of its own. See `lib/mapExport.ts` for how it is produced.// ---------------------------------------------------------------------------
export const MAP_EXPORT_VERSION = 1
/** A point / size pair in cell px. */export interface Vec2 { x: number y: number}
/** A rectangle in cell px, top-left origin (y-down). x/y may be negative. */export interface Rect { x: number y: number w: number h: number}
/** A custom, game-defined name→value property. Lives on every node kind and is * the escape hatch that keeps the exported format game-agnostic. */export interface Property { key: string value: string}
/** Which world axis depth runs along (larger coordinate = nearer the viewer). * `y` is top-down (default); `x` sorts left-to-right. */export type DepthAxis = 'y' | 'x'
/** A canvas node's depth band (the whole canvas sorts as one unit). * • `dynamic` — joins the depth pool; interleaves with objects along the axis. * • `ground` — pinned behind all dynamic content. * • `sky` — pinned in front of all dynamic content. */export type CanvasSort = 'dynamic' | 'ground' | 'sky'
// ---------------------------------------------------------------------------// Background (the map's backdrop, drawn behind every node). Passed through the// export verbatim from the authoring runtime — kind-tagged (not the wire// `$type` form). A consuming renderer may draw it; the editor Sandbox does not.// ---------------------------------------------------------------------------
/** How an image backdrop fills: `tile` repeats; the rest fit one copy. */export type BackgroundFit = 'tile' | 'cover' | 'contain' | 'stretch'
export interface ColorBackground { kind: 'color' color: string}
export interface GradientStop { color: string /** percentage along the gradient line (0 = start, 100 = end). */ position: number}
export interface GradientBackground { kind: 'gradient' /** gradient line angle in degrees: 0 = left→right, 90 = top→bottom. */ angle: number stops: GradientStop[]}
export interface ImageBackground { kind: 'image' name: string /** image bytes as a data URL. */ dataUrl: string fit: BackgroundFit}
/** A single tile of the map's tileset (by grid col/row), repeated to fill. */export interface TileBackground { kind: 'tile' col: number row: number}
export type MapBackground = | ColorBackground | GradientBackground | ImageBackground | TileBackground
// ---------------------------------------------------------------------------// The baked atlas + the flat scene.// ---------------------------------------------------------------------------
/** A region of the baked sheet, in baked-sheet px (already cell-scaled). */export interface AtlasEntry { id: number x: number y: number w: number h: number}
/** One blit: an atlas entry placed at a world-px position (size = entry size). */export interface Sprite2D { atlas: number x: number y: number}
/** An animated blit: one display tile that cycles through `frames` (atlas entry * ids) on a lockstep clock. Emitted for cells whose resolved Wang tile changes * as an animated material advances — the engine plays it as a sprite sequence * exactly like an animated object. */export interface AnimSprite2D { /** atlas entry ids, frame 0 first. */ frames: number[] x: number y: number frameDurationMs: number}
export interface CanvasExport { kind: 'canvas' id: string name: string /** the parent canvas's depth band. */ sort: CanvasSort /** for a `dynamic` band: its far-edge depth (cell px along axis). Retained for * provenance; `sortKey` is what the runtime sorts by. */ depth: number /** the node's concrete, pre-resolved sort index (cell px along the axis) — the * runtime sorts by this directly, no dynamic-depth derivation. Set for every * dynamic band (= its far-edge `depth`) and for composite members (the unit's * shared depth, plus a tiny per-member tiebreak). Absent for ground/sky, which * sort by their fixed band. */ sortKey?: number /** pre-resolved tile blits, in cell px. The engine draws these as-is. */ sprites: Sprite2D[] /** animated tile blits (cells painted with an animated material). Drawn the * same way as `sprites`, but cycling their frames. Empty for static layers. */ animSprites: AnimSprite2D[]}
/** A self-contained object definition: carries its own collision + frame art so * the engine never loads the source tileset. `source` is a back-pointer for * editing only. All geometry is in cell px, relative to the art's top-left. */export interface ObjectDefExport { id: string source: { tilesetRef: string; objectId: string } /** anchor offset from the art's top-left (cell px). */ origin: Vec2 /** art size of one frame (cell px). */ size: { w: number; h: number } /** collision boxes relative to the art's top-left (cell px). */ collision: Rect[] /** atlas entry ids, one per animation frame (frame 0 first). */ frames: number[] frameDurationMs: number /** custom game data; the animation play mode lives here under * `world.tilemap.animationMode` (absent = `'loop'`). */ properties: Property[]}
export interface ObjectExport { kind: 'object' id: string name: string defId: string /** world position of the anchor (cell px). */ pos: Vec2 /** uniform scale about the anchor (1 = unchanged). */ scale: number /** clockwise rotation about the anchor, in quarter-turns (0..3). */ rotation: number flipX: boolean flipY: boolean /** when this object belongs to a composite group, the unit's shared depth * (cell px, plus a tiny per-member tiebreak) — overrides the per-object far * edge so the whole composite sorts as one. Absent otherwise. */ sortKey?: number /** custom game data; per-instance animation state and the * `town.deception.autoGate` auto-door flag live here. */ properties: Property[]}
export interface CollisionExport { kind: 'collision' id: string name: string /** solid box in cell px. */ rect: Rect properties: Property[]}
export interface AreaExport { kind: 'area' id: string name: string /** trigger boxes in cell px (behaviour comes from `properties`). */ rects: Rect[] properties: Property[]}
export interface MarkerExport { kind: 'marker' id: string name: string /** anchored point in cell px (meaning comes from `properties`). */ x: number y: number properties: Property[]}
export type ExportNode = CanvasExport | ObjectExport | CollisionExport | AreaExport | MarkerExport
export interface MapExport { version: number /** canonical cell pixel size — the atlas bake size and the renderer's scale. */ cell: { w: number; h: number } /** source tilesets (provenance only; not needed to render). */ sources: string[] sheet: { /** PNG data URL of the baked atlas, or null when there's nothing to bake. */ dataUrl: string | null width: number height: number entries: AtlasEntry[] } objectDefs: ObjectDefExport[] depthAxis: DepthAxis /** the map's backdrop, drawn behind every node (null = none). Passed through * verbatim; a `tile` background resolves its col/row against `sources`. */ background: MapBackground | null /** painted world-px extent of all canvas sprites, or null when empty. */ bounds: Rect | null /** visible leaves only — groups are organisational and flattened out here. */ nodes: ExportNode[]}
/** Project a world point onto the scene's depth axis to get its scalar depth. * Shared so static drawables and live entities sort together (larger = nearer). */export function projectDepth(axis: DepthAxis, x: number, y: number): number { return axis === 'x' ? x : y}