Commit graph

199 commits

Author SHA1 Message Date
4aaf27c669 chore(release): v0.5.2
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m55s
commit-lint / conventional-commits (push) Successful in 2s
release / publish (push) Successful in 2m44s
2026-09-05 22:33:35 +03:00
bfa763a55b fix(install): cover a non-login interactive bash
~/.bashrc was only appended to when it already existed, so on an account without
one — a fresh container, a minimal image — `bash -i`, which is what most Linux
terminal emulators start, did not see the toolchain even though every other
shell did.

.bashrc is now created when missing. Unlike .bash_profile, whose existence stops
bash reading ~/.profile, a .bashrc that only sources the env file changes nothing
else about how bash starts.

Verified across zsh -i, zsh -c, bash -l, bash -i, sh -l and dash -l on a staged
HOME: all six resolve ludic, a second run writes nothing, and .bash_profile is
still never created.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:33:35 +03:00
a916fa5118 chore(release): v0.5.1
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m54s
commit-lint / conventional-commits (push) Successful in 1s
docs / build-and-deploy (push) Successful in 33s
release / publish (push) Successful in 2m43s
2026-09-05 22:22:40 +03:00
44e752b606 fix(install): put ludic on PATH in every shell, not just $SHELL's
The installer edited one profile — whichever ~/.zshrc or ~/.bashrc $SHELL
pointed at — and skipped any profile that did not already exist. So a fresh
account got nothing written at all, a bash user's ~/.bashrc is not read by the
login shell macOS Terminal starts, and ~/.zshrc is only read by interactive zsh.
The toolchain installed correctly and `ludic` was still not a command.

The PATH edit now lives in one file, <install>/env (plus env.fish), and each
profile gets a single line that sources it: ~/.profile for sh and for login bash
with no .bash_profile, ~/.zshenv because zsh never reads ~/.profile and reads
this one for every invocation, ~/.bashrc and ~/.bash_profile when they already
exist, and fish's config when fish is installed. Missing .profile/.zshenv are
created; .bash_profile deliberately is not, since creating it would stop bash
from reading ~/.profile at all.

Sourcing a shared file rather than appending an export keeps a re-install from
accumulating a second entry, and leaves one place to delete when uninstalling.
Verified with a staged HOME: zsh -i, zsh -c, bash -l, bash -i and sh -l all
resolve ludic; a second run reports "already on your PATH" and writes nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:20:39 +03:00
29fcef3b9c fix(cli): report the installed version, not the current directory's
`ludic version` looked for bin/ludicc and VERSION relative to the working
directory. In the toolchain repo that is right by accident; from a project — the
only place a user runs it — there is no ./bin, so a perfectly good install
answered "(version unknown)". It now resolves the compiler through
ludic_home(), like every other command.

The regression test asked for the version from the repo root, so it passed for
the same accidental reason the bug hid behind; it now asks from the staged
project, where the answer can only come from the install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:16:16 +03:00
2caf74946f chore(release): v0.5.0
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m53s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Successful in 33s
release / publish (push) Successful in 2m50s
2026-09-05 22:02:55 +03:00
aca263642d feat(cli): install in one command, and call the CLI ludic
Getting started meant cloning the repository, bootstrapping a compiler and
learning a task runner called `x`. That is a contributor's workflow handed to
everyone who wants to try the language.

Installing is now one command:

    curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh

install.sh puts a complete toolchain — compiler, CLI, engine runtime, bundled
ludic.* packages, formatter, language server — in ~/.ludic and adds it to PATH.
Prebuilt artifacts are checksum-verified; where a platform has none, or the
release predates this layout, it bootstraps from the compiler's own IR seed with
clang. The docs site publishes the script beside the pages that quote it, so the
page and the script can never come from different releases.

`x` becomes `ludic`, and the surface splits by audience. A user of the language
sees `new`, `run`, `build`, `test`, `add`, `fmt`, `lsp`, `doctor`, `upgrade`;
`ludic new` scaffolds a project that builds and plays as it stands. Everything
the toolchain repo needs moved under `ludic dev` — build, test, reseed,
bootstrap-cfree, docs-gen, release — unchanged apart from the namespace. Those
tasks read arguments one position further along, so dispatch_dev sets a shift
and commands use arg_n()/arg_total() rather than each knowing its own depth.

Release artifacts become complete install roots (bin/ beside runtime/, packages/
and VERSION) rather than bare binaries, which is what the installer unpacks.
`ludic dev test` asserts the whole shape: it stages an install, puts it on PATH
with no LUDIC_HOME, and runs new -> build -> test through it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:01:52 +03:00
005cc39394 feat(compiler): resolve the install root from the binary's location
The engine runtime and the bundled ludic.* packages belong to the toolchain,
not to the project, so the compiler has to know where the toolchain lives. It
derived that from the directory the binary sits in, which is `bin` — so an
in-repo build only found runtime/native/cocoa.ll when the caller set
LUDIC_HOME=., and an installed compiler had no way to find it at all.

ludic_home() derives it properly instead: $LUDIC_HOME when set, else the
binary's directory with a trailing `bin/` stripped, else a $PATH scan for
argv[0] (an install is invoked by bare name, which carries no directory). Both
layouts that exist then have the same shape — ~/.ludic/{bin,runtime,packages}
and a repo checkout — so the same rule serves both and LUDIC_HOME becomes an
override rather than a requirement.

`import "ludic.core/…"` also falls back to $LUDIC_HOME/packages, after the
project's own ludic_modules/, so a project that has not fetched its own copy
gets the packages that shipped with the toolchain instead of a symlink farm.

