feat(ecs): Light2D/Occluder/Ambient as auto-consumed components (#47)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m18s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 19s

The ECS-native shape the 2D lighting follow-up asked for, built on the
engine-owned-system hook from #43. A torch is just an entity carrying
Light2D, a wall an entity carrying Occluder, and one Ambient entity sets
the night tint — the engine runs the whole deterministic light pass at the
end of the Render phase (ambient modulate -> carve occluder shadows ->
accumulate additive radial lights) and presents. No Light.* calls wired.

- runtime/native/systems_light.ludic: esys_light2d, consuming the components
  through the by-name reflection ABI and reusing the #4 accumulation core
  (light_ambient / light_occlude / light_point). Reads position from a
  Position component when present, else the light's own x/y.
- Split from systems.ludic so a SpriteAnim/Motion-only game never links the
  light pass; spliced (with light.ludic) only when Light2D/Occluder declared.
- emit_main now boots rt_init like the auto-loop/test runner, so an
  entry-driven game that renders has its framebuffer allocated (headless:
  allocate only, no window, byte-identical stdout for non-rendering games).

Worked example + regression: examples/library/light_ecs.ludic (32 1 32 1).
Full suite 74 passed, self-host C-free fixpoint intact, no golden drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 15:49:50 +03:00
parent b0143337d9
commit 3679ce1797
8 changed files with 14660 additions and 14404 deletions

View file

@ -52,6 +52,11 @@ function emit_main(d: Node) -> void {
emit(" store i32 %argc, ptr @L_argc\n")
emit(" store ptr %argv, ptr @L_argv\n")
emit(" store i32 0, ptr %retval\n")
# boot the runtime like the auto-loop / test runner do, so an `entry`-driven
# game that renders (drives tick_render, draws, uses the engine light pass) has
# its framebuffer allocated. Headless it only allocates — no window, no output —
# so a non-rendering entry game is unchanged.
if (find_fn("rt_init") != null) { emit(" call void @fn_rt_init()\n") }
emit_block(d.a)
if not g_term { emit(" br label %ret\n") }
emit("ret:\n")

View file

@ -613,7 +613,18 @@ function maybe_splice_runtime() -> void {
if uses_engine_systems() {
g_uses_esys = true
cur_dir = ""
do_import("runtime/native/systems.ludic")
# SpriteAnim / Motion -> the animation systems (self-contained, reflection only)
if (find_comp("SpriteAnim") != null) or (find_comp("Motion") != null) {
do_import("runtime/native/systems.ludic")
}
# Light2D / Occluder -> the lighting render system, which links against the
# 2D light pass (light.ludic). do_import dedupes, so this is a no-op when the
# game also uses Light.* directly (g_uses_light already pulled it in).
if (find_comp("Light2D") != null) or (find_comp("Occluder") != null) {
do_import("runtime/native/light.ludic")
do_import("runtime/native/systems_light.ludic")
g_uses_light = true
}
cur_dir = saved
}
}

File diff suppressed because it is too large Load diff