Aerial perspective is a curve now. A low sun shines through far more air than a high one and shines ALONG the ground rather than down onto it, so density, height falloff and forward scatter all ride the sun's elevation; overcast thickens the air and flattens the scatter, because a grey sky has no disc to scatter from. `lowsun` falls away BELOW the horizon as well as above it, or the middle of the night gets a dawn's haze with no dawn to justify it. The term that was missing entirely is distance DESATURATION. Blending a saturated green ridge toward a saturated blue noon sky leaves a saturated ridge - which is why the same valley read as a photograph at dusk, where the fog colour happened to be a warm grey, and as a toy at one o'clock. A surface is now pulled toward its own luminance faster than the fog itself arrives. Measured far/near saturation at the camp: 07:00 1.11 -> 0.89, 09:00 1.04 -> 0.93, 13:00 0.98 -> 0.89. The grade is the hour's too - nine literals bound at the draw, written by daylight_set now. Noon is the case worth naming: direct sun is warm-white and the only thing filling a midday shadow is a blue sky, so noon gets a cool balance over a blue-lifted shadow with hard contrast, and dawn and dusk the reverse. Ground R-B, lit vs shadowed: 07:00 +42.8/+14.2 -> +48.9/+15.1, 13:00 +32.2/+14.8 -> +25.2/+2.9. Gain is left alone deliberately: the grade is `c * gain + lift * (1 - c)`, so warming it warms the whole frame, and warming it at noon made one o'clock yellower than seven in the morning - the opposite of the point. The visible sky is relit. Turning a photograph on its axis does not change what colour it was taken at, so every sunset had a mid-morning blue overhead. An analytic sky supplies the chroma and the photograph keeps the luminance: the cloud stays where it is and goes orange at dusk, the zenith goes deep blue at noon, and no second sky is shipped. It fades out under the horizon and eases off under cloud. The ground bounce follows the ground, crossing meadow to rock at the map's treeline instead of being one green constant everywhere including above the scree. R3D_NOAIR=1 restores all of it, so a before-and-after comes from one binary at one hour; it joins R3D_NOCLOUD / R3D_NOSHADOW / R3D_NOGI. Verified on macOS OpenGL, macOS Vulkan (MoltenVK) and Windows Vulkan (RTX 3070 Ti). Backends agree: mean difference 0.15-0.88/255 within a machine. Across machines the ORIGINAL renderer already differed by 5.02/255 at 19:12 and this build differs by 2.80, so cross-platform variance is pre-existing and did not grow. 400 frames: GL 7.4 s before and after, VK 7.0 s before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| .forgejo | ||
| assets | ||
| changes | ||
| docs | ||
| examples | ||
| packages | ||
| runtime | ||
| selfhost | ||
| tools | ||
| .editorconfig | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CODE_OF_CONDUCT.md | ||
| COMPILING.md | ||
| CONTRIBUTING.md | ||
| install.sh | ||
| LANGUAGE.md | ||
| LICENSE | ||
| README.md | ||
| VERSION | ||
Ludic
A compiled language for 2D games. The entity-component system is part of the
syntax, the runtime is deterministic fixed-point, and ludicc lowers Ludic
straight to LLVM IR — no C is generated, compiled or linked in a build.
The compiler is written in Ludic. It compiles its own source to a byte-exact fixpoint and rebuilds from a checked-in IR seed with clang alone; CI asserts that on every push.
- Documentation: https://workshopsoft.pages.workshopsoft.io/ludic/
- API reference: https://workshopsoft.pages.workshopsoft.io/ludic/api.html
- Issues: https://git.workshopsoft.io/workshopsoft/ludic/issues
program Hello {
property Position { column: int = 0, row: int = 0 }
property Velocity { delta_x: int = 0, delta_y: int = 0 }
handler SpawnEnemies phase Start {
spawn Enemy { Position { column: 3, row: 4 }, Velocity { delta_x: 1, delta_y: 0 } }
spawn Enemy { Position { column: 10, row: 2 }, Velocity { delta_x: 0, delta_y: 1 } }
}
# a handler declares the entities it touches; the body runs
# once per match, with each property bound by name.
@Queries(these: [Position, Velocity])
handler AdvancePositions phase FixedUpdate {
Position.column += Velocity.delta_x
Position.row += Velocity.delta_y
}
}
Getting started
Install the toolchain — the compiler, the ludic CLI, the engine runtime, the
formatter and the language server — with one command:
curl -fsSL https://workshopsoft.pages.workshopsoft.io/ludic/install.sh | sh
It installs into ~/.ludic and puts ~/.ludic/bin on your PATH in every
shell — the PATH line lives in ~/.ludic/env, sourced from ~/.profile,
~/.zshenv and your bash or fish config. Nothing else on the machine is touched;
uninstalling is rm -rf ~/.ludic and deleting those two-line blocks. Where a
prebuilt toolchain exists for your platform it is downloaded and verified against
a published checksum; where it does not, the installer bootstraps from the
compiler's own IR seed with clang. Either way you need clang (or Xcode's Command
Line Tools) to link, since Ludic emits LLVM IR and links it natively.
Then make a game:
ludic new mygame
cd mygame
ludic run # compiles src/main.ludic and opens a native window
ludic new writes a manifest, a program that already moves something on screen,
and a test. ludic build stops at the binary; ludic bundle goes on to the
thing you can actually give someone. Rendering is deterministic, so a frame can
be produced without a window, which is what CI diffs:
ludic test
ludic build --headless
printf 'ddddwww' | ./build/mygame_headless # writes build/out.ppm
ludic help lists every command, and ludic doctor checks the install.
examples/ is a tour grouped by intent: games, rendering,
ECS, events, networking, language features and the standard library — compile any
of them with ludic build examples/games/snake.ludic.
Building from a checkout
Contributors also get ludic-dev, a second binary carrying the toolchain's own
tasks — building the compiler, the suites, the docs site, releases. It is built
from a checkout and is not part of an install, so nothing a user runs is mixed
up with it. Bootstrapping is the only step Ludic cannot do for itself, since
compiling Ludic needs a compiler — clang assembles the checked-in IR seed, and
that compiler builds the rest:
mkdir -p bin && clang selfhost/ludicc.seed.ll -o bin/ludicc
bin/ludicc tools/ludic-cli/dev.ludic -o bin/ludic-dev
bin/ludic-dev build # -> bin/{ludicc,ludic,ludic-dev,ludic-fmt,ludic-lsp}
bin/ludic-dev test # the regression suite
The language
- ECS in the syntax.
property,modelandhandlerare keywords. Query withfor (a, b) in query [A, B, {Tag}] where <expr> { … };spawnanddespawnrecycle entity slots;@-annotations drive lifecycle hooks. - Deterministic by construction. Q16.16
fixedarithmetic and a seeded RNG give the same frame byte-for-byte on every run — the basis for replays, lockstep netcode and golden-image tests. - Scenes and state machines.
scene/layer/becomemodel mutually-exclusive game states with enter and exit hooks;match/machine/statehandle dispatch and per-entity FSMs. - Events and networking. A cancellable event bus (
event/emit/@On) and networking primitives (@Sync, ownership, RPCs) over a built-in transport. - Batteries in the language. Framebuffer primitives, PNG sprites, TrueType
text and a retained
uiwidget tree declared as data, plus a namespaced standard library (Math,Text,List,Random,Crypto,Tiled, …). - Whole-world snapshots.
save()andload()serialize every entity, property and programvarin one call.
LANGUAGE.md is the full reference; the API reference documents every symbol on its own page.
Shipping
A built binary is a program, not an application: it opens its assets by a path relative to the working directory, so it runs from the project root and nowhere else, and it wears the generic executable icon.
ludic pack # every asset the game opens, into one .lpak
ludic bundle # ...and that, the binary, an icon and the metadata, as a .app
Nothing about how the game is written changes. gltf_load("assets/kit/hiker", …) reads a file during development and a run of bytes inside the bundle once
shipped, and cannot tell which — the pack is spliced in at file_open, the one
place every asset in a Ludic program comes through. A bundled game also gets a
boot splash it controls (App.splash_hide()) and a writable home under
Application Support, because Finder starts a .app at / where no save could
be written.
Without a pack beside it — which is every ludic run — nothing mounts and every
open goes to the filesystem exactly as before. See docs/SHIPPING.md.
Packages
Dependencies are identified by URL, resolved with minimal version selection, and cached in a content-addressed store:
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
ludic get # install from package.ludic, write the lock
ludic verify # check locked packages against the store
The ludic.* packages — canonical ECS components, the gameplay, platformer,
RPG, shooter and NPC-AI modules — ship with the toolchain, so importing one needs
no fetch step at all.
See docs/PACKAGES.md for the manifest and lockfile model.
Editor support
Editors spawn ludic lsp; the server ships with the toolchain, so there is
nothing extra to install. It speaks LSP 3.17 over stdio, so one binary serves
every editor: completion, diagnostics from the compiler itself, go-to-definition
and rename across imports, and comment-preserving formatting. ludic fmt runs
the same formatter as a CLI, for pre-commit hooks. Both understand
```ludic fences in Markdown. Plugins and drop-in config for VS Code, JetBrains, Neovim,
Helix, Emacs, Sublime and Zed are in tools/editors/.
Status
The native 2D backend ships: a Cocoa window on macOS, a headless renderer for
CI, and the whole runtime — framebuffer, PNG/DEFLATE decoding, TrueType
rasterizer, retained UI, RNG — written in Ludic under
runtime/native/. Only the window seam (win_*: window,
keys, mouse, cursor, gamepad, touch) is hand-written LLVM IR against the
platform ABI, the same floor Rust and Swift stand on.
The web/wasm32 backend is not currently available. The browser platform
layer is in-tree under runtime/web/, but emitting wasm was a
capability of the retired C compiler and has not been re-wired on the
self-hosted toolchain. --target cross-compilation and --shared libraries are
in the same position. See COMPILING.md.
Releases follow SemVer and are cut from changesets by ludic-dev release, then built
and published by CI from the tag; see CHANGELOG.md.
Contributing
CONTRIBUTING.md covers the development loop, the commit and
code conventions, how the bootstrap fixpoint works, and what a self-hosted CI
runner needs. Issue and pull-request templates are under
.forgejo/.
License
The compiler and runtime are licensed under the
Apache License 2.0 (SPDX-License-Identifier: Apache-2.0).
The bundled Kenney art under assets/kenney/ is
third-party and released under CC0 1.0; each pack keeps its own
License.txt. Code and assets are licensed separately — Apache-2.0 covers the
source, not the art.