The `ludic` multi-call name is dropped from the compiler: that name now belongs
to the CLI, and --run is the flag it drives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:01:39 +03:00
7e07573026 fix(docs): restore the token cards on the generated site
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 2m51s
commit-lint / conventional-commits (push) Successful in 2s
docs / build-and-deploy (push) Successful in 32s
Clicking a keyword, type, builtin or namespace method in a code sample is
supposed to open a summary card for it. The script that builds those cards
never went away; its stylesheet did.

The site redesign split item.css into base.css and docs.css and filed the card
chrome — .hovercard, .hc-*, .tok, .kind-badge — under docs.css. Only the four
reference page kinds link that file. The landing page links base.css and
site.css, so a click there appended an unstyled, position:static div to the end
of the document: built, filled with the right text, and invisible.

The card chrome now sits in base.css beside the .t-* token colours it belongs
with, which every page links. It is also position:fixed rather than absolute,
matching the viewport coordinates positionCard() reads out of
getBoundingClientRect() — absolute put the card an entire scroll offset away
from its token on any reference page read past the fold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 14:41:10 +03:00
c9ee303b69 docs: rewrite the README for someone meeting the language
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m51s
commit-lint / conventional-commits (push) Successful in 1s
The README opened with three restatements of "no C", then a limitations table,
then a repo-layout map, and closed with Chrono Rift's keybindings — P2/Mage
`I`/`K` select, `J` confirm — which is a game manual, not a language README. It
never showed the language itself.

It now leads with what Ludic is, a compiling code sample, how to build, what the
language offers, and an honest status. The layout table is contributor material
and CONTRIBUTING already covers that ground.

Two things were not merely stylistic:

- Nine links pointed at wiki pages that no longer exist.
- "Language at a glance" advertised `system` with `reads`/`writes`, plus
  `requires`/`ensures`. None of those are keywords — the grammar has `handler`,
  and `System.*` is a namespace. That bullet described a vocabulary retired
  several releases ago.

The sample is verified to compile, every internal link resolves, and every
command named is one `x help` actually offers.

Also makes commit-lint survive a force-push: it linted `event.before..sha`
without checking that `before` still resolves, so rewriting or gc'ing that
commit failed the job with "Invalid revision range" on a push whose messages
were all valid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 14:25:07 +03:00
80da7e6275 fix(release): per-artifact checksums so a release can span hosts
Some checks failed
bootstrap / cfree-fixpoint (push) Successful in 24s
ci / build-and-test (push) Successful in 2m54s
commit-lint / conventional-commits (push) Failing after 1s
build_artifacts wrote a single dist/SHA256SUMS covering whatever that host
happened to build. But a release is assembled from more than one machine — the
Linux runner cannot produce the darwin-arm64 toolchain — and
forgejo_upload_assets deliberately skips an asset whose name is already
attached. So the first host to publish wrote SHA256SUMS, and every artifact
added later was silently left uncovered by it.

Emit one <artifact>.sha256 per tarball instead. The names are unique, so each
host's contribution stands on its own and nothing goes stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 14:14:00 +03:00
f81ca5c3c1 ci(docs): serialise the pages deploy
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 25s
ci / build-and-test (push) Successful in 2m54s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 32s
Publishing the site force-pushes an orphan `pages` branch. Two runs racing can
therefore land out of order and leave `pages` holding the older build, with both
runs green and nothing to indicate the site went backwards.

A `pages-deploy` concurrency group with cancel-in-progress makes a newer push
cancel an older in-flight build rather than queue behind it, so the last push to
land is the one that ends up published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 03:37:33 +03:00
266e8cdd86 docs(ci): document the runner network a self-hosted CI needs
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 25s
ci / build-and-test (push) Successful in 2m54s
commit-lint / conventional-commits (push) Successful in 5s
Every workflow's first step clones ${{ github.server_url }}, which on a
self-hosted Forgejo instance is an internal address like http://forgejo:3000.
The runner's default is to put each job on a freshly created per-job network
that the Forgejo container is not attached to, so the clone dies with

    fatal: unable to access 'http://forgejo:3000/…': Could not resolve host

Nothing in the repo said so, and the failure is intermittent: Docker forwards
names it cannot resolve to the host's resolver, which answered for the container
name often enough that CI passed for weeks before stopping.

Documents the fix (pin job containers to a network Forgejo is also on, using a
dedicated one rather than the general application network so a CI job cannot
reach unrelated services) and how to verify it without running a workflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 02:57:09 +03:00
0a3df45d20 chore(release): v0.4.0
Some checks failed
bootstrap / cfree-fixpoint (push) Failing after 18s
ci / build-and-test (push) Failing after 8s
commit-lint / conventional-commits (push) Failing after 1s
docs / build-and-deploy (push) Failing after 7s
release / publish (push) Successful in 2m43s
2026-09-05 01:52:18 +03:00
60a1547124 build(x): stop printing a clang warning on every build
Each clang invocation the task runner makes emitted

    warning: overriding the module target triple with arm64-apple-macosx15.5.0

four times per `x build`, with nothing for anyone to act on. `cc()` now passes
-Wno-override-module, the same flag ludicc already passes for its own link.

The comment in selfhost/main.ludic explaining that flag had it backwards — it
claimed "our IR carries an explicit target triple", when the emitted IR names
no triple at all, which is precisely why clang substitutes the host's and
warns. Corrected. No IR changes, so the seed is untouched.

