Parity
Last comprehensive audit against main: 2026-09-27. Targeted status and
queue reconciliation: 2026-10-27. Verify individual rows when changing them.
What a game engine is expected to do, what Sindri actually does, and the
distance between them. This file replaces function-matrix.md and
feature-integration-matrix.md, which are deleted: both graded Sindri against
Sindri, so a feature nobody had thought of had no row and could not show as
missing.
That failure was not hypothetical. The integration matrix recorded 2D physics as
Ready — masked runtime, body kinds, sensors, validation, velocity, impulse,
all true — while a character could not be given a capsule with a circle at each
side, because a collider was singular from the scene file down to
BodyRecord2d. "Ready" meant complete against what we built, not sufficient
for a game. A table with an outside edge would have carried a Compound
colliders row reading ❌ from the first day it existed.
So this table is written from the outside in. The baseline column is what a person arriving from Unity or Godot expects to find. Rows exist for things we have never built and have no plan to build, because a gap with no row is a gap nobody schedules.
How to read it
Four surface columns, then the outward judgement:
| Column | Question |
|---|---|
| Engine | Does the runtime do it? |
| Editor | Can somebody author it without editing JSON by hand? |
| Decay | Can a script reach it? |
| Proof | Does a project in this repository use it, and which one? |
Surface legend: ✅ works and is exercised · 🟡 a useful slice, with the gap named · ❌ absent · — genuinely not applicable to that surface.
Parity legend:
- Ahead — we do this better than the baseline, on purpose. Protect it.
- Par — a competent equivalent exists.
- Behind — the same idea, less of it.
- Absent — the baseline has it and we have nothing.
- Won't — a deliberate decision not to have it. See the anti-goals.
A ✅ still means what it always meant: implemented and exercised, not represented by a type, a schema, or an editor control.
What counts as proof follows the three kinds of project in AGENTS.md.
For a gameplay capability, exercised means a game uses it: Causeway, Orbital
Last Stand, or a genre showcase (games/<genre> — the platformer and Scorchball
today). A feature example (examples/<feature>) or a lab such as Voxel Lab
shows a capability on its own; that is a partial proof (🟡) for a rendering or
tooling row, where isolation is the honest test, and not a proof for a gameplay
one. Either way the gap cell names the project, so removing a project shows
which rows it was holding up. Gather's removal is why this rule exists: it left
eight rows citing a game that no longer existed.
The layout still lags the three kinds. examples/ holds camera, cube and
triangle, three feature demos — voxel-lab, weave-poc and shapes-lab —
live under games/, and most of the feature examples AGENTS.md names (audio,
Weave, Decay, tilemaps, isometric, voxels) do not exist as examples yet. Moving
and filling them is follow-up work, not a change to any cell here.
Ranking is by "does this stop somebody shipping a game", not by feature count. Unity has thousands of features and most of them do not matter. The ranked queue at the end of this file is the actionable output; the tables are the evidence behind it.
Where Sindri is already ahead
Written down first, and deliberately, because parity work is the most likely way to lose these. Every row below is somewhere copying the baseline would be a regression.
| We do | They do | Why ours is better |
|---|---|---|
Components self-describe through ComponentSchemaRegistry; templates are checked against serde at startup |
Unity's inspector is hand-written per component; drift is found by a user | A template that has drifted from its struct is a startup error, not a row quietly missing from a panel a release later |
| Capability rule: docs and a real game use, in the same change | Features ship, then rot unproven | capabilities.md describes what a game actually did, not what an API allows |
| Scenes are readable single files; no sidecars, no GUIDs | .meta files, GUID churn, unmergeable scenes |
Two people can edit a scene and resolve it in a normal diff |
| Determinism by default: a written-out PCG stream, replayable from a seed on every host, no platform entropy | UnityEngine.Random is not contractually stable across versions or platforms |
A seed reproduces a run, which is what a roguelike and a bug report both need |
| Decay and Weave designed alongside the engine | C# bolted to a C++ core; UI Toolkit retrofitted after two prior UI systems | One scripting surface and one presentation model, both generated from the same registry |
| Weave: responsive stylesheets for game UI, composed and hot-reloaded | No native equivalent; USS is the nearest and arrived late | Game UI that adapts to a phone and a desktop from one authored source |
| Isometric and grid gameplay in the engine: footprints, occupancy, walls, A* over whole footprints | Plugins (A* Pathfinding Project, tile extensions) | The thing most 2D games need is not a third-party dependency |
| Validation refuses at the boundary and names what failed, including the index of the offending collider piece | Silent clamping, silent nulls | A bad value is a named error, not a mystery at frame 4000 |
| Fixed-step loop with input edges consumed exactly once | Update/FixedUpdate confusion, input polled in the wrong phase | Input cannot be missed or double-counted |
What we will not copy
The advantage of a foundation this new is that its mistakes are still avoidable. These are anti-goals; a PR that drifts toward one should be stopped by this list rather than discovered a year later.
- No sidecar metadata files or asset GUIDs. Identity comes from the path inside the project. Renames are the price; unmergeable scenes are not.
- No serialization by field-name reflection. Renaming a Rust field must not silently drop authored data. Schema versions and explicit decoding instead.
- No second render pipeline. Unity has three that split the ecosystem in half. One path, extended.
- No second input system. The action layer already in
sindri-platformbecomes the input system; it does not grow up beside the old one. - No prefab variant hierarchy. Prefabs plus overrides through the reference. Nesting a prefab inside another is not a variant and is supported; a prefab that is "another prefab, but different" is not. If variants ever seem necessary, prove it with a game first.
- No assembly-definition graph. Crate boundaries in
dependency-policy.mdalready do this job. - No inspector that needs a plugin to be usable. Odin exists because Unity's is not sufficient. Ours has to be sufficient — see the ranked queue.
- No editor-only code path. What the editor plays is what the build runs.
- No silent defaults for things the engine cannot invent. A component that names a font, a sheet, or a clip has no blank; the registry already distinguishes a field template from a fresh default, and that distinction stays.
Stranded capability
A class the old matrices could not express: built, working, and unreachable. Neither of these appeared in either matrix, in any column, because nobody had written the row.
| Capability | State | Why it is stranded |
|---|---|---|
| Scroll input | Source::ScrollX/ScrollY are bound and parsed; the wheel now scrolls sindri.ui.scroll regions |
Still not a Decay reading of its own, so a game cannot zoom or cycle with the wheel. |
Stranded capability is the cheapest work in this file: the engine cost is already paid and only the reach is missing. It is also the most invisible, which is why it earns a section rather than a footnote.
Scenes, entities, and prefabs
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Entities and hierarchy | ✅ | ✅ | ✅ | ✅ | Par | Undo entries for script writes |
| Child transforms follow their parent | ✅ | ✅ | ✅ | ✅ | Par | A child's transform is local to its parent for sprites, meshes, tiles, voxels, cameras, lights, physics, effects and the editor's handles, not only shapes. Scripts read world_position. Scorchball's markers, signposts and ball ring ride their parents. Low Tide's crew walks a tilemap deck that drives and turns under them, in the deck's own coordinates, and re-parents between the deck and the world at its ramp. Scale composes per axis, so a rotated parent scaled unevenly does not shear its children |
| Tags and queries | ✅ | ✅ | ✅ | ✅ | Par | Query by more than one tag |
| Deterministic gameplay nearest / radius queries | ✅ | — | ✅ | ✅ | Par | Authored-tag queries use composed world positions; radius results are nearest-first with world-order ties. Orbital player and Arc retain their gameplay filters. Spatial Query Lab exposes ordered results, tag/active controls and parent transforms on Pages. Cone/box queries and indexing remain |
| Prefabs | ✅ | ✅ | ✅ | ✅ | Par | A scene places a prefab as an instance: the reference plus its overrides, each a JSON merge patch keyed by the prefab's own entity IDs, worked out on save as the difference from the prefab rather than tracked. An edit to the prefab reaches every instance, at once in an open editor as one undo step that keeps overrides and handles. The editor places an instance on a grid cell or in the middle of the Scene view, makes a prefab from a subtree, opens a prefab to edit it, and shows each instance's overrides with revert, apply and unpack; script Prefab fields have a picker. The platformer's ten coins are instances of one prefab. An instance can also do without its prefab's entities, patch a list by index, and keep its inner entities' editor state; a prefab is dragged from the browser into the view; Apply and Make prefab are undone with the scene, files included; and a scene whose prefab is missing opens with placeholders that save back unchanged. The editor's panels have no automated coverage; the operations under them do |
| Nested prefabs | ✅ | ✅ | ✅ | ✅ | Par | A prefab places instances of others; a scene reaches inside one by path (loot/sparkle), a script's spawn makes the whole nest, and a loop is refused naming it. Apply writes an override into the outer prefab rather than changing the inner one. No variants, deliberately — see the anti-goals |
| Reusable data profiles | ✅ | ✅ | ✅ | ✅ | Ahead | Unity has no native equivalent; ScriptableObject is close but needs code per asset. Optional schemas when a second catalog proves the shape |
| Transform | ✅ | ✅ | 🟡 | ✅ | Par | No structured vector or rotation value in Decay |
| Game progress saves (numbers, flags and text) | ✅ | ✅ | ✅ | ✅ | Par | Shared versioned store, typed fallbacks, Unicode text calls and atomic host writes. Orbital persists pilot names, ship unlocks, settings, commendations and six sortie records; editor Play keeps an in-memory save. No running-world snapshot operation |
| Scene save / load | ✅ | ✅ | — | ✅ | Par | Readable single file, which is Ahead; see the advantages table |
| Multiple scenes / additive loading | ✅ | 🟡 | ✅ | ✅ | Behind | World::add_scene loads a scene beside the ones a world already holds; LoadedScenes keeps which one is played and switches between them, leaving everything a scene holds as the player left it. Scene.go/Scene.restart/Scene.current let a script ask (go finds a scene as it was left, which makes a pause screen a scene of its own; restart throws the scene away first, which is how a level starts over), [project] scenes declares them and the exporter walks each one for its own assets. Scene.go now switches scenes in the browser as well as natively, so a title screen reaches its game on both targets. The editor's Scenes panel shows the project's scenes as a board — a card each, with the last frame drawn of it, arrows for each literal Scene.go or Scene.restart door, and a warning on a door to a scene the project does not carry — and adds, removes, orders and nominates them. A world holding several scenes does not round-trip through to_scene |
| Scene streaming / Addressables | ❌ | ❌ | ❌ | ❌ | Absent | Terrain chunks now stream inside one scene, but scene/asset streaming remains absent |
| Undo / redo | ✅ | ✅ | — | — | Par | Command-backed; script writes are outside it |
Assets and the import pipeline
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Asset IDs, manifests, async load | ✅ | ✅ | 🟡 | ✅ | Par | No general typed asset value in Decay |
| Project browser: import, rename, copy, delete, preview | — | ✅ | — | ✅ | Par | — |
| Content-hashed static export | ✅ | ❌ | — | ✅ | Par | No editor export workflow |
| Hot reload | ✅ | 🟡 | 🟡 | ✅ | Ahead | Native reload works; Unity's domain reload is a byword for slow |
| Per-asset import settings | ❌ | ❌ | — | — | Absent | No filter mode, mip, wrap, or compression choice. Every texture is imported one way |
| Texture compression | ❌ | ❌ | — | — | Absent | Ships raw. Fine at current scale, a problem for a real download |
| Build-time atlas packing | ❌ | 🟡 | — | — | Behind | The slicer authors sheets by hand; nothing packs loose sprites automatically |
referenced_audio gather |
❌ | — | — | — | Behind | Hosts cannot infer every clip a scene needs, unlike referenced_textures |
Voxel worlds and terrain
The ordered work required to close these gaps is tracked in
voxel-system-completion-checklist.md.
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Engine-owned voxel coordinates and 16³ sections | 🟡 | 🟡 | 🟡 | 🟡 | Behind | sindri-voxel owns signed world/section/local coordinates, palette-backed sections, air/material IDs, revisions, and deterministic random-access generation. sindri.voxel_world makes the engine world visible in authored scenes and Voxel Lab now opens that path in the editor. On a grid of boxes it is the grid's ground: scripts read, build and remove its blocks through Grid.block/set_block/tagged, placement stands things on it, Grid.step_toward paths across it and the pointer aims at its blocks. Causeway is played on it. |
| Bounded voxel residency | 🟡 | 🟡 | ❌ | 🟡 | Behind | Engine world tracks entering/staying/leaving 3D sections, keeps render and simulation radii distinct, preserves sparse edits across unload/reload, and exposes deterministic deduplicated generation/mesh work queues. The scene component authors a bounded 3D window and Voxel Lab exercises it; follow_camera keeps the window under the world camera. Work is drained synchronously. |
| Voxel dirty-region tracking | 🟡 | ❌ | 🟡 | ❌ | Behind | A voxel edit dirties its section plus a neighbouring section when the edit touches that boundary, and queues resident affected sections with new monotonic mesh revisions. A scene's edits are the component's edits list, so a script's Grid.set_block remeshes only the sections it touches. Causeway's planks are such edits. |
| Neighbour-aware block meshing | 🟡 | 🟡 | ❌ | 🟡 | Behind | Engine block mesher samples through VoxelSource across section boundaries, applies game-provided material/occlusion policy, and emits indexed section-local geometry split into opaque, cutout, and transparent passes. The scene/editor bridge renders the opaque path with atlas-neutral per-face materials in Voxel Lab; cutout/transparent rendering and Causeway migration remain. |
| Persistent voxel compiled/GPU mesh cache | 🟡 | 🟡 | ❌ | 🟡 | Behind | SectionMeshCache<T> keys compiled representations by section and block/smooth/hybrid/custom profile, retains old geometry during replacement, rejects stale results, and can release all profiles named by a leaving residency delta. sindri-render caches textured GPU buffers by opaque identity and revision, while the scene bridge maps semantic voxel faces to stable texture batches, releases departed sections, and conservatively culls transformed section bounds before draw submission. Voxel Lab exercises this through both the editor scene and browser instrumentation; non-opaque pipelines remain. |
| Voxel collision | ✅ | ✅ | ✅ | 🟡 | Behind | sindri.physics3d.voxel_collider turns an authored voxel world into static 3D collision: block sets say what collides (collides, default true) and its exact bounds, residency follows dynamic bodies plus a margin, and edits/generator/block-set changes re-revision only the affected sections. ScenePhysics3d validates the voxel and body batches together before either commits. No game uses it since Causeway became a tactics game on model islands: a fixture over Voxel Lab gives the world collision and drops a block that must come to rest on it with a downward ray agreeing, natively (a_block_lands_on_voxel_lab.rs) and in an exported Chromium run. The native review edits the component and block-set Collides, and lands bodies in Play with Stop/replay. Behind: kinematic bodies do not drive residency, sensors are discrete and per-block coefficients are not authorable |
| Voxel world authoring | 🟡 | 🟡 | ❌ | 🟡 | Behind | Voxel Lab can inspect the engine-owned world and exercises engine picking. The inspector keeps the material list valid -- new materials get unused IDs, a material the generator uses cannot be removed, and generator layers choose from defined materials -- and an invalid definition keeps the last valid terrain drawing instead of freezing the view. But a project author cannot yet use the Scene view as a complete terrain tool. Camera-driven residency, material/block palette painting, place/remove strokes, selection/fill, slice/cutaway views, undoable world edits, and saved edit deltas remain. This is editing a world, not creating a reusable voxel object. |
| Reusable voxel asset authoring | ❌ | ❌ | ❌ | ❌ | Absent | No project asset format or dedicated editor exists for bounded reusable voxel models such as trees, rocks, buildings, furniture, machines, or item-like props. The intended editor is a small orbitable voxel workspace with block/material palette, place/remove/paint tools, undo/redo, preview, save/load, and a placement path into scenes/worlds. These assets must be ordinary project assets rather than game-specific Rust data. |
| Voxel persistence and digging | ❌ | ❌ | ❌ | ❌ | Absent | Sparse overrides now survive engine residency changes in memory, and the engine's natural terrain generates a real vertical volume with caves, tunnels and overhangs, but there is no save format, Causeway migration, or persisted tunnel proof. |
| Voxel world viewed as a map | ✅ | 🟡 | ✅ | ✅ | Ahead | "view": "map" draws a voxel world flat from straight above: each column the top face of its highest supporting block, lit by height (slope against a north-western sun, paler high ground, water darker with depth), with only what the camera sees worked out, sixteen columns square at a time. The same world, blocks, edits and VoxelGround as the blocks view, so one generator serves a 3D game and a top-down one. Grid.surface and Grid.height read a column's top from a script. Low Tide separates permanent liquid columns from the reversible flood overlay for swimming and marked shallow dives; these are Decay gameplay rules. Its Basin is a generated drained sea floor drawn this way, and its crawler and crew read the ground through those calls. Gathering removes biome resource blocks with existing Grid.set_block edits; the six-resource inventory, carrying and cargo weight are Decay rules, exercised with keys and phone touch. The inspector offers view as a choice, but the editor's Scene view has not been looked at with a map world. It is lit by 2D lights, and normal_maps gives its squares relief by texture. Biome edges are hard column squares: no transitions or autotiling across a map's blocks. No decoration layer: a flower or tuft on top of a block is not drawn, and animated or glowing faces draw as their first frame |
| Reversible voxel map flooding | ✅ | 🟡 | ✅ | ✅ | Behind | Low Tide found and exercises a continuous map_flood overlay: Grid.set_flood and Grid.flooded share the drawing threshold without changing terrain, edits or cached columns. Decay drives rising water, cargo loss, waiting and a nonlethal escape; headless keyboard and phone-touch tests exercise recovery after the ebb. Builtin overland blocks retain their original terrain. Map face/cache tests cover strict height boundaries. Editor Scene view is unverified; no dedicated authoring tool, 3D flood mesh or fluid simulation. |
| Smooth / hybrid voxel meshing | 🟡 | ❌ | ❌ | ❌ | Behind | Causeway once overlaid one smoothed chunk on its terrain as a prototype; that went with its own terrain code when it moved onto the voxel world. A seam-safe density mesher and Block/Smooth/Hybrid engine profiles remain unbuilt. |
2D rendering
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Sprites, sheets, UVs, layers, blending | ✅ | ✅ | 🟡 | ✅ | Par | Decay has sprite asset and visibility paths only |
| Advanced sprite colour transform | ✅ | ✅ | ✅ | ✅ | Par | Per-channel multiply and offset beyond tint, as sample * tint * multiply + offset, collapsed behind an advanced section with a reset. A transform that is not finite is refused at extraction; one merely outside zero to one is left to clip. Orbital's mine blast opens white-hot through its colour offset, which a tint cannot do, and sheds the flash within its first frames (games/orbital-baked/tests/the_blast_flashes.rs) |
| Sheet-declared ground anchor | ✅ | ✅ | — | ✅ | Ahead | Says where a sprite meets the ground, so sorting is authored rather than guessed |
| Procedural shapes | ✅ | 🟡 | ✅ | ✅ | Ahead | Instanced with sprites; Unity needs a plugin or a mesh. No point-handle authoring |
| Tilemaps (ortho + iso, overhang) | ✅ | ✅ | ✅ | ✅ | Par | Grid.tile/set_tile read and write cells; Grid.columns/rows give the size. The platformer's level is a painted tilemap and is the proof. Low Tide's scripts read its deck for walls and its Basin for soft sand, and write typed resource bundles into and out of its starter's four-slot hold with Grid.set_tile; its workbench spends physical cargo on up to three stern extensions (sixteen slots), a bunk and an engine upgrade, preserving the deck origin and crew while driving. Keyboard and phone-touch runtime tests exercise construction and refusal; this is fixed-blueprint Decay gameplay using existing engine APIs. Low Tide's high-tide dive also reads an authored side-view hull for swimming collision and returns recovered cargo to the original deck; keys and phone touch play the trip, air loss and ebb recovery. Its site history is session-only, and the authored interior is not a voxel cross-section |
| 2D depth and grid placement | ✅ | 🟡 | ❌ | ✅ | Ahead | sindri.grid.placement names a cell and the runtime derives the transform from it — plane position, the height of the ground in that column, and the Z that orders it. Depth is a consequence of position rather than an authored number, so a placed sprite carries no render layer and no script computes one. Causeway places its entities this way. depth_step on the grid turns projected depth into Z; zero keeps a scene ordering exactly as it did. A moving entity omits the cell and takes depth alone. A placement names its grid with the same stable-ID type an occupant uses and carries the cells it covers, and every one of them has to hold it up: standing over a hole is an error naming that cell rather than a prop resting at height zero. Flat ground no longer draws over what stands on it: a placed entity sorts one cell forward, past ground no higher than its feet, because no face of such a cell — top or wall — can cover it, while a block raised a step ahead keeps its own depth and still covers it. Terrain ordering is untouched by that rule. Placing a prefab by clicking a cell in the editor, and one depth per sprite ordering a tall sprite against a tall block, both remain; docs/2d-depth-and-placement.md carries the contract |
| Ordering a walker against several occluders at once | ❌ | ❌ | ❌ | ❌ | Behind | One depth per entity answers for one relationship at a time. Standing in the corner between open ground a step ahead and a raised block a step ahead, the ground wants the walker sorted forward and the wall wants it left alone; the wall wins, and the ground seam returns for that one case. The same single point is why a multi-cell wall or house sorts from one anchor rather than from the cells it occupies. Sort footprints — ordering derived per overlapping pair from occupied cells rather than from one point — are the way out and are not built; docs/2d-depth-and-placement.md records why no single depth can do it. It was measured rather than estimated: sweep_occlusion stood on all 387 standable columns of Gather's island and reached the corner in 22 of them. That test left with Gather, so no project holds the number as a budget any more. The editor draws the report on the grid under Build → Ordering, filling the ground stood on and outlining what covers it. The sweep needs a volume to stand on, so a scene whose props are sprites on no grid gets an empty report rather than a guarantee |
| Stackable tile volumes | ✅ | 🟡 | 🟡 | ❌ | Behind | Sparse XYZ storage, tile sets, face culling/rendering, undoable block painting, and one engine-owned 16×16 runtime chunk coordinate/store work. No game uses a tile volume any more: Causeway moved its ground onto the voxel world, which reads the same tile sets. Eviction, persisted edits, asynchronous budgets, physics collision, asymmetric climb/drop, second walkable levels, and slice views remain |
| Autotiling / rule tiles | ✅ | 🟡 | ✅ | ✅ | Behind | A sheet declares terrains: 16 edge or 47 blob sprites by neighbor pattern, or 16 corners sprites by the terrain at a tile's four corners (a Wang or Tiled corner set, or a generated two-terrain tileset). The tilemap brush and Grid.set_terrain paint an edge or blob terrain and redraw the neighbors, erasing included; past the map edge counts as the same terrain. Grid.set_terrain_corner sets or clears one corner of a corners terrain and rewrites the up to four tiles that meet there. The platformer's level is painted with its ground terrain; Lamplighter's ground is a corners terrain painted from Decay (games/lamplighter/assets/scripts/ground.decay), and the editor brush leaves a corners terrain out of its palette. No Tiled edge or mixed sets beyond edges and blob, no random variants or probabilities, no custom rules, and no Tiled map import. Contract: docs/terrains.md |
| Tilemap collision | ✅ | 🟡 | — | ✅ | Par | sindri.physics2d.tilemap_collider makes painted tiles solid, merged into as few boxes as cover them so nothing catches on tile seams, with passable sprites for decoration. The Scene view outlines the generated boxes. Passable sprites are typed as names rather than picked from the palette. The platformer's level is solid through it |
| 2D lights and shadows | ✅ | 🟡 | ✅ | ✅ | Behind | sindri.environment.lighting_2d sets an ambient color and sindri.light2d point lights add theirs to world sprites, tilemaps, flecks and shapes; a sprite opts out with unlit. Sprites and tilemaps name a normal map and a light a height, so a low light rakes across the art. sindri.light2d.occluder boxes, polygons and tilemap solids cast CPU-built polar shadows. Optional five-point area-light sampling produces approximate soft shadows, used by Lamplighter. Decay drives color, intensity, radius, falloff and height. The platformer's cave proves it; Low Tide lights a whole voxel map with a sun that keeps time with its tide, normal-mapped terrain and crawler, and headlights whose shadows wrecks block. At most 16 lights a frame, no spot or cone lights, no light cookies or blend modes beyond add, and polygon points do not drag in the Scene view. Contract: docs/2d-lights.md |
| Custom shaders / materials | 🟡 | ✅ | ✅ | 🟡 | Behind | A sprite names a project .wgsl file defining fn sprite_shader and carries four per-instance shader_params; the engine wraps it in the sprite pipeline, so sprites sharing a shader still share a draw. Hosts check it with naga on bind, the editor hot-reloads it, export refuses one that does not compile, and a failure names the file and line and falls back to the built-in look. The inspector picks the file and edits the parameters; Decay writes this.sprite.shader_params. examples/sprite-shaders is the feature-example proof. Sprites only, one texture, no material asset or shader graph; docs/sprite-shaders.md is the contract |
| 3D directional + ambient lighting | 🟡 | 🟡 | 🟡 | 🟡 | Behind | sindri.environment authors ambient fill and one bounded shadow map, and a sindri.light entity is the directional sun, aimed by rotation and drawn in the Scene view, shared by textured world and voxel geometry in editor and browser Voxel Lab. Lighting Lab exercises typed sun color/intensity/enabled and live rotation; Lighting Lab also exercises typed ambient intensity/color and background changes; Decay also controls authored fog and post effects in the shared player, with Causeway proof. Bounded point lighting has its own partial row; material texture bindings, cascaded shadow quality and shipped-game proof remain. |
| Voxel ambient occlusion / contact depth | 🟡 | 🟡 | ❌ | 🟡 | Behind | Engine block meshing samples neighbouring cells around exposed corners, flips face diagonals to preserve the AO gradient, and carries the result into the shared textured renderer. sindri.environment authors the effect strength and Voxel Lab exercises it in editor and browser rendering. This is deterministic mesh-time voxel AO, not general SSAO for arbitrary geometry. |
| Bloom | ✅ | ✅ | ✅ | ✅ | Par | sindri.environment authors bloom and the shared renderer applies it in editor viewports and browser Voxel Lab. Causeway ships it on its lanterns and beacon, and this.environment.bloom lets a script ease its threshold and strength from one island's mood to the next. |
| Post-processing stack | 🟡 | 🟡 | ✅ | ✅ | Behind | sindri.environment authors exposure, tone mapping, contrast, saturation, bloom, and vignette through one ordered world path in editor and browser Voxel Lab, with overlay/UI rendered crisply afterward. Decay controls the authored stack and Causeway uses it in the shipped host; LUT grading and advanced cinematic effects remain absent. |
| Nine-slice sprites | ✅ | ✅ | ✅ | ✅ | Par | A sheet records a border per sprite in pixels; sindri.sprite and sindri.ui.image draw sliced as nine quads in the image's own space, with a border_scale, squeezing borders that would cross. A sliced UI bar keeps its caps as it fills. The sheet slicer drags the border's guides. The platformer's HUD frame and signboard share one sliced panel. No tiled (rather than stretched) middle. Contract: docs/nine-slice.md |
| Sprite masking / stencil | ✅ | ✅ | ✅ | ✅ | Behind | sindri.sprite_mask places a picture whose alpha is the shape; a sprite with mask: inside or outside and a mask_name draws only within or beyond it. The mask is batch state read in the fragment stage, so masked sprites still batch and no stencil is needed. examples/sprite-masks reveals a meadow and a gem through a wandering keyhole. World sprites only, one mask per sprite, no unions. Contract: docs/sprite-masks.md |
| Sorting and draw order | ✅ | ✅ | 🟡 | ✅ | Par | Layers plus the ground anchor |
| Camera follow / confine / shake | ✅ | 🟡 | 🟡 | ✅ | Behind | sindri.camera.behavior provides stable-target follow with offset/dead zone/smoothing/max speed, XY confinement, and deterministic trauma shake, and every gameplay host advances it after scripts. Optional viewport-aware bounds keep an unrotated 2D orthographic view inside world edges; Lamplighter uses them for portrait and landscape. Optional portrait look-ahead follows the target’s travel direction with smooth transitions; Lamplighter uses it to show more of the path ahead. Follow targets resolve within the camera's scene or prefab. Decay exposes runtime follow/offset/dead-zone/smoothing/speed/bounds/shake controls and trauma/impact; Camera.bounds preserves the authored containment mode. Camera Lab exercises controls, and Orbital uses impact shake. Rotated or parent-transformed cameras and shake crossing bounds remain limitations; dedicated authoring UX/gizmos remain. |
| 3D orbit / third-person camera | 🟡 | 🟡 | 🟡 | 🟡 | Behind | sindri.camera.orbit and explicit Camera.orbit controls provide world-space follow, aiming, smoothing and 3D ray pull-in; Camera.perspective_fov changes FOV. Orbit Camera Lab runs to its goal through Session and Chromium. Editor creation uses a real active authored spatial target, requires a Camera, rejects invalid settings/targets before checked commands and exercises Undo/Redo and save/reopen. Native review authors orbit without a camera script, observes pull-in/recovery and reaches the walker goal. Selected Scene guides show world-space focus, requested position/yaw circle and actual pose during Play, with near-plane clipping and no picking targets. Target IDs for runtime-only spawns, camera-volume collision and Explorer proof remain open |
3D rendering
3D has a shared foundation and is used by Causeway, but broader authoring, rendering, animation, performance and game proof remain incomplete. Follow-ups are ordered in 3d-update.md.
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Cameras, depth, cube primitive, inline textured surface mesh | 🟡 | 🟡 | ❌ | 🟡 | Behind | The cube primitive is a unit cube, the size of a default Collider 3D box. examples/cube draws an authored inline mesh; inline surfaces retain u32 geometry across frames and share shadow/color buffers; authored mesh assets remain absent |
| Retained inline mesh geometry | 🟡 | 🟡 | — | 🟡 | Behind | Immutable u32 resources, geometry-only invalidation, per-renderer GPU caches and old-frame lifetime; scene history/lifecycle and GPU shading/upload regressions, 3D Benchmark and Lighting Lab feature proof. Identical entities are not deduplicated; instancing and game adoption remain open. Contract |
| glTF / model import | 🟡 | ❌ | ❌ | ✅ | Behind | Static self-contained GLB CPU decoding and a separate reusable GPU model path retain hierarchy, normals/UVs, both index widths, materials and textures. Decoder/resource/GPU readback tests cover diagnostics, transforms and reuse. Scene references, facade conversion and external-project packaging are tested. The actual Low Tide cutaway GLB is visually verified in native offscreen and WebGPU project hosts. Causeway draws every island, figure and building as a generated GLB with emissive materials, lit by the sun and point lights. Static subset only; no animation/skinning or imported-model editor UX; see docs/imported-models.md. Offline isometric baking is not runtime 3D |
| Imported-model occlusion silhouettes | ✅ | ❌ | — | ✅ | Behind | Optional model RGBA draws hidden fragments against opaque terrain without self-occlusion; Causeway uses gold/rust subjects. Shared geometry and depth behavior have scene/GPU regressions. Filled silhouettes, alpha overlap and transparent occluders remain limitations; editor authoring is unexercised. See docs/model-occlusion.md. |
| Model asset field metadata and picker | ✅ | ✅ | — | 🟡 | Behind | Core model asset meaning describes render/static-collision references and is generated into the catalogue. Editor GLB-only choices and real project-backed Add Component defaults have metadata/layout/ownership regressions; independent widget and component history scopes preserve Undo/Redo while retaining drag merging. Native review exercises independent selections, Add Model, save/reopen and Play using an alternate GLB wall. Native uppercase GLB discovery preserves asset IDs and installs collision. Explorer adoption remains pending |
| Materials | 🟡 | 🟡 | ❌ | 🟡 | Behind | Numeric standard factors, reusable profiles and imported-model materials exist; shared texture bindings, unified alpha and broader game proof remain. See the detailed material rows below and the contract. |
| Shared standard material factors | 🟡 | 🟡 | ❌ | 🟡 | Behind | One numeric MaterialFactors type and GGX shader serve opt-in cubes, inline/cached geometry, voxel owners and imported primitives. Authored sindri.material has checked color/metallic/roughness/emission, per-entity overrides and mask-alpha delivery. Core GLB emission is retained/rendered with override restoration and unchanged masked shadows. GPU pixel/cache tests, scene undo/residency tests, native/browser swatches and native editor edit/Undo/Redo/save/reopen review exercise it. Legacy looks remain opt-out; material texture ownership, unified alpha, emissive maps and game adoption remain open. See docs/rendering-materials.md. |
| Shared render material profiles | 🟡 | 🟡 | — | 🟡 | Behind | Typed .profile assets share numeric factors and sidedness across cubes, inline/cached geometry, voxel owners and imported primitives, with explicit local overrides and old-frame/residency regressions. Existing async loaders, checked profile references, structured validated Save and export discovery cover the asset path. Lighting Lab carries shared/inline and shared-edit feature controls. Native/WebGPU shared and inline captures are byte-identical; one profile edit changes both cube and imported model. Native editor creation, validated Save/correction, selection/history, reload, reopen and Play are exercised. Texture bindings, unified alpha and game adoption remain open. Contract |
| Authored material sidedness | 🟡 | 🟡 | — | 🟡 | Behind | Shared inherit/single/double face visibility for cubes, inline/cached geometry, voxel owners and imported primitives; reflected back-face lighting and matching shadows have GPU regressions, while material changes preserve geometry residency. Checked editor choices and scene history use the same schema; Lighting Lab carries paired native/WebGPU controls. Game adoption, material texture bindings and unified alpha remain open. Contract |
| Authored textured geometry normals | 🟡 | ❌ | ❌ | 🟡 | Behind | Cubes have outward face normals; inline surfaces accept validated optional XYZ normals and cached render vertices carry them. Missing normals retain flat shading. GPU comparisons match imported normals under rotation/nonuniform scale/reflection, and orientation-aware color/shadow pipelines preserve reflected world faces. Scene validation/extraction/Undo/Redo and native/browser smooth/flat ellipsoid controls exercise the path. Textured vertices grow 24→36 bytes. Editor authoring review, Decay access and game adoption remain open; see docs/rendering-materials.md. |
| Lighting | 🟡 | 🟡 | 🟡 | 🟡 | Behind | One authored ambient contribution plus one directional world light shade textured 3D geometry and voxel terrain in editor viewports and Voxel Lab, with one bounded directional shadow map. The shared player and offscreen capture/benchmark now apply authored shadows; the fixed 300-prop benchmark and on/off native/browser captures exercise them. Imported models receive the shared map, with native/browser green-receiver controls and GPU resize/cache regressions; model casting now reuses cached hierarchy/geometry with material alpha/facing GPU regressions and native/browser imported-only controls. Authored fog reaches shared project hosts and is controlled by Decay; see the shipped-host environment row. Bounded point lighting now has feature controls (row below); shared material texture bindings, normal maps and cascaded shadows remain open; typed light control now has feature proof in Lighting Lab; opt-in numeric standard factors now have their own row. |
| 3D point and spot lights | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Strongest-eight active points/spots, world-unit range/falloff, feathered half-angle cones and composed parent positions/directions share Lambert/GGX across meshes, voxels and imported models. Scene/GPU regressions and native/browser controls exercise updates and cache reuse; editor creation, clipped selected range circles and spotlight cone guides are implemented. Typed Decay Color/intensity/range/cone/enabled access validates atomic writes and preserves entity activity; Lighting Lab exercises the live path through Session and native/browser controls. Feature proof for Explorer, with game adoption and local shadows still open. Fixed 512-byte cost per color draw; see 3d-lights.md. |
| Runtime ambient and background controls | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Typed entity environment access shares authored defaults and validates full candidates atomically. Lighting Lab compiles and plays through cool/warm ambient and clear-color phases, then restores its goal state. Session extraction, native/browser phase captures and editor Play exercise it. Fog/post controls and in-tree game proof remain open; Day/Night Lab now exercises a configurable project clock; see runtime-environment.md. |
| Scripted day/night cycle | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Day/Night Lab opens, compiles and plays two Decay clock configurations through sunrise/midday/sunset/night and a continuing goal. A moving imported prop carries night lights while the sun is disabled; frozen-hour native/browser shadow pairs isolate changed sun direction on imported ground. Native editor Play exercises the same project. This is feature proof; in-tree game adoption, fog/post controls and atmospheric rendering remain open. See daynight.md. |
| Authored fog/AO/post stack in shipped project hosts | ✅ | — | ✅ | ✅ | Par | SceneExtractor::world_settings gathers an environment's light, shadows, fog, occlusion strength and grade, and TexturedCubeRenderer::configure applies them, so the shared native/browser player and the offscreen project renderer light a frame as the editor does; a frame with post-processing goes through encode_lit_frame and its bloom. Causeway ships tone mapping, grade, vignette and bloom on lanterns and beacon this way. Decay reads and writes fog, the grade and bloom while a game runs through this.environment.fog, .post_process and .bloom, checked as authored values are; Causeway eases a mood per island and flashes them for lightning |
| Skeletal animation | ❌ | ❌ | ❌ | ❌ | Absent | — |
| 3D physics | ✅ | ✅ | ✅ | ✅ | Behind | PhysicsWorld3d exercises fixed steps, all body kinds, box/sphere/capsule compounds, quaternion/local offsets, compound mass, collision/sensor events, masks, controls, keyed static groups and atomic validation. ScenePhysics3d owns authored bodies, colliders and voxel colliders, validates each batch before committing it and writes back parent-space poses. Shared native/browser sessions and editor Play step it before scripts. Typed Physics3d controls, ordered spawn queues and copied collision/sensor events are exercised in bridge/session tests and Chromium. The Voxel Lab landing fixture drops a dynamic body onto a voxel world. The native review authors Rigid Body 3D/Collider 3D in the inspector and lands the body on voxels in Play, with Stop and replay. Behind because joints, CCD and one-way support remain 2D-only; 3D profiles and characters still await their game proof, and the 3D scene/controller foundation is tracked below |
Animation
Still the weakest major system relative to the baseline, but no longer the one where nothing connects. Sprite clips exist, play, and are now chosen by gameplay: a script plays, stops, restarts and times a clip, and is told when a one-shot has ended. Sequences now animate any numeric field and tweens move values from scripts; what remains absent is frame events, blending and skeletal animation.
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Sprite frame clips, timing, loop state | ✅ | ✅ | ✅ | ✅ | Par | Animation.play/stop/restart/is_finished/frame/clip/set_speed. The platformer's hero switches between idle, run, jump and fall; Scorchball's players choose a clip and its speed from how they move; Orbital's mine blast plays once and despawns itself when it ends; Low Tide's deck-child tread belts change clip and speed while driving/pivoting, hold their frame in a flood and resume after ebb |
| Clip authoring and preview | — | ✅ | — | ✅ | Par | — |
| Animation events (a frame fires a callback) | ❌ | ❌ | ❌ | ❌ | Absent | Footsteps, hit frames, spawn-on-frame all need it |
| Property animation (animate any component field) | ✅ | ✅ | ✅ | ✅ | Behind | A sequence track keys any numeric component field, by component and a path into it (sindri.sprite/tint.3), or a transform channel, edited in the Timeline; Sequence Stage's intro is authored this way. Unity's Animation window also keys colours, vectors, booleans and references as one value; a track here holds a single number, so a colour is four tracks and nothing non-numeric animates |
| Tweening / easing | ✅ | 🟡 | ✅ | ✅ | Behind | Managed number/vector/colour tweens, pause/resume/cancel/restart, typed values, progress/completion and disposal, composed with a delay, a loop count (or for ever), yoyo and Tween.after sequences. Orbital's pickup appears and then breathes with an endless yoyo; Tween Lab chains a crossing, a wait and a return. Weave shares named easing through CSS transitions. Authored choreography is a sequence edited in the Timeline (see Timeline / cutscenes); scripted tweens have no editor view, and property binding and completion callbacks remain |
| State machine / blending | ❌ | ❌ | ❌ | ❌ | Absent | Animator controller, Godot AnimationTree. Transitions are hand-written today |
| Skeletal / cutout 2D animation | ❌ | ❌ | ❌ | ❌ | Absent | Spine, Unity 2D Animation. Frame sheets only |
| Timeline / cutscenes | ✅ | ✅ | ✅ | ✅ | Behind | A sindri.sequence component holds named sequences: keyframed tracks that move a transform channel or any numeric component field on the entity, a named child, or, with a path starting /, anything in the scene, each key with a CSS easing, and cues that a script waits on with Sequence.cued and that may play a sound. The runtime advances them after scripts in the shared game session and the editor's Play. The editor's Timeline panel draws keys and cues against time, scrubs a preview into the Scene view without touching the document, and adds, keys, moves, retimes and removes tracks, keys and cues as undoable edits. Sequence Stage is the feature example, and Causeway's beacon lights by an authored swell whose cue sounds the chime, then breathes on a loop; an export carries a cue's sound without the project listing it. No curve editor, no sprite-clip, enable or event tracks beyond cues, no blending between sequences, and keys hold a single number |
Physics and collision
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Bodies, fixed-step stepping, velocity, impulse | ✅ | 🟡 | ✅ | ✅ | Par | — |
| Forces and rotation (2D) | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Runtime and typed Decay force/torque, angular velocity/impulse and world-point impulses; one-step accumulation/reset, validated body kinds and spawn requests. Platformer wind crate and input-driven launch/rotation regressions are added. Native, WASM and browser CI passed on the forces/rotation head; browser interaction and visual inspector remain in final integration |
| Solid contact snapshots (2D) | ✅ | 🟡 | ✅ | ✅ | Behind | Copied entity-based world points, support normals, normal/friction impulses and force from the last solve. Deterministic ordering, sleeping support, sensor exclusion and teleport/removal invalidation. Platformer grounds from contacts and flashes its crate on hard landings. Editor Play inspection and browser interaction remain in final integration |
| Compound colliders (several pieces, one body) | ✅ | ✅ | — | ✅ | Par | Pieces are added, removed, reordered and fully edited, a piece's shape included. The platformer's hero is one body of two pieces, a capsule and a box |
| Masks, sensors, collision events | ✅ | 🟡 | ✅ | ✅ | Par | Physics Playground checks existence before consuming 2D last-solve references in shell/pane/backstop/bumper scripts. A reset-after-contact project regression and desktop/phone browser smoke exercise cleanup; copied event references can outlive their entities |
| Named collision layers | ✅ | ✅ | ✅ | ✅ | Par | A scene's sindri.physics2d.world names its layers bit by bit; Physics.layer/Physics.mask turn names into masks and refuse a name the world does not give; the inspector shows every mask field as a menu of named layers, keeping unnamed bits. The platformer names ground, hero and pickups and probes for ground by name; Physics Playground's mask control asks for its layers by name. Named per scene rather than in project settings, and colliders still store masks |
| Per-piece validation naming the failing index | ✅ | — | — | ✅ | Ahead | Neither baseline tells you which collider was wrong |
| 2D closest-hit raycasts | ✅ | — | ✅ | ✅ | Par | Finite world-space casts return entity/point/normal/distance or null; query membership masks, sensor opt-in and entity exclusion; deterministic ties. Platformer HUD measures ground clearance; the Physics Playground's probe exposes hits, normals and masks on Pages. Query-only per-piece BVH updates on insert/remove/teleport and completed solves; filtering/ties remain unchanged |
| Overlap and shape casts (2D) | ✅ | — | ✅ | ✅ | Par | PhysicsWorld2d::overlap and shape_cast answer for circles, boxes and capsules from synchronized pieces with the raycast's mask/sensor/exclude filter; Decay has Physics.overlap_circle, overlap_box, cast_circle and cast_box. Orbital's hostile mine damages what its blast circle overlaps, by collider; Physics Playground shows a swept circle and an area. Solid contacts are exposed separately |
| 3D replaceable static geometry groups | 🟡 | ❌ | ❌ | ❌ | Behind | Keyed static collider groups share one real owner; six engine regressions exercise immediate indexed queries, preserved body/unaffected collider handles, canonical ordering, filters, atomic rejection, empty/removal/reuse and actual dynamic landing/contact followed by falling on removal. Native/WASM checks pass. The scene snapshot adapter exercises revision caches/resident lifecycle/budgets natively; authored Causeway/editor/Decay/browser group integration remains absent |
| 3D spawn-window controls | 🟡 | ❌ | 🟡 | ❌ | Behind | Standalone ordered velocity/angular-velocity/impulse requests validate kind and finite values before queuing, replay after compound mass is known and expire on removal/successful synchronization. Engine/scene tests exercise ordering, snapshots, locks, complete-batch rejection/retry and pre-solve replay. Typed Physics3d bridge tests exercise a real prefab spawn, copied last-setter/authored reads, locks, kinds and invalid authoring. Nonempty authored colliders are required; impulses affect reads only after materialization. A shared native session and exported Chromium fixture verify spawn replay and movement. Game/voxel/editor proof remains absent. Existing 2D queued controls are unchanged |
| 3D physics queries | ✅ | ✅ | ✅ | 🟡 | Behind | Rays, overlaps and fixed-orientation shape casts exercise boxes/spheres/capsules, quaternion poses, masks/sensors/whole-entity predicates, sorted unique overlaps and deterministic ties, over a per-piece BVH checked against exhaustive candidates up to 10,000 pieces. Typed Physics3d rays, overlaps and rotated box/capsule probes exercise copied snapshots, named layers, exclusion and invalid inputs in bridge tests and Chromium. Queries see resident voxel geometry with the world entity as the hit: the Voxel Lab landing fixture and editor Play both ray-cast onto the world. No shipped gameplay decision uses a 3D query yet, and queries see only voxel sections resident near dynamic bodies |
| Automatic 3D collider scaling | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Composed positive scale resizes local dimensions/offsets at the next shared physics step; unchanged frames preserve motion, geometry edits rebuild. Nonuniform boxes require orthogonal local axes; spheres/capsules require uniform scale. Shear/invalid scale/overflow reject the whole batch. Native queries, landing, parent scale, shared editor wireframes and Session wall resizing are tested; Decay uses existing transform scale. Converted Orbit Lab reaches its WebGPU goal. Orbit Lab keyboard shrink/restore verifies synchronized center/edge rays in Session and real Chromium, including reactivation. Native review exercises scale editing, Undo/Redo, save/reopen, Play queries/goal and Stop restoration. Explorer game proof remains pending |
| Extreme 3D query numerical failure reporting | ❌ | — | — | ❌ | Absent | Bounds overflow retains conservative candidates, but backend shape casts at near-maximum finite coordinates/extents can return non-finite time/witness/normal values. The engine has no explicit numerical query-failure contract yet |
| Joints and constraints | 🟡 | 🟡 | 🟡 | ✅ | Behind | Scene-authored maximum-distance constraints own separate joint entities and stable scene endpoint IDs. Edit/undo, endpoint rebuild, inactivity and removal are exercised; platformer's Decay-driven hanging lantern proves the constraint. Legacy Physics.connect_distance is preserved. Hinges add local anchors, limits and torque-capped velocity motors; typed Physics.set_hinge_motor and the platformer windmill exercise motor reversal. Sliders add aligned local axes, travel limits and force-capped drive; springs add force-based stiffness/damping. Typed controls and the platformer lantern trolley exercise reversal and retuning. Typed enabled-state controls suspend/reconnect all four constraint kinds, and distance tuning changes owned maximum lengths; platformer Decay reels/releases/reconnects its lantern. Command undo/redo and atomic rejection are covered. Typed endpoint retargeting and clearing validate both scoped handles atomically for all four kinds; platformer switches hooks with R, including while suspended. Typed joint removal releases only the authored constraint at next synchronization, preserving owner/body state and legacy connections; platformer Z cuts its cord. Typed distance creation validates scoped endpoints and a positive length before authoring on an owner with no joint; platformer C repairs the cut cord, including repeated creation/removal. Typed hinge creation validates finite local anchors under the same owner/scope contract; platformer H rebuilds its placed windmill hinge and restarts the motor independently of the spawned instance. Typed spring creation validates local anchors, positive rest length and non-negative coefficients; platformer B rebuilds its lantern spring while preserving the tuning phase and independent trolley. Typed slider creation validates local anchors, unit axes and optional travel bounds; platformer J rebuilds its bounded trolley slider while preserving its spring and motor direction. Hinge position motors add damped angle holding with torque caps; typed controls switch modes, and platformer P holds both windmills or resumes reversal while H retains the selected drive. Slider position motors add signed distance holding along local axes; typed controls switch modes, and platformer O parks/releases its trolley while J retains the selected drive and spring. Editor Save/Save As and subtree authoring now remap references through the active registry; stable IDs remain required and Play save is refused. Script-triggered world snapshots remain absent; native reference picking/clearing and missing/inactive diagnostics are exercised through real picker clicks and command undo for all four kinds; motor modes now use enum-derived inspector choices. Native scratch-scene review exercised the endpoint picker, clearing, Save and undo, both mode dropdowns, nested hinge override saves and slider mode undo/redo; native visual review now verifies all four numeric edits on Save/reopen, mode switching/rebuild/spawning in Play, refused Play Save and Stop restoring authored settings. This 2D slice is complete; 3D joints remain absent |
| Authored joint references in prefabs | 🟡 | — | 🟡 | ✅ | Behind | Runtime-only instance identity resolves local sibling paths and original top-level/nested root IDs without assigning saved scene IDs. All four joint kinds are exercised across repeated spawns, inactivity and removal; core reparenting/undo preserve scope. Platformer Decay places/removes a reusable powered windmill. Placed roots retain aliases through library-aware native/browser loading and scene namespaces; editor reload/duplication/undo and all four kinds are exercised. Platformer places the same nested assembly in its level. Opt-in registry-based save remapping is exercised across all four joint kinds and the reopened platformer spawn; flattened documents discard metadata |
| Saved spawned-prefab reference remapping | ✅ | ✅ | — | ✅ | Behind | World::to_scene_with_references remaps registered entity fields to assigned stable IDs, including dotted/list paths, without mutating the live world. Unknown payloads remain unchanged; malformed/unresolved/unstable targets fail. All four joint kinds reopen with isolated endpoints; platformer reopens a Decay-spawned nested windmill with motor reversal and a fixed axle. Editor Save/Save As and subtree authoring use the active registry automatically; all four kinds reopen with isolated endpoints and saved subtrees remain reusable. Invalid saves preserve disk/path/document. Platformer’s real Decay-spawned windmill reopens through editor Save As with reversal and a fixed axle. Stable IDs remain a prerequisite; ordinary core serializers remain verbatim and Play save is refused |
| Script-triggered world snapshot save/load | ❌ | — | ❌ | ❌ | Absent | Decay Save stores numbers, flags and text. It has no operation to persist/reopen a running world snapshot; editor authoring saves and the core scene serializer do not provide a gameplay persistence lifecycle |
| Physics materials as assets (2D) | ✅ | 🟡 | — | ✅ | Behind | Typed reusable .profile assets, shared coefficient validation and explicit entity overrides. Editor async loading/hot reload and export reference collection; shared native/browser resolver. Platformer crate and planks share wood, with a plank bounce override and rebound regression. Visual editor/browser gameplay review remains in final integration; 3D waits for its runtime |
| Physics materials as assets (3D) | 🟡 | 🟡 | — | 🟡 | Behind | Shared .profile format and explicit overrides resolve on primitives, characters, static model triangles, convex hulls and resident voxels. Coefficient edits preserve motion, support, geometry/groups and contact continuity; voxel edits consume no geometry rebuild budget. Native scene/Session/export regressions cover invalid batches, reload/removal, latest-host checkpoint restoration and inactive/prefab references. Editor Physics family/default/profile metadata use checked generic controls. Model Collision Lab floor and spawned prop share a slide profile; browser and native editor review are recorded in the physics contract. Explorer gameplay adoption remains open |
| Accelerated 2D physics queries | ✅ | — | ✅ | ✅ | Par | Query-only per-piece BVH selects rays, overlaps, shape casts and controller penetration/sweep/ground candidates at synchronized poses. Skin/contact prediction expands controller bounds; deterministic entity/piece ordering and exact geometry retain masks, sensors, exclusions, inside hits and one-way rules. Historical platform support reads only its known entity's pieces, independent of current bounds. Immediate insert/remove/teleport, solved/pending poses, overflow fallback and differential exhaustive-scan tests are exercised. At 10,000 sparse pieces sampled rays visit 28–34 nodes; sampled controller calls visit one local entity. Platformer exercises indexed clearance, steps/slopes, carry/drop-through and goal play through Decay. Dense/long queries can still visit every piece; non-static pieces need post-step refresh. Indexed 3D queries are exposed through the scene driver, without host/Decay/game proof |
| Character controller (2D) | ✅ | ✅ | ✅ | ✅ | Behind | Read-only 2D sweep/slide foundation computes collision-limited displacement with skin, bounded iterations, initial-penetration reporting, filtering and deterministic ties. Native engine tests exercise walls, corners and rotated surfaces. Ground probes now classify the nearest surface against configurable up/slope limits, including zero-travel support and steep obstructions. Composed grounded movement detects support after sliding and optionally snaps downward to walkable surfaces, suppressing snap and grounded state for upward requests. The same slope limit prevents steep contacts from generating unrequested upward movement while permitting explicit jumps and ungrounded descent; downhill snap accepts walkable support only. Opt-in steps try a clear lift, slope-limited forward sweep and walkable landing, require starting support and better progress, and retain the ordinary result when rejected. Opt-in support snapshots add synchronized translation/rotation-point carry with collision clipping before character motion; snapshots remain host-owned. One-way support-side/cone policy now applies across controller phases, with request-scoped drop-through that preserves ordinary solids and sensor filtering. Scene-owned settings and a runtime request queue now derive stationary kinematic bodies, seed support before solving, apply motion once against current solved poses and retain timed controller drop-through/cached results. Teleports, parenting and structural edits invalidate snapshots. Typed Decay queues displacement and timed drop-through, and reads copied nullable motion/support/carry snapshots with live reference filtering. Shared runtime/editor Play offer controller context; native script/shared-session regressions and a rebuilt Chromium controller fixture exercise queued motion, grounding and copied snapshots. Platformer adopts the controller and native gameplay tests reach its goal, collect coins, preserve variable jumps/braking/respawn and traverse/drop through one-way planks. Authored stone steps and a sloped boardwalk exercise accepted step lifts, walkable ascent and downhill snap through the Hero script; control tests disable steps/lower the slope limit and block on the same terrain. Chromium keyboard captures show terrain traversal. The first-gap ferry proves solved translation carry/reversal and once-only displacement, running-jump boarding, airborne carry release, landing and timed drop-through/respawn. Chromium runs the same assets from an aboard start and shows riding and a keyboard jump. Native editor review exercises every field, add/remove, undo/redo, save/reopen and Game Play running/jumping; live controls are read-only, Play Save refuses and Stop restores the document by file hash. Review. A CI Chromium keyboard run from the normal spawn reaches the flag with at least three coins and no falls; local repeated runs collect five. Read-only fixture telemetry supplies assertions without driving gameplay. Solver response and discrete sensor events see controller motion at the following solve. Rotation uses a straight chord with fixed probe orientation. Contract and remaining slices. Platformer uses controller support; coyote time, jump buffering, gravity and velocity remain Decay gameplay policy |
| Character controller (3D) | 🟡 | ✅ | 🟡 | 🟡 | Behind | Read-only Rapier movement wrapper with engine-owned options/results, current-pose BVH, XYZ wall slide, slope/step/snap controls, stationary correction, filtering and copied hits. Native engine tests cover geometry, teleports, copies/clone replay and invalid input. Backend contact/flag/iteration semantics remain explicit limits. Scene-owned stationary kinematic bodies now consume world displacement once after solving, resolve scaled probes, keep terrain resident for character travel and cache copied results. Native scene and shared Session regressions exercise lifecycle, filtering, parent writeback, terrain budgets and checkpoint replay. Read-only classified ground probes add deterministic support entities, slope checks and penetration rejection with native regressions, as a prerequisite for carry. An opt-in read-only composition verifies previous support, sweeps solved translation/rotation-point carry, and reports separate movement/carry hits and classified final support; native regressions exercise it. Scene support capture, once-only carry, dependent lifecycle invalidation, opt-out and actual solved terrain reach now have native scene/Session regression proof, including parented rotation, jumps, wall clipping and post-solve budget failure. Typed Decay requests and optional nested support/movement/carry/collision copies have native host/script and shared Session timing/replay tests, with compiler/LSP metadata. Character Lab now exercises Decay gravity/jumps, idle ferry carry/reversal and a keyboard-driven goal through native Session; exported Chromium verifies carry, jump, floor landing and goal with a rendered capture and no runtime/GPU errors. Selected 3D slope/step/snap guides reuse the orange character overlay and validated scaled probes. Native editor review verifies settings, undo/redo, save/reopen, keyboard Play goal and Stop restoration. Explorer goal proof remains pending; contract |
| Static triangle mesh colliders (3D) | 🟡 | 🟡 | ❌ | 🟡 | Behind | Assets prepare bounded GLB triangles; immutable physics meshes share acceleration structures across keyed static groups. Eight native physics regressions prove immediate queries/character movement, gaps, filtering, atomic insertion/edits, dynamic landing and replay. Static scene components and explicit Session GLB bindings now have native scale/lifecycle, character collision, replacement and replay regressions. Collision-only scene/prefab discovery and verified export are tested. The shared native/browser player now binds authored collision assets before starting gameplay, including later scenes, inactive entities and expanded prefabs/overrides. Model Collision Lab exercises imported-wall obstruction and a goal route through player Session tests and exported browser smoke. Native headless loading and editor Play now install collision before gameplay. Editor startup uses queued decoded assets, unsaved references and retained later-scene/prefab models; model reload failures keep previous collision. Model Collision Lab's editor/headless parity regression reaches its goal with matching world state and restores/replays wall obstruction. Native editor review verifies the goal, moved-wall reload, invalid-reload retention, Save refusal and Stop restoration on X11/software Vulkan. Model asset picking now has independent Undo/Redo and native save/reopen/Play proof; Explorer game adoption remains pending. Contract |
| Convex hull colliders (3D) | 🟡 | ❌ | ❌ | 🟡 | Behind | Clone-shared volumetric hull resources and mixed primitive/hull bodies support all four body kinds. Eight native regressions prove bounds/degeneracy/mass validation, exact geometry, dynamic landing/replay, pending impulses, kinematic controls and sensor events/filtering. Explicit convex-model scene/Session bindings now have native scale, dynamic landing, mixed-compound, lifecycle, atomic failure and checkpoint-aware decoded-GLB replacement regressions. Shared convex discovery/export/player/headless/editor Play loading, project-backed defaults and ownership guards are implemented. Model Collision Lab adds a Decay-driven falling GLB prop and impulse/travel goal. Explorer game proof remains pending. Contract |
| Convex model collision project loading | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Shared discovery/export and player/headless/editor Play installation retain both collision representations per logical GLB. Failed convex preparation leaves shared triangle/hull bindings intact. Project model defaults and ownership guards are tested. Model Collision Lab exercises a falling prop, Decay impulse and travel goal; Explorer adoption remains open |
| Pre-synchronization controls for convex-only bodies | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Convex-only and mixed bodies accept existing typed motion reads/setters/impulses before materialization. Host authoring checks preserve earlier valid requests; full hull geometry/mass validation occurs at synchronization. Native regressions exercise scaled/combined mass, ordering, locks, invalid authoring, deferred binding retry and checkpoint replay. Model Collision Lab spawns a typed GLB prefab and queues controls before its first solve; Explorer game adoption remains open |
| Carry on a scene-owned 3D character | ❌ | — | ❌ | ❌ | Absent | Controllers collide geometrically, but carry support excludes other controllers: they remain stationary during the solve and apply input afterward, so controller-on-controller carry would otherwise depend on entity processing order |
| Compound solid character probes | ❌ | — | ❌ | ❌ | Absent | Scene controllers require one solid primitive piece; extra sensors are allowed. Multiple solid probes are rejected because the movement primitive sweeps one convex shape |
| Same-step solver response to controller motion | ❌ | — | ❌ | ❌ | Absent | Scene controller moves occur after the solve to use current platform poses. Queries/rendering see them immediately; dynamic response and discrete sensor processing see them at the following solve. No controller push impulse is applied |
| Swept controller trigger events | ❌ | — | ❌ | ❌ | Absent | Controller sweeps exclude sensors; enter/exit remains discrete at solver boundaries. Crossing a small sensor between solves can produce no event |
| Sweeping rotating platform carry along its arc | ❌ | — | ❌ | ❌ | Absent | Platform carry sweeps the straight line between synchronized support-point positions; large rotations can miss obstacles along the circular arc. Hosts can use smaller synchronized steps, but no continuous arc sweep exists |
| Sweeping a rotating character probe | ❌ | — | ❌ | ❌ | Absent | Movement and platform carry retain the probe rotation. There is no swept orientation change or automatic rotation of a box/capsule riding a turning support; endpoint penetration remains explicit |
| Moving a body by its transform | ✅ | — | ✅ | ✅ | Par | A script writing a body's position teleports it, keeping its velocity, as in Unity; a position-kinematic body takes it as its next target. The platformer's respawn is the proof |
| One-way platforms | ✅ | 🟡 | ✅ | ✅ | Behind | Local support normal/cone on solid pieces, including tilemaps; CCD pair filtering and timed Decay drop-through. Platformer planks prove ascent, descent/landing and dropping to an ordinary floor. Command-backed policy/undo regressions preserve velocity/joints. Native, WASM and Chromium export/load smoke passed; visual inspector and full browser interaction verification remain in final integration. Geometric queries still see both sides |
| Continuous collision (CCD) | ✅ | 🟡 | ✅ | ✅ | Behind | Opt-in 2D body flag, scene synchronization and typed Decay controls; thin kinematic-wall and spawn-window/undo lifecycle regressions. Platformer hero opts in; command-backed edits/undo preserve solver state. Native and WASM checks passed; visual inspector/browser verification remains in final integration. Sensors stay discrete; bullet-versus-bullet sweeping is not guaranteed |
| Collider gizmos in the Scene view | — | ✅ | — | — | Par | Every 2D collider is outlined from the pieces physics is given, tilemap boxes included; the selected one's box edges, circle radius and capsule height drag, one undo step a drag. 3D colliders are drawn as wireframes. Offsets and rotations are still typed, and 3D pieces have no handles |
| Scene gravity | ✅ | ✅ | — | ✅ | Par | sindri.physics2d.world sets the scene's gravity, so editor Play runs a platformer as its build will; the platformer and Scorchball each set theirs. One vector; no per-area gravity. Decay reads and turns it with Physics.gravity/Physics.set_gravity (the Physics Playground's GRAVITY button), and a change wakes resting bodies in 2D and 3D |
Navigation and grids
Sindri's strongest domain relative to the baseline.
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Orthogonal and isometric grids | ✅ | ✅ | ✅ | ✅ | Ahead | First-class, not a plugin |
| Footprints, occupancy, walls | ✅ | ✅ | ✅ | ✅ | Ahead | — |
| A* over whole footprints | ✅ | ✅ | ✅ | ✅ | Ahead | Unity needs A* Pathfinding Project for the equivalent |
| Navmesh / off-grid navigation | ❌ | ❌ | ❌ | ❌ | Absent | Grid only. Fine for the games we build; a wall for a free-movement game |
| Steering / avoidance | ❌ | ❌ | ❌ | ❌ | Absent | Agents walk through each other |
| Per-path costs and policies | ❌ | ❌ | ❌ | — | Behind | One uniform cost |
| Viewport wall painting, height tools | — | 🟡 | — | ✅ | Behind | Inspector authoring only |
Audio
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| WAV / Ogg / MP3, native + browser + silent backends | ✅ | ✅ | ✅ | ✅ | Par | The editor's Play now plays what a build plays: autoplay sources and every scripted Audio request, through the same mixer. With no device it plays silently and still lists what is playing |
| Play, loop, pause, resume, stop | ✅ | 🟡 | ✅ | ✅ | Par | — |
| Per-play volume | ✅ | — | 🟡 | ✅ | Behind | — |
| Buses / mixer / master volume | ✅ | 🟡 | ✅ | ✅ | Behind | Every sound plays through a named bus under master; AudioMixer applies bus gain in front of every backend, changing live voices. Decay: Audio.set_volume, Audio.volume, play_on/loop_on; an authored source names its bus. Orbital's pause screen moves master, music and effects and saves them; Sound Mixer is the feature example. The editor's inspector edits a source's bus as text. The editor's Audio panel lists every bus Play uses and every sound playing, and monitors them with a trim, mute and solo per bus that change only what the author hears, never Audio.volume. No level meters, and a project cannot yet declare its buses and their starting volumes. No effects (reverb, ducking) or snapshot blending |
| Spatial audio / panning | ❌ | ❌ | ❌ | ❌ | Absent | — |
| Per-voice handles | ❌ | — | ❌ | — | Behind | A script cannot stop the specific sound it started |
| Music transitions / crossfade | 🟡 | ❌ | 🟡 | ✅ | Behind | Causeway crossfades playing tracks by setting named bus volumes from Decay. A dedicated authored music-transition system is not established. |
| Sound effect authoring (Sound Lab) | — | ❌ | — | ✅ | Behind | tools/sindri-chiptune designs seeded game effects (coin, jump, laser, explosion, power-up, hurt, blip, thud) from editable parameters and sends each into a project: the WAV and its .sindrisfx source under assets/audio/, listed in [assets] include so an export ships it, with the Audio.play line to paste. The platformer's jump, landing, coin and flag sounds were made and delivered this way, its run hears all four, and a test re-renders each committed source to its WAV. A standalone tool, not an editor panel. It also runs on the site (/examples/sound-lab/), where Send becomes a download of the same two files |
| Procedural music authoring / MIDI note editing | ❌ | ❌ | ❌ | ❌ | Absent in engine | tools/sindri-chiptune composes 32-bar chiptune songs from seeded chord loops, six grooves and a developed motif that returns, editable monophonic scores, bounded MIDI interchange and .sindrisong files that reopen for editing, and sends a song into a project as a looping clip. Its workstation locks individual tracks across generation, can regenerate one part against the same harmony while preserving the others, and can make an in-key lead variation from the current melody. Variations keep rhythm and repeated phrase positions, preserve the resolving note, and can be undone; locks remain session-only and .sindrisong stays compatible. While tracks are locked, generation retains the current seed, key and mood so replacement parts share that harmonic context. The standalone Lab also seeds coordinated instrument ensembles while keeping all layers enabled by default; section participation remains manually editable, with separate sound and arrangement locks, independent A/B mixes, random 64-bit seeds and stable word/phrase seeds. Bar-range regeneration, ensemble auditioning, selected-note harmony inspection and named snapshots let users develop an existing score. Explicit motif transforms, Arpeggio/Lead call-and-response, performance expression and related soundtrack-family snapshots develop that material further. New sessions use versioned Musical generation: related Rise motifs, modal eight-bar cadences, accompaniment spacing and individual-voice chord inversions from bar roots. Saved songs and snapshots retain the version; missing fields select Legacy and its previous generation/playback rules. Saved masks and presets reopen explicitly; old songs default to the original full-layer mix and the CLI retains a --classic mode. A stateful stereo engine plays it: band-limited oscillators, patch filters and envelopes, chords read from the bass, a synthesized kit, reverb, echo, ducking and a limited bus at about -14 LUFS, with seamless loops. A song sent to a project is encoded as Ogg Vorbis (the reference encoder, fixed stream serial), about a seventh of its WAV; Causeway plays one at 2 MB. The browser Lab still downloads WAV, and there is no shorter-loop export. Audio-device audition remains unverified |
Input
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Keyboard, mouse, touch behind the platform boundary | ✅ | ✅ | ✅ | ✅ | Par | — |
| Mouse displacement for 3D look | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Read-only Pointer.delta / Input.Pointer.delta returns accumulated viewport-pixel mouse movement once per fixed step, positive right/down. Bridge and Orbit Camera Lab Session tests exercise component/vector access, reset, re-entry, atomic write refusal and drag-to-look with pitch limits. Feature example evidence; Editor Play exercises raw captured motion through the same input state; Explorer game proof remains absent. Touch uses Gesture. Captured motion uses native raw device counts or browser physical pixels; cursor capture is exercised in Orbit Camera Lab |
| Pointer world ray | ✅ | — | ✅ | ✅ | Par | Read-only Pointer.ray_origin and Pointer.ray_direction are the world camera's ray through the pointer, worked out by the session before scripts run as Aim is. A game whose ground is a model rather than a voxel world tests it against its own cells and figures; Causeway picks cells this way, and its click tests assert the cell under a projected pixel. Zero with no pointer or no world camera |
| Pointer lock | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Pointer.lock() / unlock() and read-only locked, also under Input.Pointer, connect Session to native/browser and native editor Play capture. Shared relative motion excludes cursor warps, clears on release/focus loss and is spent once per fixed step. Orbit Camera Lab, a native X11/Vulkan window and real Chromium exercise unbounded motion, U/Escape/focus release and browser sandbox denial without false success or gameplay interruption. Native editor Play verifies raw motion, capture/release, pause/stop, focus changes and hidden-view release. Explorer game proof remains pending |
| Unified pointer, bounded fingers | ✅ | ✅ | ✅ | ✅ | Par | Active/released touch takes priority over parked mouse; Causeway sea swipes cover hybrid input. |
| Touch stick built from a finger | ✅ | — | ✅ | ✅ | Ahead | A considered solution to a problem most engines leave to the game. Low Tide steers, walks and drives with it on a phone, draws it where the thumb landed, and its tests play a whole salvage run by touch |
| Stick anchor in screen units | ❌ | — | ❌ | — | Behind | Stick.anchor_x/anchor_y are physical pixels, and a script cannot learn the view's size in pixels, so a game drawing its stick cannot place the ring from the numbers the input came from. Low Tide reads Pointer.overlay when the stick engages instead, which is right only while the steering thumb is the pointer's finger |
| Integer-scale pixel camera | ❌ | — | ❌ | — | Behind | pixel_perfect snaps the camera's position to the pixel grid but not the scale: an orthographic camera with a fixed vertical_size draws a 16 pixel tile at whatever fraction the window gives, so a 320 by 180 pixel game is exact at 960 by 540 and 1920 by 1080 and uneven at 1366 by 768. A pixel game wants the largest whole multiple that fits, with the rest letterboxed. Found drawing Lamplighter's art (docs/lamplighter-plan.md); to be closed in the engine, in its own change |
| Action mapping (named actions, rebindable) | ✅ | 🟡 | ✅ | ✅ | Behind | A scene declares sindri.input.actions; scripts read Action.held/pressed/released/axis/vector and rebind with Action.rebind/Action.last_pressed, written back into the component. The platformer's hero runs and jumps by actions; the Input example rebinds boost. Declared per scene rather than per project, edited in the inspector as raw JSON, no per-player maps, and no automatic binding persistence; Orbital saves and restores its burst binding through Save.text |
| Gamepad, by player slot | ✅ | 🟡 | ✅ | ✅ | Behind | Pads on desktop and in the browser, read by player slot: a face button or Start joins, unplugging leaves, one join per frame. Scorchball is played by up to four pads and its tests press them. Play in the editor reads pads, not yet exercised with a real one in CI. No rumble or per-player action map |
| Scroll wheel | 🟡 | — | 🟡 | ✅ | Behind | Scrolls UI scroll regions in every host (Orbital's pause manual, the Control Room archive); a script sees the region's offset, not the wheel itself |
| Rebinding UI | ❌ | ❌ | ✅ | ✅ | Behind | No engine widget; a game builds one from Action.bindings, Action.last_pressed and Action.rebind, as the Input example does. Rewired sells on this |
UI and presentation
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Anchored image and text, fill bars, buttons, row/column, safe area | ✅ | ✅ | ✅ | ✅ | Par | Low Tide exercises viewport-sized HUD/menu panels, direct blueprint buttons, live material costs, icon-led hold/speed/tide meters and contextual actions; keyboard/phone runtime tests cover selection, refusal and returning to play. A crate outline and cell-bound discard confirmation are Decay gameplay on existing APIs. Wreck-child cargo prefabs can be chosen by proximity, set down and recovered ashore; runtime tests exercise inventory and per-slot streaming history. Its workbench is authored from existing UI primitives, not a new dialog widget. |
| Pointer hit-testing, hover/press/held | ✅ | ✅ | ✅ | ✅ | Par | — |
| Weave responsive stylesheets | 🟡 | 🟡 | — | ✅ | Ahead | CSS selectors (element names, compounds, descendant and child combinators, lists) with CSS specificity, inheritance, custom properties with var(), and combined media queries. Neither baseline has stylesheets this close to CSS; docs/ui-direction.md is the plan to make it complete. :hover and :active follow the pointer in every host, and hit-testing uses the presented geometry. The editor's Styles section shows each element's box model, matched rules with file:line and overridden declarations, and computed values; a value edited there is written into the stylesheet, and an element can be picked by clicking it in the running game |
| Project fonts | ✅ | ✅ | — | ✅ | Par | — |
| Slider | ✅ | ✅ | ✅ | ✅ | Par | Horizontal or vertical, with min, max, step, disabled and a label; a drag and a scripted write pass through the same clamp-and-quantize contract, so a value cannot enter the component off-step. Weave styles it like any other node. Ui.slider_value, Ui.set_slider_value and Ui.slider_changed read, write and detect the change. The editor names it in Add Component with orientation as a declared choice — but no field carries a bounded-range meaning, so value is a free number box beside the range it is supposed to obey. A focused slider moves by its step under the arrows, the d-pad or the stick, and takes autofocus. Orbital's pause screen mixes its audio with three, saved between sessions, and Sound Mixer is the feature example |
| Toggle and checkbox | ✅ | 🟡 | ✅ | ✅ | Par | sindri.ui.toggle, one boolean component whose art is authored: a switch or a checkbox is a matter of styling, with Weave's :checked, :disabled, :hover and :focus. Click, tap, Space or Enter flips it; Ui.is_checked, Ui.set_checked and Ui.changed read, write and detect it. Orbital proves it with a compact-HUD switch that is saved and changes the HUD. The editor lists it in Add Component with plain fields; there is no dedicated authoring gizmo |
| Text input (single line) | ✅ | 🟡 | ✅ | ✅ | Par | sindri.ui.text_input with a placeholder and a limit counted in characters. Editing happens at a caret with a selection: Left, Right, Home, End, Shift to select, Backspace, Delete, and Ctrl or Cmd with A, C, X and V through the system clipboard; Ui.caret, Ui.selection_start and Ui.selection_end let a script draw the caret and highlight. IME composition is committed once on desktop; in a browser the field gives the page's focus to a hidden textarea, so composition, paste and a phone's on-screen keyboard reach it. Keys are held back from gameplay while it edits. Orbital's pilot callsign proves it. No multi-line field, no word-wise movement, and no caret placed by clicking inside the text (a click puts it at the end); iOS raises its keyboard only for a tap that lands while the game is already editing |
| Scroll region | 🟡 | 🟡 | ✅ | ✅ | Behind | sindri.ui.scroll scrolls vertically by wheel, drag or finger, clamps to its laid-out content, clips drawing and hit-testing to itself (nested clips intersect), and a drag never becomes a click on a row. Ui.scroll_offset and Ui.set_scroll_offset. Orbital's pause screen scrolls a field manual. Vertical only, no scrollbar or momentum, and a rotated region clips to its axis-aligned bounds |
| Radio group and dropdown | ✅ | 🟡 | ✅ | ✅ | Par | Toggles sharing a group are a radio group. sindri.ui.dropdown opens a popup of authored sindri.ui.option rows that the engine shows only while open; the selected option is :checked and the open header :open. Click, tap, keyboard or pad open it, choose, and close it; a press elsewhere or Escape puts it away, and the arrows stay inside an open list. Ui.selected, Ui.set_selected, Ui.is_open. Orbital's boss picker is a scrolling dropdown of the twelve bosses; the Control Room has both |
| Tabs, dialog, tooltip | ❌ | ❌ | ❌ | ❌ | Absent | Still built by hand from buttons and switched-off screens |
| Rich text | ✅ | ✅ | ✅ | ✅ | Behind | rich: true on sindri.ui.text or sindri.text reads [b], [i] and [color=#rrggbb] spans, nested, with [[ for a bracket; a span's color fades with the text's alpha, and measuring, wrapping and reveals ignore the tags. Orbital's critical hits are a bold gold span. No inline images, size changes or links. Contract: docs/world-text.md |
| World-space text | ✅ | ✅ | ✅ | ✅ | Par | sindri.text is the UI label's fields in world units, placed at its entity and drawn over the sprites of its layer through the world camera. Decay sets its words with Ui.set_text and Ui.set_number and reaches this.world_text.{color,font_size,visible,layer}. Orbital's landed shots float their damage. Labels stay level unless follow_rotation turns them with their entity. Contract: docs/world-text.md |
| UI transitions | ✅ | ✅ | ✅ | ✅ | Par | CSS transition with named and cubic-bezier easings, delays and all, on colours, lengths and numbers; USS has the same. @keyframes is still absent |
| UI devtools (computed style, which rule won) | — | ✅ | — | — | Par | Unity's UI Toolkit debugger was the bar. The editor's Styles section now meets it: box model, matched rules with file:line, overridden declarations and computed values, and an element picked by clicking the running game — see the Weave row |
| Keyboard focus navigation | ✅ | — | ✅ | ✅ | Par | Tab and Shift+Tab walk controls in the order the scene is written; the arrow keys move focus to the nearest control that way, preferring those in line, as browser spatial navigation does; a row reached inside a scroll region is scrolled into view; Space or Enter presses, Escape backs out. Weave's :focus shows it and Ui.is_focused reads it. No focus scopes beyond an open dropdown |
| Gamepad focus navigation | 🟡 | — | ✅ | ✅ | Behind | Any pad's d-pad moves focus as the arrows do, South presses or submits and East backs out, in every host. A control marked autofocus takes focus when its screen appears, so a menu is ready for a pad; with nothing focused and nothing asking, the d-pad and arrows focus nothing, so gameplay that uses them never presses a button. The Control Room's regression drives its settings with a pad, and Orbital's title, chooser, pause and results all start focused. A pad's left stick moves focus as the d-pad does, once per push, and moves a focused slider. There is no per-player focus |
| Accessibility labels | ❌ | ❌ | ❌ | ❌ | Absent | — |
| Drag and drop | ❌ | ❌ | ❌ | ❌ | Absent | — |
Effects
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Pooled fleck bursts, batched with sprites | ✅ | 🟡 | ✅ | ✅ | Behind | Measured and fast, but it is one effect shape |
| Particle system (emitters, curves, shapes) | ✅ | 🟡 | ✅ | ✅ | Behind | sindri.particles emits into the fleck pool with rate, cap, lifetime, speed, direction and spread, point, circle, box and ring shapes, gravity, drag, size over life, a color ramp, rotation, spin and world or local simulation, from its own random stream. Decay plays, stops and emits; the Scene view previews the selected emitter. Particle Lab adds editor and browser authoring with the shared particle simulation and JSON export; its preview supports dropped textures but does not reproduce scene lighting. The platformer's coins sparkle and its hero trails dust; Low Tide's treads throw dust and its engine smokes while it drives. Linear size only rather than curves, no sheet animation over life, sub-emitters, trails or collision, and no curve editor; the inspector and Particle Lab expose the color ramp. Contract: docs/particles.md |
| Effect preview in the editor | — | 🟡 | — | 🟡 | Behind | The Scene view previews selected particle emitters, and Particle Lab previews the shared simulation and applies edits. Its preview does not reproduce scene lighting; broader effect preview is not established. See particles. |
| Trails / line renderer | ❌ | ❌ | ❌ | ❌ | Absent | — |
| Screen-space feedback (shake, flash, hitstop) | 🟡 | 🟡 | 🟡 | 🟡 | Behind | Camera behavior provides deterministic trauma shake. A general coordinated feedback system with flash and hitstop remains absent; see the camera row for its authoring and Decay limits. |
Scripting (Decay)
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Typed host, compile / run / reload | ✅ | 🟡 | ✅ | ✅ | Par | Source editing stays external. A previewed script shows its compile errors, from the check Play compiles with |
| Fractional component writes from integer-authored numbers | ❌ | — | ❌ | ❌ | Behind | A generic numeric leaf the scene wrote as an integer rounds a script's write, so a fractional 2D light intensity can be lost; a channel reached by index -- a color's alpha, a shader parameter -- no longer rounds. Checked 3D light/environment setters use dedicated floating-point storage; other leaves still need declared storage semantics (sindri-decay/host/access.rs) |
| Safe entity, prefab, profile references | ✅ | ✅ | ✅ | ✅ | Ahead | A stale handle is refused, not a null dereference |
@export properties discovered and authored |
✅ | ✅ | ✅ | ✅ | Par | — |
| Language server: highlight, complete, hover, definitions, diagnostics | — | 🟡 | ✅ | ✅ | Par | VS Code only |
Vectors as values (Vec2, Vec3) |
✅ | 🟡 | ✅ | ✅ | Par | Arithmetic, length, normalized, dot, distance, lerp; transform, pointer and stick hand them out whole. Proven in Scorchball's ball and Orbital Last Stand's bullets. A vector @export is stored as [x, y] and drawn by the inspector's X/Y/Z number row, as a position is; that has not been looked at in the running editor |
| Rotations as values | 🟡 | — | 🟡 | 🟡 | Behind | Copied parent-relative Quat x/y/z/w values use Decay structs. Atomic normalized whole writes reject zero/non-finite inputs and preserve placement/scale/Z lock. Native script regressions and Transform Lab Session/browser save/copy/restore proof; Euler authoring remains. Explorer game proof is pending |
| Quaternion composition and interpolation | ❌ | — | ❌ | ❌ | Behind | No quaternion multiplication, axis-angle value constructor or slerp API; existing Euler and world-space aim/orbit controls remain available |
| Scripts reach each other by type | ✅ | ✅ | ✅ | ✅ | Par | Bolt.on(e).damage, live fields, typed messages delivered after the pass; checked at compile time across files. Orbital Last Stand's bullets are set up this way, and Scorchball's kicks and power-ups; Orbital's enemy damage reads and Orbital Baked's signals still use the untyped calls |
| Events (emit, handle anywhere) | ✅ | ✅ | ✅ | ✅ | Par | event GoalScored(team: f32);, GoalScored.emit(1.0), on GoalScored(team) { }; delivered after the pass to every listener, checked at compile time across files. Scorchball's goals, wind and fireball |
| Shared game state, declared and typed | 🟡 | ✅ | 🟡 | 🟡 | Par | state Game { var score: f32 = 0.0; } gives Game.score, checked across files, with one type and one starting value; kept on the board so Game.get("score", 0.0) still reads it. Numbers, flags and enums. Scorchball, the platformer and Orbital use it: Orbital Last Stand declares 108 values and Orbital Baked 121. Each keeps under a hundred and fifty Game.get/Game.set calls for keys built at runtime and for eight values whose callers disagree about the fallback (hp, max_hp, boss_hp, boss_max, next_level, sector, target_x, target_y) |
| Timers as values | ✅ | 🟡 | ✅ | ✅ | Par | Timer(2.4) with done, left, duration, progress; a script's timer fields run down before each update. Scorchball's banners, power-ups, burning and bot reactions use them. No pause, and no timer that calls something when it ends |
| Shared helper functions across files | ✅ | ✅ | ✅ | ✅ | Par | A top-level fn is its file's; shared fn is callable by name from every script in the project, checked across files, with no this. No namespaces: shared names are one flat space per project, clashes refused. Orbital's view helpers use it |
| Constants | ✅ | ✅ | ✅ | ✅ | Par | const LIMIT: f32 = 3.0; for a file, shared const for the project; numbers, flags, text and enum variants, worked out at compile time from literals, operators and other constants, and written in place. None inside a script or function, and no vector, list or struct constants. Orbital's VIEW_SIZE |
| Structs | ✅ | ✅ | ✅ | ✅ | Par | struct Card { name: String, weight: f32 } in any file; built with every field named, read and written by field, copied where assigned, kept in lists. Methods after the fields, asked of a value with this a read-only copy; none that change their struct. A field may have a default, worked out like a constant, and then may be left out when one is built. A scene authors a struct or list @export as JSON, and the inspector edits it field by field and item by item. Orbital's module chooser uses one, with show and hide methods, and reads its card names from a scene-authored list |
| Lists and ranges | ✅ | ✅ | ✅ | ✅ | Par | [a, b], push, pop, insert, remove_at, clear, contains, index_of, xs[i] = v, for i in 0..n; the same List<T> host queries return. Value semantics, capped at 10,000 elements. No sort, filter or slicing. Orbital's module chooser uses them |
| Maps | ✅ | ✅ | ✅ | ✅ | Par | Map<K, V>, written ["a": 1.0] or [:]; m[k], m[k] = v, get(k, fallback), contains, remove, clear, keys(), values(), length. Keyed by text, numbers, flags, variants or entities, in the order keys were first set; value semantics, capped at 10,000 keys. Not authored by a scene yet, and no game uses one yet |
| Colours | ✅ | ✅ | ✅ | ✅ | Par | Color(r, g, b[, a]) or Color("#rrggbb"); r/g/b/a read and written by channel; lerp, with_alpha; constants and struct defaults may be colours. The engine's tint, fill, stroke, color_multiply and color_offset are Color, assigned whole or by channel. Orbital's charger elite colours use it |
| Optional values | ✅ | ✅ | ✅ | ✅ | Par | f32?, String?, List<T>?: a value or null, never used as its type until ?? fallback or a check (!= null, or an early return on == null) narrows a let or parameter. Replaces -1 sentinels |
| Text: joining and methods | ✅ | ✅ | ✅ | ✅ | Par | "Score " + score, joining numbers, flags, vectors and variants; length, contains, starts_with, ends_with, find, slice, replace, case, trimming. Capped at 64 KiB. Numbers written with n.fixed(digits) and n.padded(width); no format strings or interpolation, and UI templates still own a HUD's words. Scorchball's clips, Orbital's stat keys and run clock |
Enums and exhaustive match |
✅ | ✅ | ✅ | ✅ | Par | enum Phase { Lobby, Play }, written Phase.Lobby; match must cover every variant or end with _. Usable in state and as an @export authored by name, with an inspector dropdown. match runs blocks as a statement, or gives a value where one goes (let clip = match kind { Power.Grow => "enlarger", ... };), every arm the same type. No variants carrying data, and a value arm is an expression, not a block. Scorchball's phase and power-ups use them |
| Coroutines / sequencing | ❌ | — | ❌ | — | Absent | "Wait a second, then do this" is a Timer field and a check, not a sequence |
| Debugger (breakpoints, stepping) | ❌ | ❌ | ❌ | — | Absent | print debugging only |
| Formatter | ❌ | ❌ | ❌ | — | Absent | — |
| Script and entity throughput at scale | ❌ | ❌ | ❌ | ❌ | Absent | Rendering is benchmarked and the Profiler times each script, but nothing measures how many scripted entities a frame can run, natively or in the browser. Survivor-likes and colony or factory sims depend on this number |
| In-editor script editing | — | ❌ | — | — | Behind | Scripts are created and previewed read-only; editing is external |
| Signals / event bus | 🟡 | 🟡 | 🟡 | ✅ | Behind | Typed Decay events broadcast to listeners and Scorchball uses them; typed references and queued messages also support direct communication. The scope and limitations of these events compared with a general event bus need an explicit audit; see Events (emit, handle anywhere). |
| Script unit tests | ❌ | ❌ | ❌ | ❌ | Absent | No way to test a .decay file without running a game |
Editor experience
Where the most day-to-day friction is, and where one architectural change removes most of it.
The root cause of hand-typed fields (fixed)
This records the defect ranked queue item 1 removed; the tables it describes are gone. It stays because its conclusion — a component declares what its fields mean, and no consumer guesses it from a name — is the rule every new component follows.
The editor used to infer meaning for some fields, and it is worth being precise about how, because the mechanism was the defect rather than its absence.
Three lookup tables in the editor guessed a field's meaning from its name:
// editor/src/native/inspector_panel/field.rs
match (type_name, key) {
(_, "texture") => Some(assets.textures),
(_, "font") => Some(assets.fonts),
(_, "clip") => Some(assets.audio),
("sindri.script", "source") => Some(assets.scripts),
_ => None,
}
plus is_colour, which requires the key to be literally tint, color or
colour, and editor/src/inspector/choices.rs, which maps (type_name, key)
to an enum's spellings. The choices module is careful — it takes each list
from the engine's own constants rather than repeating them — but it is still a
table in the editor keyed by field name.
Four consequences followed, and they were why more table entries was not the fix:
- The bare-key rules are global.
(_, "texture"),(_, "font")and(_, "clip")match any component, including one a game brings of its own. A game component with aclipfield is offered the audio list whether or not it holds audio. - Anything absent from the table is a free-text box, silently. A field
named
sheet,iconorportraitgets nothing. This is what happens to every component added since the table was written, which is why new work keeps landing as raw fields. - A colour must be spelled
tint. Named anything else it is four number boxes; and the check isNumbers(4), which a UV rect and a quaternion also satisfy. - It lives in the editor.
sindri-capabilitiescannot document it, and no other tool can use it.
Underneath all four: meaning is guessed by the consumer rather than declared
by the component. That is a second copy of knowledge about a component living
away from the component — exactly the drift check_template was written to stop
for field lists. ComponentSchemaRegistry stores a field template, a
serde_json::Value exemplar checked against what serde asks the type for, so it
captures each field's shape and nothing about its meaning. A texture id
and a display label are both String to the registry.
A second consequence used to follow, and is now fixed. A value that is an array
of objects fell to ValueKind::Opaque and was displayed as stored — the
pieces array of a compound collider is exactly that shape, so compounds were
authorable in a scene file and not in the inspector. The template's exemplar
item is what closed it: it says what a piece consists of, what each field means,
and what a fresh one is, so the panel can draw a list and add to it without
inventing anything.
The fix was to move meaning to the registration — asset(texture), asset(script), asset(clip), entity reference, choice, colour, angle, bounded range, list-of — and have the editor read it instead of guessing. It extended the registry that already existed, made the knowledge checkable against the template the way field lists already were, travels to every tool rather than only the editor, and covers components not yet written. It was the highest-leverage item in this file.
| Feature | Editor | vs. baseline | Gap that matters |
|---|---|---|---|
| Scene view, hierarchy, generic inspector, project browser | ✅ | Par | — |
| Gizmos: transform, snapping, Z-lock-safe movement | ✅ | Par | 2D colliders, 3D colliders, 2D joints, a selected character's footing and a selected effect burst's reach are drawn; only 2D colliders have handles. No camera-behaviour gizmo |
| Play / pause / stop / single-step, snapshot restore | ✅ | Ahead | Single-step and snapshot restore are better than Unity's play mode, and so is editing while playing: Unity throws play-mode changes away, Sindri lists them at Stop to keep or discard. Play steps the shipped runtime session on a styled world, and a parity test ends four games in the same world in the editor and the build |
| Tilemap painting, sheet slicer, texture picker | ✅ | Par | — |
| Normal map studio | ✅ | Ahead | Unity and Godot leave normal maps to outside tools. The editor Normal map tab and standalone browser tool make maps from sprite brightness or a painted height map, per sheet cell, with presets, and offer split, channel and orbiting-plane previews. The editor uses the game's own 2D lighting and can point scene sprites at the saved map; both tools save the map and its settings. The platformer's hero proves it. The browser light preview is approximate; the editor uses the game renderer. No painting on the map itself, and the 3D view shows the 2D convention because the 3D renderer reads no normal maps. Contract: docs/normal-map-studio.md |
| Asset pickers for schema fields generally | ✅ | Ahead | Declared per component in the schema registry, checked against the field template, and carried in docs/generated/. Unity needs a plugin (Odin) for the equivalent |
| Array-of-object editing | ✅ | Par | A list of objects is added to, removed from and reordered, with each item's fields drawn through its meanings. Decided by the template, so a tilemap's thousand tiles stay a readout |
| Tagged-enum (variant) fields | ✅ | Par | A field that decides what else its object holds is switched as one edit, at any depth — Godot's equivalent is swapping a Resource subtype. What is ours is that every variant is proved to decode at startup, so an unpickable one fails the build rather than the scene |
| Console / log panel | ✅ | Par | Every failure the editor reports, plus script print named by the entity that printed it, filtered by level, repeats collapsed to a count, with a jump to the entity a line is about and an error count in the status bar. It can now be put wherever it is wanted — it opens beside the Scene view — so watching the log no longer costs the project browser, and a failure recurring every frame is one counted line wherever it sits in the log rather than a new line per frame. What is wrong now is grouped by cause, one message with every entity it reaches under it, and the status bar counts causes |
| Profiler view | 🟡 | Behind | The Profiler panel times the whole editor frame by phase — upkeep, each gameplay phase of Play, the views' presentation, extraction and encoding, panels, painting and waiting — as a stacked chart of the last 300 frames against the 60 fps budget, with each script's share, slowest first, and optionally while editing. CPU only: no GPU timings, no per-call breakdown inside a script, and shipped builds are not profiled in-game |
| Search / filter in hierarchy or project | ✅ | Par | The hierarchy filters entities and retains matching ancestors; the project browser searches assets. Multi-select and bulk edit remain separate gaps. |
| Project settings surface | ❌ | Absent | sindri.toml is edited by hand |
| Build / export UI | ❌ | Absent | Export is CLI-only |
| Prefab creation from a selection | ✅ | Par | Make prefab writes a subtree as a prefab beside the scene and puts an instance in its place, keeping instances inside it nested; on a selection, a prefab of each as one undo step |
| Save inspector | ❌ | Absent | Persistence is play-testable but not viewable |
| Multi-select and bulk edit | ❌ | Absent | — |
| Customisable layout | ✅ | Par | Every panel is a tab, draggable into any of seven docks or four scene-anchored overlays — edges dock, corners float — with the arrangement and every size persisted. Three presets to start from. Behind Unity and Unreal only in that a panel cannot yet be torn off into a window of its own |
| Canvas-first workspace | ✅ | Ahead | The scene view is the document and panels overlay its corners, rather than the viewport being the rectangle left over when the docks have taken theirs. Unity and Godot have no equivalent posture; the nearest comparison is a design tool. Still to come: command palette, chrome collapse, and the Game view as an anchored thumbnail — see docs/editor-direction.md |
| Guided local-AI setup | ✅ | Ahead | One button inside the editor downloads a pinned llama.cpp runner and model into the person's own folder, verifies both by SHA-256, starts the runner and checks the model against Sindri's repair cases, showing each step with progress, speed and time left, and resuming where it stopped. No installer, terminal, download page or password; removal is one button. Linux x86-64 and Apple silicon only — Windows and Intel Macs are told it is not available yet. No comparable engine ships a local-model setup: Unity and Unreal have no local story, and third-party assistants start from "install this yourself and paste a key" |
| Local AI Decay repair | 🟡 | Par | A previewed script that does not compile offers Propose a fix once a model has repaired both verification cases. The answer is compiled by the same check Play uses and sent back with its own errors at most twice; only a candidate that compiles and keeps every declared script is shown, as a diff, and it is written only on Accept, with the previous text kept to put back. Unity's assistant proposes code through a cloud service and does not grade it with the compiler first. No model runs in CI — the loop is proved against scripted models — and it rewrites whole files rather than making semantic edits. Writing a new script from a description is the next use of the same loop |
| Project check for coding agents | ❌ | Behind | Decay scripts are checked with structured JSON by decay-lsp --check --json; scenes, prefabs, Weave and sindri.toml are validated only when the editor or a game loads them, so an agent editing them cannot tell whether its work is valid. Unity ships a CLI and an official agent plugin for this. See docs/agent-native-direction.md |
| Headless play for coding agents | ❌ | Absent | Each game has a scripted-run test and some have offscreen capture binaries, but there is no general command that runs a project with scripted input and returns its log and frames |
| Reload a scene changed on disk | ❌ | Behind | Scripts, textures, prefabs and Weave styles are watched; the open scene is not, so an external edit is lost on the next save unless the scene is reloaded by hand |
| Command palette | ✅ | Ahead | Ctrl+K over panels, entities, project files, scenes, arrangements and verbs, ranked by a scored subsequence match with multi-term search. Unity has no equivalent; Unreal's is command-only and Godot's is files-only |
| Customisable shortcuts | ❌ | Behind | Keys are fixed in native/shortcuts.rs |
| Live edit while playing | ❌ | Behind | Stop restores the world wholesale, so a value tuned during a run is lost. The largest single cost in the change-and-feel loop; see docs/editor-direction.md |
| Record and scrub a run | ❌ | Absent | The step is fixed and deterministic, so this is available rather than aspirational, and nothing offers it |
| In-editor script editing | ❌ | Behind | — |
Build, export, and platform
| Feature | Engine | Editor | Decay | Proof | vs. baseline | Gap that matters |
|---|---|---|---|---|---|---|
| Static web export, base-path aware, content-hashed | ✅ | ❌ | — | ✅ | Ahead | Genuinely simple next to Unity's WebGL output |
| Native desktop | ✅ | ✅ | — | ✅ | Par | — |
| Game-independent browser project host | ✅ | — | — | ✅ | Par | sindri-player loads and runs exported projects without Causeway-specific game logic. Browser saves use an entry-scene-based key; see export for its scope and migration behavior. |
| Browser / WASM | ✅ | — | — | ✅ | Par | — |
Versioned sindri.toml, validation |
✅ | 🟡 | — | ✅ | Par | — |
| Native packaging (installer, icon, splash) | ❌ | ❌ | — | ❌ | Absent | A desktop build cannot be shipped to a player as-is |
| Mobile targets | ❌ | ❌ | — | ❌ | Absent | Touch input exists; a build does not |
| Console targets | ❌ | ❌ | — | ❌ | Won't | Not a realistic target and should not pretend to be |
Diagnostics
| Feature | State | vs. baseline | Gap that matters |
|---|---|---|---|
| Named validation errors at the boundary | ✅ | Ahead | — |
| Deterministic replay from a seed | ✅ | Ahead | Reproducing a bug is a seed, not a video |
| In-editor console | ✅ | Par | See the editor section. It can be placed anywhere and opens beside the Scene view |
| Profiler / frame timing | 🟡 | Behind | The editor's Profiler panel times every phase of its frame and each script; sindri-editor --benchmark and project-benchmark record comparable reports for the editor and the shipped host's loop. A fixed 300-prop perspective benchmark has native project/extraction and browser ready/render proof, with authored shadows applied in player/capture hosts. Standalone benchmark reports measure physical textured 3D/model draws and passes, uniform writes/bytes and scoped geometry/cache upload events; shadowed/unshadowed consecutive-frame CI checks exercise them. Editor benchmark frames carry matching primary-view counters and separate extra Scene-view work, with capture/reset regressions and native shadow-control runs. Native standalone benchmarks optionally record GPU timestamp intervals separately from CPU wait, with supported/null status and real query reuse/baseline-device proof. Native editor benchmarks also time primary and extra Scene viewport intervals independently, with preserved device policy and native shadow-control/two-view proof. Sprite/shape/glyph physical batch draws, passes, instances and buffer-write payloads are also reported, with GPU empty/reuse proof and native Platformer reports. Clears, post, atlas/resource uploads, editor UI totals and browser timestamps remain absent; a running browser or desktop build has no timing of its own |
| Debug draw from scripts | ❌ | Absent | A script cannot draw a line to show what it thinks it is doing |
| Frame / draw-call debugger | ❌ | Absent | — |
| Crash and error reporting in a shipped build | ❌ | Absent | — |
Absent whole systems
Named so they are decisions rather than oversights.
| System | Status | Position |
|---|---|---|
| Localization | Absent | No string table, no locale switch. Needed before any non-English release |
| Networking / multiplayer | Won't, for now | A large system that no planned game needs. Revisit only with a game that requires it |
| Video playback | Absent | Rarely load-bearing for 2D games |
| Visual scripting | Won't | Decay is the answer. A second authoring path would split the ecosystem |
| In-game console / cheats | Absent | Cheap, and useful for testing |
| Object pooling (general) | Behind | Effects pool internally; nothing general exists |
| Analytics / telemetry | Won't | Not our business to add |
What the Unity Asset Store proves
A plugin that sells for a decade is a feature the engine should have had. This is a market-validated gap list, and it is more honest than our own judgement because somebody paid for every row.
Tier 1 — Unity had to absorb it
The strongest possible evidence: Unity bought or cloned these, conceding they were native gaps. Treat these rows as close to automatic.
| Package | What it fixed | Sindri |
|---|---|---|
| TextMeshPro | Text quality, rich text | Behind — distance-field text with outlines and shadows, and color, bold and italic spans; no inline sprites or per-glyph animation |
| Cinemachine | Camera follow, framing, confining, shake | Behind — sindri.camera.behavior follows, confines and shakes, and the platformer uses it; Decay camera-mode controls and authoring gizmos remain |
| Post Processing Stack | Bloom, colour grading, vignette | Behind — an authored world stack now covers exposure, tone mapping, contrast, saturation, bloom, and vignette; LUT grading and advanced cinematic effects remain |
| Shader Graph | Shader authoring without code | Absent — no visual shader graph; numeric materials and sprite WGSL exist |
| Input System | Action mapping, rebinding, gamepad | Behind — actions reach scripts and rebind at runtime; gamepads read by player slot, without rumble or per-player actions |
| Addressables | Asset streaming and release | Absent — not urgent at our scale |
| ProBuilder | In-editor geometry | Won't — a general in-editor mesh modeling suite is not planned; 3D authoring remains in scope |
Tier 2 — still not native, still selling
| Package | What it fixes | Sindri |
|---|---|---|
| Odin Inspector | Unity's inspector is not sufficient for real data | Par — components declare what their fields mean, so asset pickers, colours, choices, lists and variants need no plugin, which is the anti-goal met. Multi-select and bulk edit remain absent |
| DOTween | Tweening and easing | Behind — managed tweens support sequences, loops and yoyo; property binding and completion callbacks remain |
| A* Pathfinding Project | Real pathfinding | Ahead — ours is native |
| Behavior Designer | Behaviour trees, AI authoring | Absent — AI is hand-written Decay |
| Rewired | Input mapping and rebinding | Behind — as above |
| Feel / NiceVibrations | Game juice: shake, hitstop, flash | Behind — camera shake exists; integrated flash and hitstop remain |
| Master Audio | Buses, mixing, music transitions | Behind — named buses and scripted crossfades exist; bus effects and authored transitions remain |
| Easy Save | Save/load that works | Ahead — versioned, atomic, damaged-state aware, three backends |
| Spine / 2D Animation | Skeletal 2D | Absent |
| Dialogue System | Branching conversation | Absent — a profile catalog could carry the data today |
| Localization packages | String tables | Absent |
| Peek / editor productivity | Search, navigation, bulk edit | Behind — hierarchy and project search exist; multi-select and bulk edit remain absent |
Read together, the two tiers say the same thing three times: camera, tweening, and input mapping are the features people reliably pay to add. Camera is now built and used; input mapping is built and used, including Orbital’s saved burst binding; managed tweening is now used in Orbital, with composition built and property binding still absent.
The ranked queue
This is a current proposal, ordered by impact on games and shipping. Each item needs a scoped design and proof in the named project before it is marked done; the order does not imply that every gap has been independently retested.
- Integer-scale pixel camera. Lamplighter's 320 by 180 art grid needs a whole-number scale and letterboxing at arbitrary window sizes. Prove it on different desktop and phone viewports in the game.
- Decay language tooling correctness. Follow the P0 items in decay-lsp-modernization.md: synchronization, workspace refresh, positions and protocol coverage. Incorrect diagnostics and completion undermine every game using Decay.
- 3D character animation. Follow the skinning slice in 3d-update.md: import a rigged GLB, sample and render a clip, expose playback and authoring, and prove it in a real game. Establish a small end-to-end slice before claiming general animation support.
- Complete the shared 3D path where a game needs it. Material texture bindings and alpha, model authoring and performance remain; choose a concrete Causeway or 3D game need and follow the dependencies in the 3D plan.
- Ship a complete game. Use a real game's release to expose the remaining asset import settings, compression, native packaging and shipped-build error reporting needs. Browser export already uses the shared project host.
- Shorten the editor feedback loop. Prioritize multi-select/bulk editing, live edits during Play, and export and project settings workflows against work in Lamplighter and Causeway.
- World persistence and navigation as games demand them. Voxel edits need a durable save path for a game that depends on digging; off-grid navigation and avoidance need a game that exercises them.
- Finish scoped profiling. Native benchmark GPU intervals exist; the interactive profiler, browser GPU timing and full-frame accounting remain incomplete. Measure actual game bottlenecks before setting new targets.
- Scene serialization and input follow-ups. A world containing multiple
scenes does not round-trip through
to_scene; project-level/per-player input and binding persistence also remain. Prioritize the specific game failure each would resolve.
The original queue's basic autotiling, camera behavior, audio buses, tween composition, 2D query and inspector slices have shipped. Their remaining limitations are recorded in their feature rows rather than described as absent. The 3D query, editor and game-proof gaps are not closed by the 2D query slice.
Proof that lived outside this repository
The advanced sprite colour transform and the UI slider were both found by Mujaffa Remaster, an external project, and for a while neither had a user here. Both now do: the slider in Orbital's pause mixer and Sound Mixer, and the colour transform in Orbital's mine blast, which flashes white-hot through the colour offset because a tint, being a multiply, can only darken.
Beyond parity — where Sindri could lead
Everything above answers "what is an engine expected to do", and every row can be checked against Unity or Godot. This section answers a different question: what could an engine provide that none of them do? It is kept separate deliberately. Mixed into the tables above, a reader could no longer tell "the baseline has this and we do not" from "nobody has this and we might", and that distinction is what makes the rest of this file worth reading.
Nothing here is scheduled. These are candidates, and they compete with each other rather than with the ranked queue.
The thesis
Sindri already refuses to hand a game raw dt and raw key states and wish it
luck. It has a fixed step, input edges consumed exactly once, and a seeded
stream that replays a run on every host. Read together those are one idea:
The engine gives you primitives for translating imperfect human input and time into deterministic gameplay.
That is a stronger position than any single feature below, and it is a description of what Sindri is rather than a direction bolted on. The candidates worth taking are the ones that follow from it.
Judged against the games, not against plausibility
A candidate earns a row by replacing something a game in this repository is doing by hand today. Where a game is not asking for it, that is recorded too — an idea that sounds good and nothing needs is the most expensive kind.
| Candidate | What it would replace | Position |
|---|---|---|
| Gameplay spatial queries — nearest, within radius, within cone, within box, over tagged entities and backed by an index | Orbital player uses World.nearest with a World.within_radius fallback for off-screen targets; Arc uses sorted radius results with its impact-distance filter |
Partly taken. World.nearest and inclusive radius scans use canonical world transforms and world-order ties. Cone/box queries and indexing remain deferred |
| Entity lifecycle policies — despawn after a duration, off-camera, or on animation end | World.despawn(this.entity) and a hand-decremented countdown in bullet, beam, arc, core, charger, drifter, challenger |
Take. Small, and seven scripts want it |
| Cooldowns and charges — start, ready, remaining, normalised, recharge | player.decay hand-rolls cooldown and mine_cooldown; director.decay hand-rolls spawn_timer |
Partly taken. Decay's Timer covers start, ready (done), remaining (left) and normalised (progress), and runs down on its own; charges and recharge are not built. Orbital's countdowns have not moved yet |
| Buffered actions — a press remembered for a window and consumed exactly once | Nothing yet; the games are not platformers | Take, but narrowed. See below |
| Named time domains — gameplay, UI, physics and real clocks, scalable and freezable | Nothing. No game here scales time; only pause exists | Row, not queue. Best fit with the thesis, no current demand, and cross-cutting: every consumer must declare a clock |
| Gameplay sensors — vision cone, aggro radius, interaction range, with enter/stay/leave | Enemy scripts compute their own geometry | Downstream. Mostly spatial queries plus a component; thin once those exist |
| Feedback orchestration — one asset firing sound, effect, shake, haptics, hit-stop and flash | Real glue: camera shake and particles exist, but haptics, hit-stop and general feedback composition remain absent | Capstone. It would coordinate capabilities that remain incomplete |
| Deterministic state history — a short rolling window of selected properties | Nothing. No game here rewinds, trails, or replays | Low. Unusually cheap given determinism, and nothing needs it |
| Spawn regions and patterns — a random point in a circle, edge, or area | A few lines of Decay per use | Utility, not a system. Does not earn a subsystem |
The one that changes something above
Spatial queries expose a framing error in this file's own physics section, which
lists raycast and overlap as physics queries. Target selection in
player.decay is not a physics problem — it is a gameplay query over tagged
entities. The first slice now lives in World.nearest and World.within_radius,
with Orbital proving the filtered targeting path. Physics overlap/shape casts, cone/box
queries and spatial indexing remain separate follow-up work.
Determinism also matters here in a way it does not for the baseline: a query that answers in world order answers the same on every host, and a game built on "the nearest enemy" then replays from a seed. Neither Unity nor Godot promises that.
Buffering, narrowed
The tempting version of this is a general forgiveness system — jump buffering, coyote time, grace periods — authored in one block. Half of it does not belong in an engine.
"Remember a press for 120ms and let gameplay consume it exactly once" knows nothing about a game and is genuinely engine-level. "A grace period after being grounded" requires the engine to know what grounded means, which is game-specific: a top-down shooter has no such state, and an engine that assumed one would be a system games fight rather than use.
So the half worth taking is the input half, and it already has a home: the
input action layer in sindri-platform, which now reaches scripts.
Buffered press and consume-once are features of a named action, not a new
subsystem — which folds this candidate into an existing queue item instead of
adding an eleventh.
Decay tooling modernization
Decay is the primary gameplay language, so language tooling is part of the
authoring surface rather than optional editor polish. The September 2026 deep
audit found concrete drift and protocol/test gaps in decay-lsp and the VS Code
integration. The complete prioritized checklist and definition of done live in
docs/decay-lsp-modernization.md. Update that
checklist in the same change that closes or discovers a Decay tooling gap.
Maintenance rule
Update this file in the same change that moves any cell, as
AGENTS.md requires. Four specific rules keep it from rotting into a
checklist nobody reads:
- A surface cell claims only what is implemented and exercised. For a
gameplay capability, exercised means a game uses it, and the gap cell names
which — see "What counts as proof".
capabilities.mdremains the detailed evidence. - A parity cell is a judgement and must survive an argument. "Ahead" is a claim about the baseline, not enthusiasm.
- Add the row before the feature. The value of this file is that absent things have rows. A gap discovered during work belongs here in the same change, marked ❌, even when nothing is planned.
- Keep the two questions apart. Everything above "Beyond parity" is answerable against Unity or Godot. Everything below it is not, and moving a candidate up requires the baseline to have grown it, not for us to have liked the idea. A candidate earns its place by naming what a game in this repository does by hand — and when no game wants it, the row says so.