import type { Graph } from '../graph/Graph'; import type { PropertyDef } from '../properties/PropertySchema'; import type { AssetType } from './AssetTypes'; import type { PropertyBag, PropertyValue, Vec2 } from './Properties'; import type { Scope } from './Scope'; import type { SceneObject } from './SceneObject'; export interface AssetBase { id: string; type: AssetType; name: string; props: PropertyBag; } export interface SpriteAsset extends AssetBase { type: 'sprite'; } export interface CameraAsset extends AssetBase { type: 'camera'; } export interface TextAsset extends AssetBase { type: 'text'; } export interface SceneAsset extends AssetBase { type: 'scene'; objects: SceneObject[]; } /** `variables` is this asset's own property schema; `props` holds their default values. */ export interface BehaviorAsset extends AssetBase { type: 'behavior'; variables: PropertyDef[]; graph: Graph; } /** * Declarations shared by the whole run: a `Variables` node naming this asset reads and writes one * value per key, however many graphs point at it. Nothing is placed, so nothing overrides it – * `props` holds the starting values, as a behavior's does for its own variables. * * A `persistent` asset keeps its values past the end of the run (#166): the next run starts from * what the last one left, not from `props`. Absent is not persistent. * * `scope` says how long one set of values lives: absent is `global`, the whole run, and `scene` * builds them again from the declarations with every scene that is loaded (#164). A `scene` asset * is never persistent – what a scene swap throws away has nothing to keep between runs. */ export interface VariablesAsset extends AssetBase { type: 'variables'; variables: PropertyDef[]; persistent?: boolean; scope?: Scope; } /** Placed as a child of the object it belongs to; its position is the offset from that body. */ export interface ColliderAsset extends AssetBase { type: 'collider'; } /** * Placed as a child of the object it moves – at most one per object. Its props say how the physics * engine treats that object: falling, resting or pushed along by the graph (see `properties/rigidbody.ts`). */ export interface RigidbodyAsset extends AssetBase { type: 'rigidbody'; } /** * `objects` is a template, not content: a placed container shows it without copying it, so an * edit here reaches every instance (see `model/instance.ts`). Positions are local to the * container's origin. */ export interface ContainerAsset extends AssetBase { type: 'container'; objects: SceneObject[]; } /** How the parts between the corners of a nine-slice frame fill the space they are given (#41). */ export type FrameFill = 'stretch' | 'repeat'; /** Distances from the frame's own edges, in image pixels; all zero means the frame is not sliced. */ export interface FrameBorders { left: number; top: number; right: number; bottom: number; } /** * `id` shares the generator with asset ids and is unique across the document (see `model/texture.ts`). * `border` and `fill` are absent on a frame nobody sliced, which is what every document written * before #41 says – read them through `frameBorders`/`frameFill`, never off the object. */ export interface TextureFrame { id: string; name: string; x: number; y: number; w: number; h: number; border?: FrameBorders; fill?: FrameFill; /** * The outlines a tile made of this frame blocks with (#141), in pixels from the frame's top-left * corner, so they move with the frame. Several, because an autotile's block holds several shapes * – its bordered tile and its notches. Absent is none; one of fewer than three corners collides as none. */ collision?: Vec2[][]; /** * Where a sprite wearing this frame stands when a layer sorts by Y (#228): whole image pixels up * from the frame's bottom edge. Absent is 0, the bottom edge itself. */ sortLine?: number; } /** * `file` names the image in `data/textures/` and is content-addressed (`{sha256}.{ext}`), so it * can be cached forever; empty means the upload failed or has not happened yet. `width`/`height` * are the size as uploaded, so frames can be sliced and validated before the image loads. */ export interface TextureAsset extends AssetBase { type: 'texture'; file: string; width: number; height: number; frames: TextureFrame[]; } /** How a key blends into the next one (#101); absent is `linear`. `step` holds until the next key. */ export type KeyInterpolation = 'linear' | 'step' | 'ease-in' | 'ease-out' | 'ease-in-out'; /** * A value on a whole frame. Its interpolation rules the way to the next key. A value that cannot * blend (a texture, a switch) holds until the next key whatever it says. */ export interface AnimationKey { frame: number; value: PropertyValue; interpolation?: KeyInterpolation; } /** * One row of the timeline: a property of the root (`target` empty) or of an object inside it * (`target` = the template object's id). `keys` are sorted by frame, at most one per frame. * `relative` adds a number to the value the target had when the animation started. */ export interface AnimationTrack { id: string; target: string; property: string; relative?: boolean; keys: AnimationKey[]; } /** * A timeline over one root asset – a sprite, text, container or scene. It is played on an object, * and its rows reach that object and the objects inside it, so every instance plays on its own. * The last key decides how long it runs (see `model/animation.ts`). */ export interface AnimationAsset extends AssetBase { type: 'animation'; root: string; tracks: AnimationTrack[]; } /** * The graph holds one `State` node per state and an edge per transition; a behavior reads the * machine's current state through `State branch` and moves it through `Set state` or `State transition`. * A `global` machine has one current state for the whole run instead of one per behavior instance; * a `scene` machine shares one too, but starts over with every scene that is loaded (#164). * * `start` is the id of the state every instance begins in, empty when the machine has no start – * then it is in no state until `Set state` runs. It lives on the asset rather than on the node * because only one state may be the start, and that is structural here instead of a rule someone * has to keep. A deleted state leaves a dead id behind, which reads as no start. */ export interface StateMachineAsset extends AssetBase { type: 'statemachine'; graph: Graph; scope?: Scope; start?: string; } /** * The graph is GLSL in nodes: `Property` nodes declare the sprite rows, the `Output` node ends * the fragment (see `core/shader/compile.ts`). A sprite wears it through its `shader` property. */ export interface ShaderAsset extends AssetBase { type: 'shader'; graph: Graph; } /** * `file` names the typeface in `data/fonts/` the way a texture names its image: content-addressed * `{sha256}.{ext}`, empty when the upload failed. A text object wears it through its `font` * property; the family the browser knows it under is derived from the file name, see `model/font`. */ export interface FontAsset extends AssetBase { type: 'font'; file: string; } /** * `file` names the sound in `data/sounds/` the way a font names its typeface: content-addressed * `{sha256}.{ext}`, empty when the upload failed. Nothing wears a sound – a graph plays it with * `Play sound`, and `props` says how loud and whether it repeats (see `properties/sounds.ts`). */ export interface SoundAsset extends AssetBase { type: 'sound'; file: string; } /** * A tile a tilemap paints with: `pictures` names one block per animation step (a texture id for a * sheet that holds nothing else, a frame id for one block cut out of a sheet), and `props` say * which kind of block it is – a plain tile or one of the two autotile blocks that draw their own * edges – how fast it runs and whether it loops or bounces. What a block is cut into is * `model/tile.ts`, which quarter goes where `model/autotile.ts`; placing one is the tilemap of #128. */ export interface TileAsset extends AssetBase { type: 'tile'; pictures: string[]; } /** * One drawing layer of a tilemap: `cells` maps `"x,y"` to the id of the tile lying there, and a * layer nobody switched off is drawn. Which of that tile's quarters a cell shows is worked out * from its neighbours while drawing, never stored – see `model/tilemap.ts`. `z` is added to the `z` * of the placed tilemap (#174), as a child's is to its parent's, so every placement of the map * keeps its layers apart by the same steps; layers of equal `z` keep their list order. * `sortByY` draws the layer row by row, sorted in by Y with every object on its `z` (#228). */ export interface TilemapLayer { id: string; name: string; hidden?: boolean; z?: number; sortByY?: boolean; cells: Record; } /** * A grid painted with tiles, placed in a scene as a sprite is. How wide the grid is and how * big one cell is are properties, so the inspector edits them with undo like any other row; the * layers are intrinsic data, as a texture's frames are (#128). */ export interface TilemapAsset extends AssetBase { type: 'tilemap'; layers: TilemapLayer[]; } /** * A point of a ring. `in` and `out` are the Bezier handles, as an offset from the point itself, so * a point may be dragged without touching them; absent is a corner, and the edge to that side is * straight. They are kept apart rather than mirrored, because a point may bend differently on each * side (#129). * * `mode` says how the two handles move together when one is dragged (#195): a corner leaves the * other alone, a smooth point keeps it pointing the opposite way at its own length, a symmetric one * mirrors it exactly. Absent is a corner. It changes no pixel – only what the next drag does. */ export interface VectorPoint { x: number; y: number; in?: Vec2; out?: Vec2; mode?: VectorPointMode; } export type VectorPointMode = 'corner' | 'smooth' | 'symmetric'; /** * One outline of a shape, in canvas pixels from the canvas's top-left corner. A shape holds several * of them so it can have holes: the rings are filled even-odd, so a ring inside another cuts it out * (#129, answer 6). `closed` false is an open path – it is stroked and not filled. */ export interface VectorRing { points: VectorPoint[]; closed: boolean; } /** The outline drawn along a ring; `width` is in canvas pixels. Absent on a shape is no outline. */ export interface VectorStroke { color: number; width: number; /** A gradient along the outline (#186); absent is the solid `color`. */ paint?: VectorGradient; } /** One colour of a gradient; `offset` runs from 0 at `from` to 1 at `to`. */ export interface GradientStop { offset: number; color: number; alpha: number; } /** * A gradient in canvas pixels (#186). Linear runs from `from` to `to`; radial has its centre on * `from` and reaches its last colour at the distance of `to`. */ export interface VectorGradient { type: 'linear' | 'radial'; from: Vec2; to: Vec2; stops: GradientStop[]; } /** * A picture as fill, repeated across the shape (#186). `texture` names a texture or one of its * frames, as a sprite's Texture row does; one tile spans the rectangle from `from` to `to`. */ export interface VectorTextureFill { type: 'texture'; from: Vec2; to: Vec2; texture: string; } export type VectorPaint = VectorGradient | VectorTextureFill; /** * One filled figure. `fill` is a solid colour and `opacity` scales it; a `paint` (#186) fills with a * gradient or a picture instead, and `fill` stays what the shape goes back to. The list order is the * drawing order: the last shape of the asset lies on top, as the last layer of a tilemap does. */ export interface VectorShape { id: string; name: string; rings: VectorRing[]; fill: number; opacity: number; paint?: VectorPaint; stroke?: VectorStroke; } /** * A picture drawn from curves instead of uploaded (#129). It is rasterised onto a canvas whose size * is a property, and handed to the picture path through the generated-picture key of #185 – so a * sprite, a tile, a tilemap and a particle source wear it exactly as they wear a texture, and the * shapes never leave this asset. The shapes are intrinsic data, as a texture's frames are. */ export interface VectorArtAsset extends AssetBase { type: 'vectorart'; shapes: VectorShape[]; } /** * One drawing layer of pixel art (#130). The list order is the drawing order, the last layer on top, * as with vector shapes. `opacity` runs from 0 to 1; absent `hidden` is shown. */ export interface PixelLayer { id: string; name: string; hidden?: boolean; opacity: number; } /** * One picture of the sequence. `cels` names the pixels of each layer on this frame: a file in * `data/pixels/` (see `model/pixelCel.ts`), content-addressed like a texture. A layer missing here, or * named with `''`, is empty on this frame. The id shares the generator with asset ids, so a sprite's * Texture row can name a frame the way it names the frame of a sliced texture. */ export interface PixelFrame { id: string; cels: Record; } /** * A picture painted pixel by pixel (#130). Every pixel is an index into the palette the `palette` row * names, 0 being transparent. The canvas size is intrinsic here, not a property as with vector art: * resizing rewrites every cel, so it is a command with an anchor rather than a free row. All frames * are drawn side by side into one generated picture (#185), and a frame reads as a cut of it. */ export interface PixelArtAsset extends AssetBase { type: 'pixelart'; width: number; height: number; layers: PixelLayer[]; frames: PixelFrame[]; } /** * The colours pixel art is painted with (#130). Index 0 is transparent and not stored, so `colors[0]` * is index 1. Several pixel art assets may share one palette; changing a colour here recolours all of them. */ export interface PaletteAsset extends AssetBase { type: 'palette'; colors: number[]; } /** * The graph is the motion of one particle: every output of its `Output` node is a function of the * particle's age and its own random number (see `core/particles/compile.ts`). A placed object is * the source that emits them; how many and for how long are its properties. */ export interface ParticlesAsset extends AssetBase { type: 'particles'; graph: Graph; } export type Asset = | SpriteAsset | CameraAsset | TextAsset | SceneAsset | BehaviorAsset | VariablesAsset | ColliderAsset | RigidbodyAsset | ContainerAsset | TextureAsset | VectorArtAsset | PixelArtAsset | PaletteAsset | AnimationAsset | TileAsset | TilemapAsset | StateMachineAsset | ShaderAsset | FontAsset | SoundAsset | ParticlesAsset; /** The assets edited as a node graph – what the graph canvas and the graph commands work on. */ export type GraphAsset = BehaviorAsset | StateMachineAsset | ShaderAsset | ParticlesAsset; const GRAPH_TYPES: ReadonlySet = new Set(['behavior', 'statemachine', 'shader', 'particles']); export function isGraphAsset(asset: Asset): asset is GraphAsset { return GRAPH_TYPES.has(asset.type); } export function isSceneAsset(asset: Asset): asset is SceneAsset { return asset.type === 'scene'; }