Also adds .claude/launch.json entry for previewing the generated docs locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:48:09 +03:00
4b81b55849 fix(docs): make the documented bootstrap work on a fresh clone
bin/ is gitignored and not checked in, so the first command in README.md,
COMPILING.md, CONTRIBUTING.md, tools/x/main.ludic and on the site —

    clang selfhost/ludicc.seed.ll -o bin/ludicc && ...

failed on a clean checkout with `ld: open() failed, errno=2 for 'bin/ludicc'`.
Verified against a fresh clone. Every copy now leads with `mkdir -p bin`, which
is what the CI workflows had been doing all along.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:48:01 +03:00
d41de1f7c9 ci(release): publish releases from a tag, not from a laptop
There was no release workflow. Artifacts were built by `x release --publish` on
whatever machine the maintainer was sitting at, from whatever happened to be in
bin/, with no checksums and nothing proving the tagged tree passed its tests.

Pushing a v* tag now publishes. The workflow 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 when CHANGELOG.md has no section for that version.

`x publish [vX.Y.Z]` is the command behind it and runs locally too. It builds
dist/ — a source tarball from the tag, this host's toolchain, and a SHA256SUMS
covering both — and takes the release notes from that version's CHANGELOG
section, so notes and changelog cannot drift. It only adds assets the release
is missing, which is how a macOS build gets attached to a Linux-built release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:48:01 +03:00
5c4c10a1d7 fix(release): keep a changeset's markdown in the changelog
build_section piped every changeset body through `tr '\n' ' '`. A multi-line
changeset came out as one paragraph, so nested bullets rendered as inline
" - " runs and a whole release read as a single unbroken block — v0.3.0 was one
~4 KB bullet.

The renderer is now Ludic rather than a shell one-liner. A section is grouped
by conventional-commit type (Features, Fixes, Performance, ...), each changeset
is one bullet, and continuation lines are indented two spaces so nested lists
and paragraphs stay inside their item. Bullets are sorted within a group, so
cutting the same release twice produces the same text.

Also:

- `x release --dry-run` renders the pending section to stdout and touches
  nothing, so a release can be read before it is cut.
- `x changelog-render` re-renders a section from a directory of changesets, and
  `x changelog-section` prints one release's section back out of CHANGELOG.md.
- The v0.1.0 and v0.3.0 sections are re-rendered with the former, from the
  changesets recovered at each tag's parent commit; the bullet counts (6 and
  25) and word multisets are unchanged. v0.2.0 is left alone: it carries a
  hand-written summary and topical subheadings, and regenerating it would have
  replaced curation with raw changeset dumps. The file header now says that
  a section may carry such a summary, since it previously claimed released
  sections are never hand-edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:47:49 +03:00
2f3833a295 refactor(docs): rebuild the site around reading, not launching
The generated site had the shape of a product launch page: a near-black navy
ground with mint/coral radial glows, a gradient-clipped headline, a glowing
pill badge, nine emoji feature cards and scroll-reveal animations. None of it
told a reader anything about the language.

It is now typographic and light-first — a warm paper ground, a serif display
face, one ink-blue accent used only where it means something, and rules
instead of floating cards. Colour is reserved for code. Dark mode is the same
design with the ground inverted, defined once as tokens under a single
prefers-color-scheme block.

Structurally:

- base.css holds the tokens and shared chrome; site.css and docs.css hold what
  is specific to the landing page and the reference pages. They ship as linked
  files rather than being inlined into all 900+ pages, which takes the site
  from 16 MB to 5 MB and means a design change no longer needs a regenerate to
  be seen.
- api.css was dead — the generator never referenced it, rendering the API index
  with item.css — and is gone. docs.css replaces item.css and covers all four
  reference page kinds.
- Grids draw their separators as cell borders instead of bleeding a ruled
  background through gaps, so a final row with fewer cards than columns stops
  cleanly instead of leaving a grey hole. The feature card count is not a
  multiple of the column count at any breakpoint.
- Inline code loses its tinted chip; in a language reference, a box behind
  every keyword turns a paragraph into confetti.
- Fonts are the platform's own, so the site makes no webfont request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:47:36 +03:00
583783449a docs: correct stale references across LANGUAGE, README, COMPILING, CONTRIBUTING
- LANGUAGE.md: real diagnostic format, list literals, Font.load / Ui.*
  (no `reg`/`set_reg`, no `/Handler/Library` typo), `extern function`,
  existing example links, Input.key(); builtins table trimmed to what exists
