# Changelog All notable changes to the Ludic toolchain, newest first. Each section is generated from the changesets under changes/ by `ludic dev release`, grouped by change type. Preview the next one with `ludic dev release --dry-run`. A released section may carry a hand-written summary paragraph above its groups (v0.2.0 has one); the generated bullets below it are not edited by hand. ## v0.5.0 — 2026-09-05 ### Features - **One command installs Ludic, and `ludic` is the command you use.** Getting started no longer means cloning the repository and learning a task runner called `x`. - **`curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh`** installs a complete toolchain — compiler, CLI, engine runtime, bundled `ludic.*` packages, formatter and language server — into `~/.ludic` and puts it on your `PATH`. Prebuilt artifacts are verified against a published checksum; where none exists for the platform, the installer bootstraps from the compiler's own IR seed with clang. Uninstalling is `rm -rf ~/.ludic`, and `ludic upgrade` re-runs the same script. - **`ludic` replaces `x`** and is the only command a user of the language meets: `ludic new` scaffolds a project that builds and plays as it stands, `ludic run` / `ludic build` compile it (`--headless` for a deterministic render), `ludic test` runs every `test` block in the project, and `ludic add` / `get` / `update` / `verify` / `vendor` drive packages. `ludic fmt` and `ludic lsp` are the formatter and language server, so an editor needs no path configuration. `ludic doctor` reports whether the install is complete and usable. - **The toolchain's own tasks moved under `ludic dev`** — `dev build`, `dev test`, `dev reseed`, `dev bootstrap-cfree`, `dev docs-gen`, `dev release` and the rest are unchanged apart from the namespace. `bin/x` is gone; the bootstrap is now `clang selfhost/ludicc.seed.ll -o bin/ludicc && bin/ludicc tools/ludic-cli/main.ludic -o bin/ludic`. - **An install root is a first-class layout.** The compiler derives it from its own location — the parent of its `bin/` directory — so `~/.ludic` and a repo checkout are the same shape, and `$LUDIC_HOME` is no longer needed to build a windowed game outside the repo. The bundled `ludic.*` packages resolve from `$LUDIC_HOME/packages`, so `import "ludic.core/components.ludic"` works with no `ludic_modules/` to set up. Release artifacts are complete install roots (`bin/` beside `runtime/`, `packages/` and `VERSION`) rather than bare binaries, and the installer ships with the documentation site it is served from. ### Fixes - **Token cards are back on the docs site.** Clicking a keyword, type, builtin or namespace method in any code sample opens its summary card again. The card's styling had been left behind in `docs.css` during the site redesign, so on the landing page — which links only `base.css` and `site.css` — the card rendered unstyled at the foot of the document instead of beside the token. It now lives in `base.css` with the rest of the highlighter chrome, and is positioned `fixed`, matching the viewport coordinates the script computes, so a card opened on a scrolled reference page lands on its token rather than off it. - Release checksums are one `.sha256` file per artifact instead of a single `SHA256SUMS`. A release is assembled from more than one host — a Linux runner cannot build the macOS toolchain — and `ludic dev publish` never overwrites an asset that is already attached, so a shared `SHA256SUMS` was written by whichever host published first and then never covered anything added afterwards. Per-artifact names compose across hosts. Verify one with `shasum -a 256 -c ludic-X.Y.Z-src.tar.gz.sha256`. ### Documentation - CONTRIBUTING documents what a self-hosted Forgejo runner needs for CI to work at all: every workflow clones `${{ github.server_url }}`, which on a self-hosted instance is an internal address, so job containers must be able to resolve it. The runner's default is a fresh per-job network the Forgejo container is not on, which fails the clone — intermittently, because Docker forwards unresolved names to the host resolver, so CI can look healthy for a while before it stops. - The README is rewritten around what a reader needs first: what the language is, a code sample, how to build it, and an honest status. Removed the repo-layout table and the Chrono Rift keybindings (a game manual in a language README), the nine links to wiki pages that no longer exist, and a "language at a glance" bullet describing a retired vocabulary — it advertised `system`, `reads`, `writes`, `requires` and `ensures`, none of which are keywords; the declaration keyword is `handler`. ### CI - The commit-lint workflow survives a force-push. It linted `${{ github.event.before }}..${{ github.sha }}` without checking that `before` still resolves, so rewriting or garbage-collecting that commit failed the job with `fatal: Invalid revision range` on a push whose messages were all valid. It now falls back to linting the tip commit when `before` is gone. - The docs deploy is serialised. Publishing is a force-push of an orphan `pages` branch, so two runs racing could land out of order and leave the site holding the older build — with both runs reporting success. A `pages-deploy` concurrency group with `cancel-in-progress` means a newer push cancels an older in-flight build instead of queueing behind it. ## v0.4.0 — 2026-09-05 ### Features - **Prefabs.** `prefab Grunt: Creature { Stats { hp: 30 }, Weapon { def_id: 1 } }` names a model with preset fields; `spawn Grunt { Position { x: 40 } }` spawns it, the spawn's own fields winning over the presets. Prefabs chain (`prefab Grunt: Foe` where `Foe` is a prefab) so shared presets live once. `spawn` is now also an expression yielding the new entity (`let e = spawn Grunt { … }`), and `Prefab.spawn(name: "Grunt")` spawns one chosen at runtime by name (-1 when none matches). - **`countdown` fields.** A component field declared `frames_left: countdown = 0` is an `int` the engine steps toward 0 once per Update for every live entity carrying the component (never below 0). Roll timers, invulnerability frames, hit flashes and cooldowns need no hand-written "decrement each frame" handler: set the field, test it. - A `machine` over an enum-typed store maps its states to the enum by name: with `enum HeroState { Idle, Rolling }` and `var hero_state: HeroState = HeroState.Idle`, `machine hero_state { state Idle { … } state Rolling { … } }` dispatches on `HeroState.Idle` / `HeroState.Rolling` — no `state Idle = HeroState.Idle` repetition, and a state that names no variant is a compile error. A bare enum is now a first-class `int`-sized type for `var`, params, fields and returns (`llty`), and `Enum.Variant` folds in a global initializer. - A handler inside a scene `layer` may carry `@Queries(these: […], on: Model)`, so a scene can own its per-entity systems (`@Queries(these: [Particle]) handler AgeSparks phase Update { … }` runs once per matching entity only while that scene is active). Any other annotation on a layer handler is reported. - A program-scope `var` may be initialized with any expression: `var run: Progress = new Progress`, `var speed: int = BASE_SPEED * 2`, `var origin: IVec2 = IVec2.zero()`. Initializers the compiler cannot fold run once at startup (`@L_init_globals`, after the runtime boots and before the `Start` phase), in declaration order. Previously such an initializer was silently replaced by `0` / `null`. - Engine managers for what every action game hand-rolls: **`Fx.sparks` / `Fx.number` / `Fx.clear`** — engine-owned sparks and floating damage numbers, moved and aged each Update and drawn after the sprites, with no component, model, handler or draw call in the game; **`Audio.define(name:, path:)` + `Audio.play(name:)` / `Audio.play_music(name:)` / `Audio.named`** — a sound bank by name (the handle form still works); **`Camera.shake_for(amount:, frames:)`** — a timed shake the engine decays; **`Assets.enqueue`** now loads `.wav` / `.mp3` into the sound bank and `.ttf` / `.ttc` into a font table (**`Assets.font(name:)`**) alongside images, so one loading scene covers everything; **`Prop.count()`** — how many live entities carry a component. - Every compiler diagnostic is reported as `file:line: error: message` — the file the line really lives in, even through imports — so editors can jump to it. Unexpected characters are errors (they used to be skipped silently), and defining one `function` twice is reported in source terms instead of failing in the IR assembler. - Less to write for a game. **`Map` cell API** — `Map.get/set/fill/rect/border/random_cell/is_solid/is_solid_at/width/height`: a game edits the engine's tilemap in place and asks it what is solid (from the `Solids` config) instead of keeping its own grid. **`Sprite` does the small animation work**: `move_id` is the strip drawn while the entity moves, `face: 1` turns it toward its movement, and the `flash` / `blink` countdowns give a white hit flash and an invulnerability blink with no handler. **`scene X shows Menu`** — the engine opens the menu (and frees the cursor) on enter, draws it last in Overlay, and closes it on exit. **`import "dir/*.ludic"`** imports a directory in name order. **`IVec2.distance2/within/heading/along/step`** and **`Angle.diff_degrees`** cover the geometry every action game rewrites; **`List.sample`** draws distinct random picks; **`Input.move_i`** is the standard top-down movement intent; **`AimMode.Auto`** aims with the mouse, or the right stick while a pad is connected; **`Weapon.set_rate` / `Weapon.rate`** change a fire rate in place; projectiles now die on `Solids` tiles by themselves; **`Screen.bar`** draws a meter. Handlers inside a scene's layers are scene-qualified (`Play_Draw`), so two scenes may both name a handler `Draw`; `enable` / `disable` of a scene's own handler by its bare name still works from inside that scene. - The engine's numeric parameters have names. ludic.core: `BodyPolicy { Platformer, TopDown }`, `BoundsPolicy { Clamp, Wrap, Bounce, Kill }`, `AnimMode { Loop, Once, PingPong }`; ludic.shooter: `AimMode { Mouse, RightStick, MoveDirection, NearestEnemy }`, `WeaponPattern { Single, Cone, Ring, Spiral }`; ludic.npcai: `BrainModel { StateMachine, Utility, BehaviourTree }`, `AiState { Patrol, Chase, Attack, Flee }`; ludic.gameplay: `StatKind { MaxHp, Attack, Defense, Speed }`, `ModifyOp { Flat, Percent }`; ludic.rpg: `StatusKind { Poison, Regen }`; the input runtime: `CursorMode { Normal, Hidden, Locked, Confined }`. `Body { policy: BodyPolicy.TopDown }` reads as what it is; the old integers still work. The input runtime also names the gamepad buttons: `PadButton { A, B, X, Y, LeftShoulder, RightShoulder, Back, Start }` for `Input.bind_pad(button:)`. - The second "write less" round, all generic. **ludic.gameplay**: `Stats` carries the build stats every action game bolts on — `damage_pct`, `crit_pct`, `leech_pct` / `leech_hp`, `thorns`, `fire_rate_pct` — and `Combat.damage` applies them itself (a `Crit` event fires; thorns never reflect thorns); `Stats.add(e, stat, amount)` changes a base stat in place and `Stats.scale_hp(e, percent)` scales hp and max_hp; `StatKind` names every code. **ludic.shooter**: `Dash { frames, speed, cooldown_frames }` with `Dash.start(e, dx, dy)` / `Dash.active(e)` — a dodge roll with i-frames the package guards; `Melee { range, half_arc, damage, knockback, frames, cooldown_frames, arc }` with `Melee.swing(e)` (hits every hostile in the arc, knocks back, fires `MeleeHit`) / `Melee.ready` / `Melee.active`; projectiles drawn by the engine in their weapon's colour (`Weapon.set_color`); `TopDown { reticle, reticle_length }` draws the aim line and a mouse cross; the weapon system honours `Stats.fire_rate_pct`. **ludic.dungeon** (new package): `Dungeon.arena / open_arena / random_style / set_exit / entry_point / opposite / at_edge`, with `Side` and `RoomStyle` — arena rooms with mirrored cover, door lanes and exits by side, built into the engine tilemap. **Compiler**: `scene Splash lasts N then Next` (a timed scene), `button … goto: Scene` (a click changes scene, no listener to write). **Runtime**: `Map.random_cell_far`, `Sprite.draw_meter` (hearts / pips), `Assets.enqueue_dir`, `Collider.center`, `Prefs.max`. Also `scene X loads then Y` (the loading scene: pumped, drawn, `AssetsReady` fired), `Prop.despawn_all()`, `Map.to_tile`, and `Brain { hunt_blind }` in ludic.npcai (seek the nearest hostile without line of sight). `Prefab.spawn_at(name:, at:)` spawns and places; `Weapon.reset(id)` restores a definition (no pierce, no homing, its fire rate). Five regression examples cover the additions (`examples/library/prefabs`, `component_access`, `scene_menus`, `managers`, `combat_kit`). `Random.weighted(weights:)` draws an index by weight; `ui` widgets inherit `font` / `size` / `fg` / `align` from their panel; the input runtime names `MouseButton { Left, Right, Middle }`. - `@ClearColor(expr)` takes any constant expression, so a named palette colour (`@ClearColor(COLOR_FLOOR)`) works as well as a hex literal. - `Ai.seek` + path-aware `brain_seek` — when a Solids tilemap is present the NPC-AI routes a blocked straight line around obstacles with `Grid.a_star`, so foes flow around pillars instead of getting stuck. - `Key.*` compile-time key constants (`Key.Space`, `Key.Escape`, `Key.A`, `Key.Up`, ...), folded like `Color.*`; and `Font.*` / `Ui.* / File.*` namespaces so `png_load`/`font_load`/file I/O are namespaced. - `Os.pid()` returns the process id, for scratch files that concurrent runs of one tool must not share. - `Overlay` render phase — runs after the engine Render systems (sprites, lights) and before present, so a game's HUD / menus are never painted under an actor. Byte-identical when unused. - `Prop.of(entity)` and `Prop.has(entity)` — typed access to one entity's component from an entity handle, the same binding a query loop makes. `Hero.of(player).iframes = 20` reads and writes fields directly (no `World.prop_id` / `World.field_id` / `World.get` reflection chain); `Prop.has(e)` is true when `e` is in range, alive, and carries the property, so `-1` is a safe "no entity". A package that declares a real `prop_of` / `prop_has` function keeps it. - `Solids.solid2` — an optional second solid glyph (e.g. a closed door) the move system also blocks. - `Sprite.strip(sheet, col, row, count, rows)` — register N consecutive animation frames in one call (the base id for `SpriteAnim`). - `Sprite` component draws atlas ids (#90) — `Sprite.atlas = 1` routes `esys_sprite` through `atlas_draw_ex` (scale/flip/tint) so atlas cells / multi-cell spans (tall characters) use the engine sprite-render system, not just the 16x16 table. - `Ui.close()` — deactivate the retained UI (no menu open); the readable form of `Ui.open(id: -1)`. - `[a, b, c]` list literals build a slice in place; the first element fixes the element type, later elements must match, and `[]` is an error (use `new []T`). Tables of records read as `[Row { … }, Row { … }]`. - `ludic.prefs` package — `Prefs.*`, a human-readable `key=value` text store for scores / options (the right tool for "remember my best run"; a whole-world `Save.write` is not). - `v.x` and `v.y` read the components of an `IVec2` value (a local, a global, a record field, or a call result) — the readable form of `IVec2.x(v)` / `IVec2.y(v)`. The compiler's static typing now also follows function return types, namespace calls, `Prop.of(e)` and record fields, so `@Computed` fields expand in those positions too. - engine tilemap-render system (`TileSkin`, shipped from ludic.core) — one entity per glyph paints the whole `Map.*` grid each Render frame *before* sprites, so a game stops hand-looping the map. `esys_tileskin` registered ahead of `esys_sprite`. - engine-driven retained UI — a program with a `ui` block has its navigation ticked by the frame loop automatically (from the frame key) and an activation now emits a `UiClicked { id }` event, so scenes react with `@On(UiClicked)` instead of polling `Ui.clicked`. New `Ui.*` namespace (`Ui.open/tick/clicked/set_text/render/build`). ### Fixes - A `UI_Name` handle can be read from any code — a plain function, an `@On(UiClicked)` listener, a global initializer — not only from handlers and scene hooks. The widget table it indexes is now built on first use instead of when `@ui_build` is emitted, which came after functions and listeners and crashed the compiler on such a reference. - A `var` declared twice — including a game `var` whose name the spliced engine runtime already uses (`ui_font`, `grid`, …) — is now a compile error naming the variable and, when it is the runtime's, saying so (`variable ui_font is also a variable of the engine runtime; choose another name`). Previously the two became one LLVM global and clang reported a redefinition in generated IR. The same check covers `property` names (`property Cell is also a property of the engine runtime; choose another name`); before, the first declaration silently won and field lookups failed with a confusing message. - A `{` or `}` inside a string literal within an interpolation hole (`` `{f("{")}` ``) is text, not structure; the hole scanner used to miscount it. - A windowed build that reaches the audio runtime indirectly — through the atlas / `Assets.*` preload queue (which feeds `.wav`/`.mp3` into the sound bank) or the engine sprite-render system, without any `Audio.*` call in the game — now links the native audio backend (`audio.ll` + AVFoundation). Previously `ludicc -o` failed at link with undefined `snd_*` symbols for any windowed game declaring a `Sprite` component; the import of `runtime/native/audio.ludic` now flags the backend link itself. - Character literals accept the same escapes as strings (`'\''`, `'\\'` and `'\"'` were silently read as 0); an unterminated character literal is now an error. - Hand-written runtime preludes (string, Os.*, Fs.*, Crypto.*, …) now live under their own `@lp_` symbol prefix, so a user `function` named `is_ws`, `str_eq`, `path_join` and the like no longer collides with them at link time. - Named arguments now work on namespace functions (`@Namespace(Foo)` and `namespace Foo { export function … }`), not only on builtins and bare functions: `Weapon.def(name: "pistol", fire_rate: 9, damage: 14, speed: 8, spread: 0, pellets: 1, pattern: 0)` reorders to the declared parameter order like any other call. Previously every named call on a namespace function failed with "wrong number of arguments". - The documented bootstrap works on a fresh clone. `bin/` is gitignored and not checked in, so `clang selfhost/ludicc.seed.ll -o bin/ludicc` — the first command in the README, in COMPILING, in CONTRIBUTING and on the site — failed with `ld: open() failed, errno=2 for 'bin/ludicc'`. Every copy now begins with `mkdir -p bin`, which is what CI had been doing all along. - Unary minus keeps its operand type: `-f` on a `fixed` is a `fixed` (it was typed `int`, which broke mixed arithmetic and comparisons). - `become Scene` now works from an `@On(Event)` listener, a global handler, or a plain function (#91). Code outside a scene's own layers cannot know the leaving scene at compile time, so the compiler emits `@L_scene_leave()` — a dispatch on the live scene id that runs its `on exit` — and calls it there. Previously a listener's `become` reused the last emitted handler's scene (or crashed), and a global handler's `become` skipped the leaving scene's `on exit` entirely. - `ludic-fmt` keeps `rows[i]`, `new []int`, `s[a..b]`, `emit(…)`, `~x` and list-literal braces tight, and recognises `<< >> & | ^ ~` as operators. - `ludic-fmt` no longer glues an opening parenthesis to a preceding operator: `let moving = (a or b)` stays as written instead of becoming `let moving =(a or b)`. - `self()` inside an `@OnSpawn(Model)` or `@OnAttach(Property)` body is now the entity being constructed. Previously it was the entity of the innermost query loop — or the constant 0 when the spawn happened outside any loop — so a hook such as `@OnSpawn(Hero) handler Remember { player = self() }` silently recorded entity 0. - `x += y` / `-=` / `*=` / `/=` now lower exactly like `x = x op y`: a Q16.16 `fixed` multiplies and divides through the 64-bit path, a string `+=` concatenates, and an `int` added to a `long` widens (they previously emitted raw integer arithmetic on the LLVM type). - `x release` keeps a changeset's markdown intact. Bodies used to go through `tr '\n' ' '`, which flattened every multi-line changeset into one paragraph — nested bullets came out as inline `" - "` runs and a release read as a single unbroken wall of text. A section is now grouped by change type (**Features**, **Fixes**, **Performance**, …) with one bullet per changeset and continuation lines indented to stay inside it. - `x release --dry-run` renders the next section to stdout and writes nothing, so a release can be read before it is cut. - `x changelog-section ` prints one release's section from `CHANGELOG.md`; `x changelog-render` re-renders a section from a directory of changesets. The v0.1.0 and v0.3.0 sections were re-rendered with these. - cursor `mode 3` (confined) now keeps the OS cursor **associated** (absolute position preserved) and hidden, instead of dissociating it like `mode 2` (lock/relative). Only true-lock `mode 2` uses relative deltas now; `win_mouse` reports the absolute position on `mode 3` and clamps it to the framebuffer. And `mode 3` now **physically confines** the cursor: each frame the platform layer warps it back to the window's content rect (`CGWarpMouseCursorPosition`) whenever it strays past the edge, so clicks can't land outside and the window keeps focus. This lets a top-down game hide + confine the cursor while `aim_mode 0` (mouse aim) keeps resolving to where the reticle points — previously any confine/lock mode silently broke absolute mouse aim, and a confined cursor still escaped the window (#89 follow-up). - reserved words (`new`, `match`, `spawn`, ...) can no longer name a function — the compiler errors instead of miscompiling. - the shooter aims / homes / fires from a body's **centre** (Position + Collider offset + half-size) instead of the Position anchor, so auto-aim and homing target what is drawn, not a corner. ### Documentation - The generated site is redesigned around reading rather than launching: a warm paper ground with a serif display face, one ink-blue accent, and rules instead of floating cards. Colour is reserved for code. Dark mode is the same design with the ground inverted, driven entirely by tokens under one `prefers-color-scheme` block, and the landing page's scroll-reveal animations, gradient headline, glowing badge and emoji feature icons are gone. - Stylesheets are linked files (`base.css` + `site.css`/`docs.css`) instead of being inlined into all 900+ pages, which cuts the published site from 16 MB to 5 MB and means a design change no longer requires regenerating to be seen. - Fonts are the platform's own; the site makes no webfont request. - `api.css` was dead — the generator never referenced it — and is removed along with `item.css`, which `docs.css` replaces. - A page no longer flashes its own title on every plain visit; only a deep link highlights its target, and under `prefers-reduced-motion` the highlight no longer stays on the element permanently. - The copy leads with what is verifiable — ahead-of-time compiled, an ECS in the syntax, deterministic fixed-point, no C in a build — and the "get started" steps now begin with the clang-plus-seed bootstrap, without which `bin/x` does not exist on a clean checkout. ### Build - `x` no longer prints a clang warning on every build. Each `clang` invocation the task runner makes now passes `-Wno-override-module`, the same flag `ludicc` already passes for its own link step: the emitted IR names no target triple, so clang substitutes the host's and says so — four times per `x build`, with nothing to act on. (The comment in `selfhost/main.ludic` claimed the opposite, that the IR *does* carry a triple; it does not.) ### CI - Releases are published by CI from a tag instead of by hand from a laptop. The new `release` workflow triggers on a `v*` tag, builds the toolchain from the IR seed, runs `x test`, `x test-tools` and `x bootstrap-cfree` against the tagged tree, and only then creates the Forgejo release. It refuses to publish when the tag and `VERSION` disagree or `CHANGELOG.md` has no section for that version. `x publish [vX.Y.Z]` is the command behind it and works locally too: it builds `dist/` (a source tarball from the tag, this host's toolchain, and a `SHA256SUMS` covering both — releases previously shipped no checksums) and takes the release notes from that version's `CHANGELOG.md` section, so the notes and the changelog cannot drift. Re-running it only adds assets the release is missing, which is how a macOS build gets attached to a Linux-built release. ## v0.3.0 — 2026-09-02 ### Features - **Tiled map support (#67–#74)** — load and draw [Tiled](https://www.mapeditor.org/) maps (TMX/TSX/TX and TMJ/TSJ/TJ), the design record from #66. - **P0 parsing primitives (#67)** — a minimal pure-Ludic XML reader (`Xml.*`) for the element/attribute/CDATA subset TMX/TSX/TX use; standard base64 decode/encode (`Base64.*`, RFC 4648), whose decoder ignores the whitespace Tiled wraps into ``; and gzip framing (`z_gunzip`, RFC 1952) wrapping the existing DEFLATE inflater. zlib and the JSON reader already shipped. A curated, attributed golden corpus lands under `assets/tiled-fixtures/`. - **P0.5 TMX/TSX reader (#68)** — `Tiled.read` / `Tiled.read_tsx` map the native XML formats onto the *same* intermediate the JSON path produces — a `Value` tree in Tiled's JSON schema, with every tile layer's data decoded to a dense GID list (CSV, base64, base64+zlib, base64+gzip). A CSV `.tmx` and a base64+zlib `.tmj` of the same map read structurally identically; the in-repo Kenney `sampleMap.tmx` + external `sampleSheet.tsx` load with no manual JSON re-export. - **P1 core load + render (#69)** — the runtime `rt_tmap` model (heap-allocated to `w·h`, lifting the old `96×64` cap), the GID resolver (`Tiled.resolve` → tileset / local id / H·V·D flips), image-backed rendering (`Tiled.draw`, flips applied at blit), and the legacy-tilemap compatibility projection so `Grid.*`/`Path.*`/`esys_move` keep working. `Tiled.load` reads either format, resolves external tilesets + images, and auto-projects a `collision` layer. The Kenney sample loads and renders pixel-identically from `.tmx` and `.tmj`; the `grid` and `physics_tiles` demos now run off a loaded map. - **P2 collision & grid (#70)** — normalise three collision sources into the byte tilemap `esys_move`/`Grid.*`/`Path.*` read, in the design's priority order: per-tile `` hitboxes, the `solid`/`oneway`/`trigger` property convention (`Tiled.collision_kind`/`Tiled.tile_shapes`), and the designated collision layer (`Tiled.project`, any non-zero GID solid) — or drive collision from a visual layer's per-tile metadata alone (`Tiled.collide`). The property convention and the collision-layer fallback produce the same feed; `Path.a_star` over a loaded map matches the hand-authored baseline. - **P3 animated tiles + tile objects (#71)** — a tileset `` advances deterministically as a pure function of the fixed 60/s engine frame clock (`Tiled.frame_gid`/`Tiled.animated`), so an animated GID resolves at draw to the current frame's GID with its flip flags preserved and reproduces frame-for-frame across runs; `Tiled.draw_anim` draws a map with animations advanced. Object-layer entries with a `gid` render the tile image (with their own flips), bottom-anchored, as placeable sprites. - **P4 objects, properties, templates, spawning (#72)** — all object shapes (rectangle / ellipse / point / polygon / polyline / text) and custom properties parse and are queryable (`Tiled.object`, `Tiled.object_shape`, `Tiled.prop`/`Tiled.prop_int`/`Tiled.prop_type`); class properties resolve their defaults against a project custom-type table (`Tiled.load_types` over `objecttypes.xml`); template instances inherit their `.tx`/`.tj` template's fields; and an object maps onto Ludic components on demand (`Tiled.spawn`/`Tiled.spawn_layer`, off by default, via the reflection ABI). - **P5 breadth (#73)** — image layers (parallax + repeat) and group layers (flattened, with recursive offset/opacity/tint/visible; `Tiled.layer_kind`/`Tiled.layer_offsetx`/`Tiled.layer_tint`); the isometric / staggered / hexagonal orientation coordinate transforms (`Tiled.cell_x`/`Tiled.cell_y`, driving the tile draw so cells land at the correct screen coords); and Wang-set GID resolution through the standard resolver (the terrain-corner authoring concept is editor-side and ignored). - **P6 scale (#74)** — infinite/chunked maps: `` (TMX) and JSON `chunks[]` decode and flatten into the dense layer; `.world` stitching (`Tiled.world`/`Tiled.world_count`/`Tiled.world_map`) lists member maps at their offsets; and a self-contained pure-Ludic **zstd** decompressor (`z_zstd`, RFC 8878) for base64+zstd layers — frame + raw/RLE/compressed blocks, raw/RLE/direct-weight-Huffman literals, and the full FSE sequence path — decoding the low-entropy GID streams a tilemap produces (a high-entropy FSE-compressed-Huffman-weights block fails cleanly with -1 rather than emitting wrong bytes). - Builtin NPC AI (#61) — the source package **ludic.npcai**, a perception → decision → action stack that plugs into the other controllers instead of re-implementing movement. The AI never moves a body directly: it writes the SAME intent fields the player controllers read (`want_x`/`want_y`/`want_fire`, `want_jump`), so an enemy gunner reuses the shooter's weapon/projectile/auto-aim systems verbatim (set the body's `TopDown.aim_mode = 3`) and a companion reuses the mover — friendly vs enemy is faction + goal, not different code. **Perception** (`Vision` + `Memory`, throttled `esys_perception` with faction filtering and optional `Grid` line-of-sight) remembers the nearest hostile and emits `TargetSpotted`/`TargetLost`. **Decision** offers three models writing one `Brain` intent — a finite-state machine (patrol/chase/attack/flee), a utility scorer, and a canonical behaviour tree — each decision veto-able via `cancellable DecisionMade`. **Steering** adds Reynolds flocking (separate/cohere), and a `Follower` component gives companion stances. Reuses ludic.gameplay Faction (who is hostile) + Stats (hp for flee). Fully deterministic: perception + replan are frame-throttled and fixed-order. Example: `examples/games/npcai_demo.ludic` — one enemy perceives, chases and shoots a target through the shooter controller, flees at low hp under the utility model, and a companion follows its leader. - Builtin Platformer controller (#58) — the reference implementation of the six-lever extensibility contract, shipped as the source package **ludic.platformer**. Movement feel is all defaulted POD data (`Platformer { move_speed, jump_height, apex_frames, fall_gravity_mul, coyote_frames, jump_buffer_frames, air_jumps, policy, … }`); the controller is decomposed into small engine-owned sub-systems — input (Input phase), move/gravity/jump (FixedUpdate, before the shared `esys_move` sweep) and animation-state (LateUpdate, after it) — each independently switch-off-able with `disable system `. It owns movement *policy* only and reuses the engine Body/Collider swept-AABB collision. Jump feel derives gravity + impulse from height/apex, with coyote time, jump buffering, variable jump height and multi-jump; every jump decision emits a `cancellable JumpRequested` / `JumpPerformed` / `Landed` / `StateChanged` event, and a gravity `policy` enum (asymmetric / symmetric / floaty) is the formula hook. Ships the opt-in game-loop layer too (`scaffolding.ludic`): moving & crumbling platform blocks with rider carry, collectibles + `Score`, springs, hazards + a light `Life`/i-frames model, and checkpoint/goal triggers. Deterministic integer Q16.16 throughout. Examples: `examples/games/platformer_demo.ludic`, `examples/games/platformer_scaffolding.ludic`. Also fixes a latent codegen bug in `ludic_sweep_entity` (an SSA register name collided once a program declared ≥11 events). - Builtin RPG systems suite (#59) — the source package **ludic.rpg**, seven independently-usable modules on the six-lever contract, with name-keyed data registries so a game or mod adds content with zero code. **A Movement** — one `Mover` with a `mode` selector (grid / free / grid-tween) and 4/8-axis, tilemap walkability, and `cancellable MoveRequested` (locked doors/ice) / `TileEntered` (encounters) / `Interacted` (the action-button raycast). **B Inventory** — a name-keyed item registry, per-owner counts, gold, `ItemUse` veto, and `Equipment` whose bonuses flow through the gameplay Stats modifier stack. **C Crafting** — a data-driven recipe + ingredient registry; `Craft.can`/`Craft.make` consume from the inventory. **D Quests** — quests + objectives whose progress is driven by `Quest.notify` (route any gameplay signal in), auto-completing when met, plus a global flag store for branching. **E Dialog** — an Ink/Yarn-style graph registry (nodes + choices) with a per-speaker `Dialog` component and `cancellable DialogChoice` for skill-check gating. **F Puzzles** — Sokoban `Pushable` + a switch / pressure-plate / gate signal graph (logic puzzles with no code). **G Status** — over-time poison/regen effects routed through the shared Combat pipeline. Everything is integer-deterministic, so save/load (world_save) and rollback hold. Example: `examples/games/rpg_demo.ludic` (21 self-checks across all seven modules). - Builtin top-down Shooter controller (#60) — the source package **ludic.shooter**, conforming to the six-lever contract and built on the engine Body/Collider + ludic.gameplay Faction/Combat/Stats. `TopDown` decouples movement from aim (`aim_mode`: mouse / right-stick / move-direction / nearest-enemy auto-aim, with a `turn_rate` for tank-style rotation). Weapons are a name-keyed **registry** (`Weapon.def("shotgun", …)` — add a gun with zero code) with per-weapon fire-rate, damage, speed, spread, pellet count, pattern (single / spread cone / ring / spiral), plus data-driven pierce (`Weapon.set_pierce`) and homing (`Weapon.set_homing`); `esys_weapon` reads a `want_fire` intent so the **same** weapon fires for a player (input) and an NPC (AI). `Projectile` + `esys_projectile` is a self-contained deterministic pool: integrate, TTL, faction-filtered hit through `Combat.damage`, pierce, and homing that curves onto the nearest enemy — every step observable via `ProjectileSpawned` (mutable/veto) / `ProjectileHit` / `ProjectileExpired`. A budgeted `Spawner` wave director emits `SpawnRequested` / `WaveCleared`. Example: `examples/games/shooter_demo.ludic` (11 self-checks: movement, aim, faction damage, friendly-fire immunity, spread, kill, ring, homing, waves). - Canonical engine-ABI components (#77) — the shared `Position` / `Body` / `Collider` / `Solids` bundles the engine-owned movement system (`esys_move`, #65) reads by name are now shipped from a base source package, **ludic.core**, instead of being re-declared by hand in every game and example. A game `import "ludic.core/components.ludic"` and the engine moves and collides its entities for free; extend by *composition* (attach your own components on the same model). AOT means the properties compile straight into the consumer's compile-time ECS with no ABI seam, and everything stays integer + Q16.16 deterministic (lockstep / replay / `world_save` hold). Example: `examples/library/core_components.ludic`. - Cursor capture (#89) — `Input.cursor_mode(mode)` hides / locks / confines the OS mouse for a windowed game: `0` normal (visible, free), `1` hidden (hide the OS cursor while focused so a game draws its own reticle), `2` locked (hidden + dissociated — the mouse feeds *relative* motion through `Input.mouse_dx/dy`, and `Input.mouse_x/y` becomes a clamped virtual cursor, the FPS / twin-stick aim mode), `3` confined (dissociated but visible; the mouse cannot leave the window). The platform auto-releases (shows + reconnects the cursor) while the window is not key (Cmd-Tab) and on close, so the cursor is never left captured. Adds the native macOS implementation in `cocoa.ll` (`[NSCursor hide]/[unhide]`, ref-counted and toggled only on change; `CGAssociateMouseAndMouseCursorPosition`; `CGGetLastMouseDelta` for the relative virtual cursor) behind a new `win_cursor_mode` intrinsic; headless / non-windowed it is a no-op (DCE'd). Example: `examples/library/cursor_capture.ludic`. - Declarative Render clear + present (#86) — `@ClearColor(0xRRGGBB)` makes the engine own the per-frame clear and flip: at the top of the Render phase it clears the framebuffer to the declared colour, and after the Render handlers run it presents the frame, so a game's Render handler no longer repeats `Screen.clear(color)` / `Screen.show()` and the clear colour is configured *declaratively* rather than in the handler body. Opt-in and backward-compatible: a program with no `@ClearColor` is byte-for-byte identical (it clears/presents itself, or the light system owns the present). Example: `examples/library/clear_color.ludic`. - Deterministic camera zoom (#78) — `Camera.zoom(scale)` scales the whole view about the screen centre by a Q16.16 factor (`1.0` = none, `2.0` = 2x in, `0.5` = out). It rides on the same two framebuffer chokepoints (`rt_put_px` / `rt_fill_rect`) that already carry the camera offset, so it composes with `Camera.set`/`follow`/`shake`, and it is a *render-time* transform — the world coordinate types stay integer pixels + Q16.16 velocity, so lockstep, replay and `world_save` are untouched, and the zoom itself is deterministic. Gated by an internal `rt_cam_zoomed` flag so a game that never zooms renders byte-for-byte identically (golden renders unchanged); `Camera.zoom(1.0)` turns it back off. This is the concrete outcome of the #78 position-types investigation (`docs/RFC-POSITION-TYPES.md`), which rejected hardware floats for the deterministic coordinate core and identified zoom as the one genuinely-missing render feature. Example: `examples/library/camera_zoom.ludic` (pixel-readback verified). - Directional int input (#79) — `Input.axis_i(neg, pos) -> int` returns a -1/0/1 movement intent (`+1` positive key held, `-1` negative, `0` neither or both) read from the multi-key device set, so turning WASD into movement no longer needs the `ki(key_down('d')) - ki(key_down('a'))` bool-to-int glue and feeds an int mover directly: `dx = Input.axis_i('a', 'd')`, `dy = Input.axis_i('w', 's')`. Complements `Input.axis` (fixed) / `Input.vector` (normalized). Example: `examples/library/input_movement.ludic`. - Engine sprite-render system (#85) — the engine already auto-*ticks* SpriteAnim and Motion; it now auto-*draws* too. Declare a `Sprite` component (id + optional offx/offy/scale/flip/tint/hidden, shipped from **ludic.core**) on an entity with a `Position` and the engine draws it each Render frame — no hand-written Render handler querying positions and calling `draw_sprite` per entity, and no hand animation (when the entity also carries `SpriteAnim`, the current frame is added to the base id). Registered on the compile-time engine-system registry for the Render phase and spliced only when a game declares `Sprite`, so a game that never declares it compiles byte-identically; a game wanting a custom draw omits `Sprite` (or `disable system esys_sprite`). Also **deprecates the bare `draw_sprite` / `draw_sprite_scaled` globals** in favour of the namespaced `Screen.sprite` / `Screen.sprite_scaled`: a direct bare call now emits a one-time compile-time deprecation note (the bare form still lowers, since `Screen.sprite` uses it), and the in-repo `chronorift` demo is migrated to the namespaced calls. Example: `examples/library/sprite_render.ludic`. - Entity-pool stats (#80). Ludic's ECS is already pool-based: the allocator recycles freed entity slots through a freelist (a `despawn`ed slot is reused by the next `spawn` before any new slot is taken), and component storage is fixed per-entity arrays — so spawning and despawning many entities per frame (bullet-hell / horde) does **no per-spawn heap allocation** and cannot fragment. Exposes that with a `Pool.*` namespace so a game can watch the reuse and budget against the cap: `Pool.live()` (entities alive now), `Pool.free()` (freed slots waiting to be reused), `Pool.reserved()` (high-water — slots ever allocated; stays flat across a steady spawn/despawn loop, the proof that slots are pooled not reallocated), and `Pool.capacity()` (the fixed entity cap). Zero-cost — they read the existing allocator counters inline. Example: `examples/library/pool.ludic`. - Gameplay-controller foundation (#57) — the shared, cross-genre building blocks the builtin controllers stand on, shipped as the source package **ludic.gameplay**: a deterministic `Cooldown` frame timer (engine-ticked), a `Stats` attribute bundle with an unbounded timed **modifier stack** (`Stats.total` computes base+flat then percent on demand; expired modifiers self-despawn), a `Faction` friend/enemy/neutral relationship table (same-id-friendly / different-hostile by default), and a `Combat` damage pipeline whose `cancellable DamageAboutToApply` hook lets a game veto a hit *or rewrite the amount* (`Combat.set_amount`) and which emits `Damaged`/`Died`/`Healed`. Everything is integer-only so lockstep, replay and `world_save` snapshots hold. Also adds extensibility **lever 5** to the language: `disable system ` drops exactly one engine-owned system's tick at compile time, so a game can carry a well-known component but tick it with its own handler (byte-identical when nothing is disabled; the C-free bootstrap fixpoint is untouched). Example: `examples/library/gameplay_foundation.ludic`. - Incremental asset preloading (#82). Assets used to load synchronously inside Boot/Start (`png_load`, `Audio.load`), stalling the first frame(s) as content grows, with no built-in loading phase. Adds an `Assets.*` preload queue: `Assets.enqueue(name, path)` queues a named image without loading it, `Assets.pump(max)` loads up to `max` queued assets per frame (returning how many it loaded), and `Assets.total` / `loaded` / `ready` / `progress` (0..100) drive a progress bar. A loading scene pumps a few assets per frame, draws `Assets.progress()`, and `become`s the play scene once `Assets.ready()`, so the game shows a responsive loading screen and only enters play once content is ready — the deterministic, no-threads form of async preloading (the work is spread across frames instead of stalling one, and the same enqueue+pump order loads identically every run). Loaded assets are reachable by name via `Assets.get` / `Sprite.named`. Builds on the #81 atlas. Example: `examples/library/preload.ludic`. - Input Manager + automatic device-layer drive (#83). The generated frame loop now commits the input device layer itself — when a game uses any Input action-map / device method it calls `input_poll` each frame (reading the live key, recording/replaying, and rebuilding the held-key/mouse/gamepad state), so `Input.active` / `Input.key_down` / the mouse and pads read live **without the game calling `Input.poll` by hand** (previously the loop fed only `Input.key`, and the device layer read empty unless the game polled at the top of its Input phase). A game that uses no Input runtime keeps the plain `rt_poll` path, byte-identical. Adds the Input-Manager API on top: `Input.action(name, key)` ships a **default** binding (kept if already bound, so a player's `Input.rebind` or a loaded key-map is not clobbered); `Input.bind_pad(name, button)` makes an action **device-agnostic** (fires from keyboard *or* gamepad); and `Input.active` / `Input.just_pressed` / `Input.just_released` read the whole multi-key device layer with clean on-press / on-release edges (the deterministic, dispatch-free equivalent of event handlers — a handler polls the edge and reacts, so a replay fires identically). Examples: `examples/library/input_manager.ludic`, `examples/library/input_auto.ludic`. - Namespace block form (#76) — `namespace Name { export function foo(…) … internal function bar(…) … }` declares a `Name.*` namespace once and controls its public surface declaratively, instead of annotating every function with `@Namespace(Name)` one at a time. Inside the block each `function short(…)` is emitted as `namelower_short`; an `export` function (the default) is callable as `Name.short(…)`, while an `internal` function is a private helper — emitted and callable by its short name from siblings in the block (calls are rewritten to the emitted name), but not part of the `Name.*` surface (`Name.internalOne()` is a compile error). It is the block sugar for the per-function `@Namespace` annotation, so a package's public API reads at a glance. A namespace declared the old per-function way is unchanged. Example: `examples/library/namespace_block.ludic`. - Namespaced spritesheet / atlas API (#81). Sprite loading was a bare `png_load("floor0.png")` — one file per 16x16 sprite, with no way to load one sheet and address a cell by grid coords or name. Adds a `Sprite.*` / `Assets.*` runtime (over the variable-size image loader, so a cell is a sub-rect of the kept image and is **not** restricted to the 16x16 sprite table): `Sprite.sheet(path, cellw, cellh)` -> handle, `Sprite.cell(sheet, col, row)` and `Sprite.cell_span(sheet, col, row, cols, rows)` -> id (a sprite may span more than one cell — a tall character, a wide object), `Sprite.define(name, …)` / `Sprite.named(name)` to name and look up a cell, `Sprite.draw` / `Sprite.draw_scaled` (through the camera / zoom / clip, like `Screen.sprite`), `Sprite.width` / `height`, and `Assets.image(path)` / `Assets.load` / `Assets.get(name)`. Spliced on demand; a program that uses neither compiles byte-identically. Example: `examples/library/atlas.ludic`. - Optional world boundaries (#84) — declare a single `Bounds` config entity (a rect `x, y, w, h` plus a policy, shipped from **ludic.core**) and the engine-owned world-bounds system keeps every moving `Body` inside the play area each frame, so a game no longer hand-clamps `Position`. Four policies: `0` clamp (walls), `1` wrap (toroidal), `2` bounce (clamp + flip the Body velocity on the axis that hit), `3` kill (despawn a body fully outside). It reads each body's `Collider` size so the whole box stays inside; off by default (no `Bounds` entity = open world). Registered on the engine-system registry for LateUpdate (after movement integrates) and spliced only when a game declares `Bounds`, so a game that never does compiles byte-identically. Also adds `World.despawn(entity)` — the reflective, by-id form of the `despawn` statement (runs `@OnDespawn` + frees the slot), which the kill policy uses and any system can call. Example: `examples/library/world_bounds.ludic`. - Package manager (#63) — `x add` / `x get` / `x update` / `x verify` / `x vendor` bring third-party packages to Ludic with no new infrastructure. Dependencies are named by their git import path (URL-as-identity, no registry — a `git tag vX.Y.Z` is publishing), resolved by Go-style Minimum Version Selection, fetched into a content-addressed global store (`~/.ludic/store`, keyed by a file-content hash) and linked into each project under `ludic_modules/`. A `package.ludic` manifest declares dependencies, the provided `Foo.*` namespace(s), the kind (source or prebuilt) and, for prebuilt libs, the shipped targets; `package.lock.ludic` pins the resolved versions and content hashes for reproducible, verifiable builds. A source package's Ludic compiles into the consumer via a new module-root import fallback in the compiler (`import "git.workshopsoft.io/user/pkg/foo.ludic"` resolves against `$LUDIC_MODULES`, default `ludic_modules/`), so a package registers a namespace the same way the built-in stdlib does. Namespace collisions and missing prebuilt targets are hard errors. Existing programs compile byte-for-byte identically; the C-free bootstrap fixpoint is untouched. See docs/PACKAGES.md. - Package-declarable namespaces & engine systems (#62) — the two hooks that made `Foo.*` stdlib namespaces and engine-owned systems compiler-hardcoded are now data-driven registries, so a package registers them with no compiler edit. `@Namespace(Name)` on a function opens a `Name.method(…)` namespace that dispatches to the bare `name_method` through the same generic path the built-in namespaces use (applied only after them, so it never shadows a core one). `@EngineSystem(Component, Phase)` registers an engine-owned system the frame loop runs each phase when the component is present — the package-declarable form of the built-in SpriteAnim/Motion/Light2D systems, reading components by name through the reflection ABI so an unused registration is byte-identical. Both annotations are keyword-free. The core stdlib keeps its optimized codegen (byte-identical output; the C-free bootstrap fixpoint is untouched) and packages ride the generic registry alongside it. This completes the packaging prerequisite for shipping gameplay-controller libraries (#58–#61) as real packages. See docs/PACKAGES.md. - Prebuilt binary packages (#64) — a package can now ship a compiled artifact whose exported functions, systems and components a consumer uses without the source, over the stable reflection C-ABI. `x build-lib ` compiles a package's module to a per-target native dylib (`lib//`); a `kind prebuilt` dependency is fetched and linked like any other, and `x link-flags` prints the clang flags to link the module dylibs into a game (or `x app` does it in-repo). A module registers its dynamic components (`world_register_prop`) and its `@System(Phase)` functions with the host at load through a constructor, and the host dispatches every registered system each frame — the systems analogue of dynamic components. Binary packages are native-only and second-class ECS by design (source packages remain the portable, first-class, deterministic path); missing a build target is a hard error. See docs/PACKAGES.md. - The engine runtime ships with the toolchain, not the project (#75). The compiler auto-splices `runtime/native/*` for any ECS game; a `runtime/...` import that is not found relative to the build is now resolved from the install root **`$LUDIC_HOME`** (default: the compiler binary's directory — where the platform `.ll` files already come from) *before* the package module root. So an external game that consumes the `ludic.*` packages no longer has to copy or symlink the engine runtime into its `ludic_modules/`; that directory holds only third-party packages, and the runtime is part of the toolchain install. In-repo builds are byte-identical (the runtime still resolves locally there, so the `$LUDIC_HOME` fallback never fires and the C-free bootstrap fixpoint is untouched). ### Fixes - **Correct the element type of a slice indexed by a member-access expression.** `emit_index_addr` set the global `g_addr_ty` to the slice's element type *before* evaluating the index expression, so an index that was itself a struct-field access (`slice[obj.field]`) overwrote it — the load then came back typed as the field, and a following field access failed with "member access on non-aggregate". The slice branch now sets `g_addr_ty` last, matching the raw-pointer branches. Also de-duplicate the `@strcmp` declaration (centralised in the head prelude) so a program that pulls in both the world table and the filesystem prelude links. - Input.key_pressed / key_released edges now fire (#87). In a frame-loop game the edges never triggered: the loop (since #83) commits the device layer once per frame via `input_drive`, but a game that *also* called `Input.poll` by hand committed a second time in the same frame, and `input_device_commit` copies `in_held` into `in_prev` at the top of every commit — so the second commit left `in_prev == in_held` and `key_pressed` (`held && !prev`) / `key_released` could never see a transition. Fixed with an `in_have_frame_driver` flag: the loop's `input_drive` sets it, and a manual `Input.poll` under the loop becomes a no-op that returns the frame's key instead of re-committing. An entry-driven harness has no loop, so the flag stays false and each `Input.poll` still commits a frame of input as before (record/replay and the #50 device tests are unchanged). Example: `examples/library/input_edge.ludic` (press edge on the down frame, release edge on the up frame). - Windowed games no longer force-quit on Esc or 'q' (#88). The macOS platform layer (`runtime/native/cocoa.ll` `win_poll`) used to hard-code Escape (keycode 53) and the character 'q' as *quit* — storing `W_running = 0` so a shipped windowed game died the instant a player pressed Esc (a universal pause key) or typed 'q'. Those dev-loop conveniences are removed for windowed builds: **Escape is delivered to the game as key 27** and **'q' is an ordinary key**, consistently across both the single per-frame key (`@W_key`) and the `#50` held-key set (`ev_keyval` now maps Escape→27, not 'q'). A windowed game now owns Esc/pause and shuts down via `quit()` or the window close button (which still ends the run). The headless test driver (`rt_poll` in `core.ludic`) keeps its own `'q'` = quit for scripted golden runs, so nothing headless changes. Also stops forwarding consumed key events to `-sendEvent:`, which was triggering AppKit's system "funk" beep on every keystroke. ## v0.2.0 — 2026-09-01 The types-and-systems release. Ludic grows a real type system — sum types, `option`/`result`, exact and arbitrarily-big numbers, string-keyed containers and packed 2D value types — alongside an engine that auto-runs animation, motion and lighting over components a game merely declares, a full input stack from rebindable action maps to native gamepads, out-of-band Audio / HTTP / Jobs standard libraries, and a built-in test framework with line coverage. Every addition is gated and additive: a program that never touches a feature compiles byte-for-byte identically, and the C-free bootstrap fixpoint is untouched. ### Language & types - **feat**: Tagged-union enums (#56) — an `enum` variant may now carry a payload (`enum Tile { Empty, Wall, Door(int), Portal(int, int) }`), making it a sum type. Variants construct by name and `match` destructures them, binding each payload, with exhaustiveness and arity checked so adding a variant surfaces every site to update. Plain enums keep their zero-cost ordinal representation, byte-for-byte unchanged. - **feat**: `result` + `try`/`else` (#46) — a fallible function returns `ok(payload)` or `err(message)`, and `try EXPR else { … }` recovers a value with the failure message bound to `error`. A plain branch on the tag: no exceptions, no stack unwinding. `is_ok`/`is_err` classify without unwrapping. - **feat**: `option` (#53) — `some(v)` / `none()`, a maybe-a-value with no magic `-1` sentinel, read with `is_some`/`is_none`/`unwrap_or`. - **feat**: `panic(msg)` + `assert(cond, msg)` (#8) — a clear, located `file:line: panic:` / `assertion failed:` message and a clean exit 1 instead of a raw segfault; the source location is baked in at compile time. - **feat**: `IVec2` + `Rect` 2D value types (#1) — by-value spatial types that lower to packed integers, so they copy like scalars and never allocate: an integer 2D vector for grid coordinates and a Q16.16 axis-aligned rectangle for HUD boxes and hitboxes, both exact and platform-identical. - **feat**: `BigInt` + `Decimal` exact numbers (#52) — arbitrary-precision integers and exact base-10 fixed-point for game economies, so an idle counter never overflows and `0.10 + 0.20` is exactly `0.30`. No `f32`/`f64`; deterministic. - **feat**: `Huge` + `Angle` + `Percent` (#55) — a display-scale idle/incremental big number (`1.23e45`), an auto-wrapping radian angle over deterministic `Math.*` trig, and a `[0,1]`-clamped fraction for health, volume and interpolation `t`. - **feat**: `Dict` + `Set` containers (#54) — string-keyed lookups over one open-addressing hash table (FNV-1a, linear probing, tombstones), for resource counts, registries, tags and visited tiles; O(1) average instead of a linear scan. - **feat**: Value tree + reflection + JSON (#44) — a self-describing `Value` node, `Reflect.serialize`/`apply` to walk an entity's whole component set to and from it bit-exactly, and `Json.encode`/`parse` for compact, stable, diffable text. One-call save/load for entities and the backbone of data-driven tooling. ### Engine, animation & lighting - **feat**: Engine-owned systems (#43) — the ECS hook that auto-runs a system each frame over a component a game merely declares, no `handler` wired: `SpriteAnim` advances sprite-sheet frames and `Motion` advances value tweens for free. Built on the reflection ABI, so it costs nothing in a game that declares neither. - **feat**: Animation ergonomics (#48) — an ergonomic layer over those systems: `Anim.clip`/`Anim.play` for named spritesheet clips, `Anim.on_frame`/`fired` frame events, `Motion.to` one-call tweens, and fluent engine-advanced `Tween` handles (`to`/`chain`/`delay`/`parallel`/`stop`). All integer and deterministic under replay. - **feat**: `Light.*` 2D lighting (#4) — a deterministic software light-accumulation pass over the framebuffer: `ambient` tinting, additive radial `point` lights with linear falloff, and hard shadows cast against rectangular occluders. Integer + Q16.16, identical every run and headless. - **feat**: ECS-native lighting (#47) — a torch is now just an entity carrying `Light2D`, a wall an `Occluder`, and one `Ambient` sets the night tint; the engine runs the whole light pass at the end of the Render phase with no `Light.*` calls wired by hand. - **feat**: Lighting render-quality tiers (#49) — cone/flashlight `spot` lights, a `falloff` exponent, `soft` penumbra shadows, colour `gel` cookies, normal-mapped surfaces (N·L) and a `time_of_day` day/night ramp, all on the same deterministic accumulation core and consumable via optional `Light2D` fields. ### Input - **feat**: Action maps + record/replay (#7) — gameplay reads named, rebindable actions instead of physical keys, and the single per-frame `Input.poll` makes deterministic replay fall out for free: `Input.record` captures the tape and `Input.replay` feeds it back exactly — the seed of lockstep netcode. - **feat**: Device layer (#50) — multiple simultaneous held keys, analog axes and a normalized vector, the mouse (position/delta/buttons/wheel), gamepads and touch, all injectable on every target (`Input.press`/`set_mouse`/`set_pad`/`set_touch`) and snapshotted whole into the replay tape. - **feat**: Native hardware bindings (#51) — the device layer's macOS side wired into cocoa.ll: live cursor position, per-frame GameController polling into gamepad buttons/axes (SDL button order), and NSTouch routing, all DCE'd out of a headless build. No API changes. ### Standard library - **feat**: `Audio.*` (#22) — sound effects and music over a new AVAudioPlayer backend: `load`/`play`/`play_music`/`stop`/`volume`/`pitch`/`is_playing`. Out-of-band (real-time, not part of the simulation) but frame-driven, so a replay fires the same sounds at the same frames; headless builds carry it as dead-stripped no-ops. - **feat**: `Http.*` (#6) — a poll-based HTTP/HTTPS client for out-of-band data (leaderboards, cloud saves, remote config) that never blocks the frame, over an NSURLConnection transport with system TLS on by default. Pairs with `Json.parse`; the response parser is pure Ludic and tested offline. - **feat**: Jobs, Promises & opt-in Sync (#14) — a layered concurrency library. `Job.*`/`Promise.*` are the safe default: cooperative futures pumped a little each frame so heavy work spreads out, combined with `Promise.all`/`race`. The advanced `Sync.*` tier adds mutexes, atomics and bounded channels. A deterministic cooperative scheduler — results are collected on the main thread and a Job never touches the world directly, so lockstep and replays stay bit-exact. ### Testing & tooling - **feat**: Built-in test framework (#12) — a `test "name" { … }` block auto-discovered and run by a synthetic runner (no `entry` to write), with `expect`/`expect_eq`/`expect_near` assertions that report every failure and exit non-zero. `expect_near` carries the tolerance fixed-point game math needs. - **feat**: Line coverage (#45) — compile with `--coverage` and the compiler instruments each statement with a per-line hit counter dumped at exit; `bin/x test --coverage` aggregates the dumps into a per-file report naming the unreached lines. Flag-gated and additive — an ordinary build stays byte-identical. ### Fixes - **fix**: `const` of a non-int type is no longer miscompiled. A `const` reference lowered to its initializer's raw integer bits typed as `int`, so `const X: fixed = 10.0` computed as the raw Q16.16 value `655360` instead of `10.0` — silently corrupting fixed-point math (and, in one case, spinning an infinite loop). Const references now emit their initializer with its real type. Every existing const is an `int` literal, so the lowering there is byte-identical and the bootstrap fixpoint and golden renders are unchanged. ## v0.1.0 — 2026-08-30 ### Features - Editor tooling — `ludic-fmt` (formatter) and `ludic-lsp` (language server), plus VS Code and JetBrains integrations, all built by the toolchain. - Namespaced standard library — Math, Text, List, Random, Time, Screen, Color, Ease, Collide, Memory, Vector, DateTime/Date/Duration/Clock, Unicode, Os, Fs/Path/Mime, Log, Noise, Hash, Crypto and Uuid, each deterministic where a game needs it. - Native 2D backend — an ECS core with a deterministic fixed-point (Q16.16) runtime, a windowed Cocoa target on macOS and a headless PPM renderer that runs anywhere. - Self-hosted, C-free toolchain — the compiler, runtime, task runner and editor tools are all written in Ludic and built from a checked-in LLVM-IR seed with clang alone; `x bootstrap-cfree` proves the compiler rebuilds itself byte-for-byte. - Versioning and releases — SemVer with `ludicc --version`, a changeset-driven `CHANGELOG.md`, and `x release` to bump, tag, and publish a Forgejo release with source and toolchain artifacts. ### CI - Continuous integration — Forgejo Actions workflows build the toolchain from the seed, run the regression + editor suites, assert the C-free bootstrap fixpoint, and lint commit messages on every push and pull request.