- COMPILING.md: pipeline names selfhost/ (not compiler/*.c), `program`
  instead of game/module, all thirteen win_* entry points by group
- README.md: the window seam is not "five" functions
- CONTRIBUTING.md: docs are checked with `x docs-gen` / `x docs-check`
- docs/language: kw-ui and fn-ui_build use the namespaced API;
  @ClearColor documents constant expressions; examples/README lists
  operators.ludic

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:26 +03:00
bf36bc8a8f refactor(runtime,packages,examples): named constants, package enums, idiom sweep
- runtime: HEADLESS_FRAME_PATH, STICK_DEADZONE / STICK_LEFT_X/Y, key and
  byte codes as char literals throughout (`k == 'w'`, `fill(rt_map, ' ', …)`)
- ludic.gameplay/stats: drop the duplicate `stat_field` (it answered "atk"
  for every build stat); Stats.base uses stats_field_name
- ludic.shooter: compare aim modes and fire patterns with AimMode.* and
  WeaponPattern.* instead of raw ints; STICK_RIGHT_X/Y
- ludic.npcai: DecisionMade / brain_set_state use AiState.*
- examples/games/menu.ludic uses Font.load / Ui.* with FONT_PATH and
  BACKDROP named; strings.ludic header says what it prints
- whole tree: `x = x + 1` → `x += 1` (single-term right-hand sides only),
  `0 - x` → `-x`, ASCII codes → char literals; every .ludic and every
  ```ludic fence reformatted with the fixed formatter (whitespace only)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:26 +03:00
e3e1bc7784 fix(tools): formatter bracket rules, LSP compiler diagnostics, vocabulary sync
- ludic-fmt: `rows[i]`, `new []int`, `s[a..b]`, `emit(`, `~x`, list-literal
  and query-tag braces stay tight; member calls hug their paren
  (`Date.new(`, `Prefab.spawn(`); `<< >> & | ^ ~` are operators and
  `&& ||` are not — mirrored in ludic_syntax.h, LudicLexer.kt and the
  TextMate grammar. 149 of 274 tracked sources failed --check before.
- ludic-lsp: `initializationOptions.compilerDiagnostics` / `compilerPath`
  are honoured — on save the document's compilation unit is compiled and
  `file:line: error: msg` is published as a "ludicc" diagnostic (the
  option had been documented but never implemented); the workspace scan
  file is per process; bracket codes spelled as char literals
- Overlay added to LUDIC_PHASES, LudicTokens.kt, ludic-mode.el and the
  grammar (the game uses it; check-vocabulary flagged the drift)
- editors: VS Code snippets/package.json/extension.js (`${workspaceFolder}`
  and `~` expansion, PATH lookup, Apache-2.0, real repo URL), Neovim root
  markers, Helix/Zed/Sublime bin/ paths, Emacs vocabulary, JetBrains
  comments; tools/editors/README.md no longer describes C tooling
- .forgejo workflows clone `${{ github.server_url }}/${{ github.repository }}`
  so a fork or mirror tests itself; the docs publish pushes the same way

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:26 +03:00
8fed9add66 refactor(x): per-process scratch dirs, Ludic ports of the LSP test and Forgejo release
- every scratch file lives in `$TMPDIR/x_<pid>/` (tmp_dir/tmp_path in
  prelude.ludic), removed by main's new dispatch() → tmp_cleanup();
  X_KEEP_TMP=1 keeps it. `x test` and `x check-*` may now run together.
- `x test-lsp` (tools/x/lsp_test.ludic) replaces tools/test-lsp.py: the
  whole request stream is framed into one stdin file, the server runs to
  `exit`, and the response stream is parsed back by request id; adds a
  check that ludicc's own error is published on save
- tools/x/forgejo.ludic replaces tools/ci/forgejo_release.py (curl with a
  0600 header file; the token is no longer on the command line;
  LUDIC_FORGEJO_API for forks)
- `x docs-palette --check` regenerates into scratch and compares, so the
  drift guard judges the working tree rather than git HEAD; the generator
  no longer emits a trailing blank line the formatter rejects
- `x test-tools`: exit 2 from test-lsp / test-grammar.js is a visible
  skip, never a pass; the widget-prop test uses the `id: Root` syntax
- tools/test-grammar.js: current vocabulary (property/model/handler/
  prefab/scene/event/become/@Queries), LUDIC_NODE_MODULES, exit 2 on skip
- tools/atlas.ludic rewritten in the current language (it did not compile)
- operators.ludic / os.ludic registered in the suite
- json.ludic: j_quote() writer helper

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:26 +03:00
647dfec334 feat(compiler): list literals, typed compound assignment, file:line diagnostics
- `[a, b, c]` list literals (E_LIST → emit_list); static_type learns
  slice-element, `new T`, list, string and literal kinds
- `x op= y` lowers through the same path as `x = x op y` (emit_bin_vals):
  fixed `*=`/`/=` use the Q16.16 64-bit paths, string `+=` concatenates,
  int→long widens; unary `-` keeps a fixed operand's type (arith_ty)
- one `unescape()` table for "strings", 'chars' and `interpolation`;
  `'\''`, `'\\'`, `'\"'` no longer read as 0; unterminated char literals
  and unexpected characters are errors instead of silently skipped
- every diagnostic is `file:line: error: msg` (g_parse_file / g_err_file,
  Node.file + Node.line set by node()); tok_desc() in expectation errors;
  duplicate `function` names and unknown `phase` names are reported in
  source terms (phase_id used to default unknown phases to Overlay)
- interpolation holes skip braces inside string literals
- hand-IR preludes move from the user `@fn_` prefix to `@lp_` so a user
  `is_ws` / `str_eq` / `path_join` no longer collides at link time
- `@ClearColor(expr)` accepts any constant expression; `Os.pid()` added
  (docs page + inventory); `str_starts()` in support/str
- main.ludic: `else if` flag ladder, char literals, stale script comments
- examples/lang/operators.ludic covers all of the above; os.ludic covers
  Os.pid; docs pages for Os.pid and the Overlay phase; ten changesets
- reseeded: selfhost/ludicc.seed.ll is the new compiler's own fixpoint

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:16 +03:00
ad548840c7 feat(engine): #90 atlas-aware Sprite component, #91 become from listeners, 0.3.x ergonomics batch
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 32s
ci / build-and-test (push) Successful in 2m49s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 30s
Closes the two open issues and lands the pending unreleased batch:

- #90: `Sprite { atlas: 1 }` routes esys_sprite through atlas_draw_ex
  (scale/flip/tint), so cell / cell_span / strip ids of any size draw
  through the engine sprite-render system. examples/library/sprite_atlas
  is the pixel-readback regression.
- #91: `become` from an @On(Event) listener / global handler / plain
  function no longer segfaults the compiler; it emits @L_scene_leave()
  (a dispatch on the live scene id) so the leaving scene's on-exit runs.
  UI_* handles are readable from any code (widget table built on first
  use). examples/library/scene_menus covers it.
- fix: a windowed `ludicc -o` build that reaches the audio runtime only
  through the atlas/Assets preload import now links audio.ll +
  AVFoundation (the audio backend link was gated on a game-level
  Audio.* call, so any windowed game declaring Sprite failed to link).
- the hand-written "Unreleased" CHANGELOG section is converted to
  changesets under changes/ so `x release` generates it.
- plus the batch: engine-driven retained UI + UiClicked event, Overlay
  phase, TileSkin tilemap-render system, Key.* constants, Font/Ui/File
  namespaces, Sprite.strip, prefabs, managers, countdown fields,
  enum-typed machines, layer @Queries, ludic.prefs / ludic.dungeon
  packages, Ai.seek pathing, Solids.solid2, cursor confine (mode 3)
  fix, shooter centre-aim fix, reserved-word function diagnostic.

Verified: x test (124/124), x test-tools, check-impl, check-vocabulary,
check-docs, docs-gen + docs-check, bootstrap-cfree (seed is a fixpoint).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 01:36:08 +03:00
e9c2c51bc3 chore(release): v0.3.0
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m28s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s
2026-09-02 08:37:20 +03:00
b2a6a45a56 docs: 0.2 -> 0.3 migration guide
Comprehensive migration guide for the v0.3.0 release: non-breaking upgrade,
the windowed quit-key + input auto-drive behaviour changes, the draw_sprite
deprecation, and adoption recipes for the package manager, controllers, Tiled
maps, and the #75-#89 engine/language features. Linked from the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:37:13 +03:00
347352cc4c feat(ecs): #80 entity-pool stats (Pool.live/free/reserved/capacity)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m27s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s
Ludic's ECS is already pool-based — the allocator recycles freed entity slots
through a freelist (L_alloc pops @L_freen before growing @L_entc), and component
storage is fixed per-entity arrays, so spawn/despawn churn (bullet-hell/horde)
does no per-spawn heap allocation and cannot fragment. Expose that with a Pool.*
namespace so a game can watch reuse: Pool.live (alive now), Pool.free (recycled
slots waiting), Pool.reserved (high-water — stays flat across a steady
spawn/despawn loop, proving reuse not reallocation), Pool.capacity (the fixed
cap). Zero-cost inline reads of the existing counters.

Example pool.ludic proves the key property: after despawn+respawn,
Pool.reserved() stays 3 (freed slot reused) — prints 0 0 3 3 0 2 1 3 0 3 1.
4 docs pages. Full suite 118/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:23:58 +03:00
f2cb3cd7e8 feat(assets): #82 incremental asset preloading + loading-scene pattern
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m27s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s
Assets loaded synchronously in Boot stalled the first frame(s). Adds an
Assets.* preload queue over the #81 atlas: Assets.enqueue(name, path) queues a
named image without loading it, Assets.pump(max) loads up to max per frame
(returns how many), and Assets.total/loaded/ready/progress (0..100) drive a
progress bar. A loading scene pumps a few per frame, draws Assets.progress(),
and becomes the play scene once Assets.ready() — the deterministic, no-threads
form of async preloading (work spread across frames; same enqueue+pump order
loads identically every run). Loaded assets are reachable by name via
Assets.get / Sprite.named.

Example preload (enqueue 3, pump incrementally 0->33->66->100, ready flips, get
by name) prints 3 0 0 0 1 33 66 1 100 1. 6 docs pages. Full suite 117/0,
fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:19:19 +03:00
23e232e380 feat(lang): #76 namespace block form with export/internal visibility
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m25s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s
`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). Inside the block
each `function short(...)` is emitted as `namelower_short`; export (the default)
makes it callable as Name.short(...), internal keeps it a private helper
(emitted, callable by short name from siblings — calls are rewritten — but
Name.internalOne() is a compile error). Block sugar for the per-function
@Namespace annotation; a package's public API reads at a glance. Namespaces
declared the old way are unchanged (gameplay_foundation still passes).

namespace added to LUDIC_KW_DECL + JetBrains/TextMate + docs page + inventory
(vocab/impl/docs checks green). Example namespace_block (export + internal +
sibling calls + internal-visibility compile error verified). Full suite 116/0,
fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:14:54 +03:00
3eb5447f74 feat(input): #89 cursor capture — Input.cursor_mode (hide/lock/confine)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m24s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s
A windowed action game can hide the OS cursor and lock/confine the mouse to the
window. Input.cursor_mode(mode): 0 normal, 1 hidden (draw your own reticle),
2 locked (hidden + dissociated — the mouse feeds relative motion via
Input.mouse_dx/dy and Input.mouse_x/y is a clamped virtual cursor, FPS/twin-stick
aim), 3 confined (dissociated but visible; the mouse can't leave the window).
The platform auto-releases (shows + reconnects) while the window is not key
(Cmd-Tab) and on close, so the cursor is never left captured. Headless it is a
no-op (DCE'd).

Native macOS impl in cocoa.ll: [NSCursor hide]/[unhide] (ref-counted, toggled
only on change so the count stays balanced across focus changes),
CGAssociateMouseAndMouseCursorPosition, and CGGetLastMouseDelta for the relative
virtual cursor, behind a new win_cursor_mode intrinsic. Windowed-only behaviour
(not in the headless golden suite); example compiles headless and links
windowed. Full suite 115/0, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 08:06:34 +03:00
f3f1336882 feat(assets): #81 namespaced spritesheet/atlas API (Sprite.* / Assets.*)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m24s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 29s
Sprite loading was a bare png_load — one file per 16x16 sprite, no way to load
one sheet and address a cell by grid coords or name. Adds a Sprite.*/Assets.*
runtime (atlas.ludic) 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,cw,ch), Sprite.cell(sheet,col,row),
Sprite.cell_span(sheet,col,row,cols,rows) (a sprite may span >1 cell),
Sprite.define/named (name + lookup), Sprite.draw/draw_scaled (through
camera/zoom/clip like Screen.sprite), Sprite.width/height, and
Assets.image/load/get. Spliced on demand (Sprite.sheet/… or Assets.*), so a
program using neither is byte-identical.

Example examples/library/atlas.ludic (verified against a real 12x11 Kenney
sheet, incl. a 2x3 multi-cell span and named lookup). 14 docs pages. Full
suite 114/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:58:13 +03:00
ab4546e365 feat(compiler): #75 resolve the auto-spliced engine runtime from LUDIC_HOME
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m25s
commit-lint / conventional-commits (push) Successful in 6s
docs / build-and-deploy (push) Successful in 28s
The compiler auto-splices runtime/native/* for any ECS game, but resolved it
relative to the build CWD, then fell back to the package module root
($LUDIC_MODULES) — forcing every external project to copy/symlink the engine
runtime into ludic_modules/. The runtime is part of the toolchain, not the
project: do_import now resolves a runtime/... import that isn't found locally
from $LUDIC_HOME (default: the compiler binary's dir — where cocoa.ll/audio.ll
already come from), before the module root. So ludic_modules/ holds only
third-party packages.

In-repo builds are byte-identical (the runtime resolves locally there, so the
$LUDIC_HOME fallback never fires; fixpoint holds). Verified by a hermetic test
that builds an ECS game from an external CWD with no runtime/ or ludic_modules/
under it, resolving the runtime from LUDIC_HOME. Full suite 113/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:49:10 +03:00
ad3be0c53c feat(input,ecs): #79 Input.axis_i directional int + #84 world bounds
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m23s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s
#79 — Input.axis_i(neg,pos) -> int returns a -1/0/1 movement intent from the
multi-key device set, so WASD-to-movement needs no bool->int glue and feeds an
int mover directly (dx = Input.axis_i('a','d')).

#84 — a Bounds config entity (rect + policy, from ludic.core) drives the
engine-owned world-bounds system (LateUpdate): clamp / wrap / bounce (clamp +
flip Body velocity) / kill (despawn a body fully outside). Reads the Collider
size so the box stays inside; off by default, spliced only when Bounds is
declared (byte-identical otherwise). Adds World.despawn(e) — the by-id
reflective despawn (runs @OnDespawn + frees) via a new world_despawn intrinsic
whose @fn_world_despawn helper is emitted in emit_program's tail once a use is
seen (the g_uses_* prelude pattern), used by the kill policy and callable from
any system.

Examples input_movement + world_bounds. Full suite 112/0, goldens
byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:44:41 +03:00
0497029dae fix(input): #87 key_pressed/key_released edges never fired under the frame loop
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m21s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s
Since #83 the loop commits the device layer once per frame (input_drive), but a
game that ALSO called Input.poll by hand committed a second time in the same
frame; 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 the edges (held &&
!prev) could never see a transition.

Fix: an in_have_frame_driver flag. The loop's input_drive sets it; a manual
Input.poll under the loop then becomes a no-op returning the frame's key
instead of re-committing. An entry-driven harness has no loop, so the flag
stays false and each Input.poll commits a frame as before (the #7/#50
record/replay + device tests are unchanged). Frame loop now calls input_drive.

Example input_edge (press edge on the down frame, release edge on the up
frame). Full suite 110/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:27:24 +03:00
bdf1a97550 fix(windowed): #88 don't force-quit windowed games on Esc or 'q'
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m20s
commit-lint / conventional-commits (push) Successful in 5s
cocoa.ll win_poll hard-coded Escape (keycode 53) and 'q' as quit (W_running=0),
so a shipped windowed game died the instant a player pressed Esc (pause) or
typed 'q'. Remove the dev-loop quit keys for windowed builds: Escape is
delivered as key 27 and 'q' is an ordinary key, consistently across the
single per-frame @W_key and the #50 held-key set (ev_keyval maps Esc->27, not
'q'). A windowed game owns Esc/pause and quits via quit() or the window close
button (still ends the run). The headless rt_poll keeps its own 'q'=quit for
scripted golden tests, so nothing headless changes. Also stops forwarding
consumed key events to -sendEvent:, which rang AppKit's system beep per key.

Windowed-only behavior (not exercised by the headless suite); verified a
windowed build links and assembles cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:23:09 +03:00
57b8747c31 feat(render): #85 engine sprite-render system + deprecate bare draw_sprite
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m20s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s
The engine already auto-ticks SpriteAnim/Motion; it now auto-DRAWS too.
Declare a Sprite component (id/offx/offy/scale/flip/tint/hidden, shipped from
ludic.core) on an entity with a Position and esys_sprite draws it each Render
frame — no hand-written Render handler, no hand animation (adds the SpriteAnim
frame when present). Registered on the engine-system registry (Render) and
spliced only when the game declares Sprite, so a game that never declares it
compiles byte-identically; opt out by omitting Sprite or `disable system
esys_sprite`.

Deprecates the bare draw_sprite/draw_sprite_scaled globals in favour of
Screen.sprite/Screen.sprite_scaled: a direct bare call emits a one-time
compile-time deprecation note (the bare form still lowers, since Screen.sprite
uses it); migrates the chronorift demo to the namespaced calls (golden render
byte-identical).

Example sprite_render (pixel-readback: engine draws the sprite, respects
hidden). Full suite 109/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:20:13 +03:00
d9287d6b1d feat(render): #86 @ClearColor — Render phase auto-clears + auto-presents
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 23s
ci / build-and-test (push) Successful in 2m18s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
@ClearColor(0xRRGGBB) declares the framebuffer clear colour, so the engine
owns the per-frame clear and flip: the Render phase clears to the colour at
the top and presents after the handlers run. Games drop the repeated
Screen.clear(color)/Screen.show() boilerplate, and the colour is configured
declaratively (an annotation) 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).

Parser reads @ClearColor(int) into g_clear_color/g_has_clear_color;
emit_game_main emits rt_clear before and rt_present after the Render phase,
gated on the flag. Example clear_color (pixel-readback verified — an undrawn
pixel holds the clear colour, proving the engine cleared), docs page +
inventory entry. Full suite 108/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:10:44 +03:00
bccd26fb29 feat(input): #83 Input Manager + auto-commit the device layer in the frame loop
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m18s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s
Fixes the papercut: the generated frame loop called rt_poll() (feeding only
Input.key) but never input_poll(), so Input.active/key_down/mouse/pad read
empty unless the game called Input.poll() by hand. The loop now calls
input_poll() when the game uses any Input runtime method — committing the
held-key/mouse/gamepad state, and record/replay — and stores its return as the
frame key so Input.key still works. A game using no Input runtime keeps the
plain rt_poll path, byte-identical.

Adds the Input-Manager API: Input.action(name,key) ships a default binding
(kept if already bound, so a rebind/loaded map isn't clobbered),
Input.bind_pad(name,button) makes an action device-agnostic (keyboard OR pad),
and Input.active/just_pressed/just_released read the multi-key device layer
with clean on-press/on-release edges (deterministic, dispatch-free — a handler
polls the edge; a replay fires identically).

Examples input_manager + input_auto, 5 docs pages. Full suite 107/0, goldens
byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 07:04:10 +03:00
6a83c28e05 feat(camera): #78 deterministic Camera.zoom (Q16.16 render-time zoom)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m15s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
The #78 investigation rejected hardware f32/f64 for the coordinate types
(they would desync lockstep/replay/save) and identified camera zoom as the
one genuinely-missing render feature. Ship it: Camera.zoom(scale) scales the
whole view about the screen centre by a Q16.16 factor, threaded through the
same two framebuffer chokepoints (rt_put_px/rt_fill_rect) that carry the
camera offset, so it composes with Camera.set/follow/shake. 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.
The world coordinate types stay integer px + Q16.16 velocity, so it's a pure
render-time transform and itself deterministic.

Example examples/library/camera_zoom.ludic (pixel-readback verified),
docs page, RFC updated (docs/RFC-POSITION-TYPES.md). Full suite 105/0,
fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 06:55:37 +03:00
5af5bdd060 feat(core): #77 ship canonical Position/Body/Collider from ludic.core package
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 2m13s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 28s
The engine-owned esys_move (#65) reads Position/Body/Collider/Solids by name,
but no package defined them — every game and example re-declared identical
bundles by hand. Ship them as the source package ludic.core; games import
instead of copy-pasting, and extend by composition (attach their own
components on the same model). AOT → compiles into the consumer's ECS with no
seam; integer + Q16.16 deterministic. Adds examples/library/core_components
(driven by esys_move, composed with a game Health component) as a
controller_case (104/0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 06:46:06 +03:00
7f55554ae2 docs(rfc): #78 position/vector numeric types — keep deterministic int + Q16.16
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 31s
ci / build-and-test (push) Successful in 2m13s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
Investigation for #78. Decision: reject hardware f32/f64 (breaks the
determinism the whole engine/netcode/replay/save stack depends on); keep
integer Position + Q16.16 fixed. Sub-pixel is already solved (Body.rx/ry
accumulators); zoom belongs on the camera as a Q16.16 render-time scale;
i64 world extent is a clean additive opt-in to defer until a game needs it;
Position stays a reflected/saved/networked component while IVec2/Vector are
the by-value math types. Full rationale in docs/RFC-POSITION-TYPES.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 06:40:12 +03:00
dbb4ca6403 docs(controllers): overview of the six-lever contract + the five packages (#57)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 2m13s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
docs/CONTROLLERS.md documents the mechanism-vs-policy thesis, the six extension
levers, the ludic.gameplay/platformer/shooter/npcai/rpg packages, and how to
build a game against them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 17:27:01 +03:00
46c275c4c1 feat(controllers): #59 RPG systems — ludic.rpg 7-module suite (base + extensible)
Some checks failed
commit-lint / conventional-commits (push) Waiting to run
docs / build-and-deploy (push) Waiting to run
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Has been cancelled
Seven independently-usable modules on the six-lever contract with name-keyed
data registries (zero-code content): movement (grid/free/tween, 4/8-axis, veto +
interact), inventory + equipment (stat bonuses via the gameplay modifier stack),
crafting, event-driven quests + flags, an Ink/Yarn dialog graph, Sokoban
pushables + a switch/plate/gate signal graph, and over-time status effects
through the Combat pipeline. Reuses ludic.gameplay Stats/Combat. Deterministic.
21-check example, in the suite (103 passed, 0 failed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 17:25:56 +03:00
9ce69d23e3 feat(controllers): #61 NPC AI — ludic.npcai package (base + extensible)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m11s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 29s
Perception -> decision -> action AI that writes the SAME intent fields the
player controllers read, so an enemy reuses the shooter's weapon/aim and a
companion reuses the mover (friendly vs enemy = faction + goal, not code).
Vision/Memory perception (throttled, faction + optional LOS), three decision
models (FSM, utility, behaviour tree) writing one Brain intent with a
cancellable DecisionMade hook, Reynolds flocking steering, and a Follower
companion. Reuses ludic.gameplay Faction/Stats. Deterministic. 8-check example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 17:12:32 +03:00
02a8bcc324 feat(controllers): #60 shooter — ludic.shooter package (base + extensible)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 2m9s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
Top-down shooter: TopDown decoupled move/aim (mouse/stick/move-dir/auto-aim),
a name-keyed Weapon registry (fire-rate/spread/pellets/pattern + data-driven
pierce/homing), and a self-contained deterministic Projectile pool with
faction-filtered Combat hits, pierce, ring/spiral patterns, and homing. One
weapon impl serves player + NPC via a want_fire intent. Budgeted wave Spawner.
Reuses ludic.gameplay Faction/Combat/Stats. 11-check example, in the suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 17:04:49 +03:00
dd5cb5817b feat(controllers): #58 platformer — ludic.platformer package (base + extensible)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 2m8s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
The reference six-lever controller: Platformer component (jump feel as data),
decomposed input/move/gravity/jump/anim engine sub-systems (each disable-able),
JumpRequested/Landed/StateChanged events, gravity policy enum, and the opt-in
scaffolding (moving/crumble platforms w/ rider carry, pickups+score, springs,
hazards+Life, checkpoints/goal). Reuses the shared esys_move collision.

Also fixes a latent SSA-name collision in ludic_sweep_entity that triggered
once a program declared >=11 events. Reseeded; C-free fixpoint holds.
3 new regression cases (100 passed, 0 failed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 16:41:34 +03:00
ed385efa7b feat(controllers): #57 foundation — ludic.gameplay package + lever 5 (disable system)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 2m4s
commit-lint / conventional-commits (push) Successful in 4s
Shared building blocks for the builtin gameplay controllers (#57): the
ludic.gameplay source package (Cooldown timer, Stats + timed modifier stack,
Faction table, Combat damage pipeline with cancel/mutable hooks), plus the
compiler `disable system <esys_fn>` extensibility lever. Deterministic,
integer-only; C-free bootstrap fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 16:23:10 +03:00
764a0296ce feat(stdlib): Tiled P6 — infinite/chunked maps, .world stitching, base64+zstd (#74)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m4s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
- Infinite/chunked maps: <chunk x y width height> (TMX) and JSON chunks[]
  (default 16x16) decode and flatten into the dense layer array, sized to the
  chunk union; the map's 0/0 header dimensions fall back to the flattened bounds.
- .world stitching: Tiled.world / Tiled.world_count / Tiled.world_map read a
  .world (JSON, reusing Json.parse) and list its member maps at their offsets.
- base64+zstd: a self-contained pure-Ludic Zstandard decompressor
  (runtime/native/zstd.ludic, RFC 8878) — the design's "largest single item,
  explicitly last". Frame header + raw/RLE/compressed blocks; raw/RLE and
  direct-weight Huffman literals; the full FSE sequence path (predefined,
  transcribed exactly from zstd's hardcoded tables since they're not rebuildable
  from the default distributions; RLE; FSE-described) with repeat offsets and
  execution. Decodes 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 (documented scope).

Proven by library/tiled_p6.ludic (12 assertions): TMX + TMJ chunk flattening,
.world offsets, and a real zstd-compressed tile layer decoding byte-exactly to
its CSV baseline. x test: 97 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 16:07:10 +03:00
78a5719fae feat(engine): Tiled P5 — image/group layers, iso/hex/staggered coords, Wang (#73)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 21s
ci / build-and-test (push) Successful in 1m59s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 28s
- Image layers: <imagelayer> renders with parallax + optional repeatx/repeaty
  tiling across the view; the image loads relative to the map file. Group layers
  flatten at build (children render in file order); layer offset/opacity/tint are
  queryable (Tiled.layer_kind / layer_offsetx / layer_offsety / layer_opacity /
  layer_tint), a group's tint applying recursively.
- Orientation transforms: Tiled.cell_x / Tiled.cell_y compute a tile cell's
  screen position for orthogonal, isometric ((x-y)*tw/2, (x+y)*th/2), staggered,
  and hexagonal (rows step by (tileh+hexsidelength)/2, alternate rows shoved per
  staggerindex) — the tile draw now places cells through them.
- Wang: exported Wang-set GIDs are ordinary GIDs and resolve through the standard
  GID resolver; the terrain-corner authoring concept is editor-side (design cut).

Proven by library/tiled_p5.ludic (14 assertions) over the real
isometric_grass_and_water.tmx (iso + Wang) and hexagonal-mini.tmx, plus a
hand-authored image+group fixture. x test: 96 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 15:14:36 +03:00