From dbb4ca6403a65e7149d24ca0154b3b6b58463854 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 1 Sep 2026 17:27:01 +0300 Subject: [PATCH] docs(controllers): overview of the six-lever contract + the five packages (#57) 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 --- docs/CONTROLLERS.md | 84 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/CONTROLLERS.md diff --git a/docs/CONTROLLERS.md b/docs/CONTROLLERS.md new file mode 100644 index 00000000..eb5ea103 --- /dev/null +++ b/docs/CONTROLLERS.md @@ -0,0 +1,84 @@ +# Builtin gameplay controllers + +Ludic ships a layer of **ready-made gameplay controllers** — the "drop this in and +you have a platformer / shooter / RPG / NPC" starting point — as external, versioned +**source packages** (see [PACKAGES.md](PACKAGES.md)). Because Ludic is AOT, a source +package compiles straight into your game's compile-time ECS: there is no ABI seam and +the whole thing stays deterministic, so lockstep, input replay and `world_save` +snapshots hold across every controller. + +This is the direction decided in issue #57 and implemented in #58–#61. + +## The design thesis: mechanism vs. policy + +The engine ships **mechanism**; the developer owns **policy**. A controller is a thin +spine of mechanism (integrate velocity, resolve a tile collision, advance a cooldown) +with **named decision points** where policy is injected. Every controller ships a +sensible default so it works out of the box, and exposes every decision so the default +can be observed, tuned, vetoed or replaced. + +**Every builtin controller is a _base, not a cage_.** If a part of a controller can't +be reached by at least one of the six levers below, that is a design bug in the +controller. + +## The six extension levers + +1. **Data override** — every controller `property` is POD with sane defaults, so + behaviour is configured by spawning with overrides, no subclassing: + ``` + spawn Hero { Platformer { jump_height: 5, apex_frames: 10, air_jumps: 1 } } + ``` +2. **Composition** — controllers are small property bundles, never god-objects. A game + adds its own `property`/`handler` to the same model; engine systems resolve fields + by name through the reflection ABI and no-op when a field is absent. +3. **Phase ordering** — engine systems run at a defined point in their phase. A handler + in `Update` runs before the movement sweep; a handler in `LateUpdate` runs after + collision resolved. That is pre/post-processing around any engine tick, deterministic + and same-order every frame. +4. **Event hooks** — every meaningful decision emits an event, and the consequential + ones are `cancellable`. `@On(JumpRequested) { cancel }` vetoes; a mutable channel + (e.g. `Combat.set_amount`) rewrites a value. Open to in-language games and foreign + mods over the same EV0–EV7 bus. +5. **Sub-system disable + replace** — each controller is decomposed into independently + disable-able engine systems. `disable system esys_platformer_move` drops exactly one + tick so a game can carry the component but drive it with its own handler, keeping the + rest of the controller intact. +6. **Policy selection** — where a behaviour is a *formula* (gravity curve, aim mode, + AI decision model, damage calc), the controller exposes a `policy`/`mode`/`model` + enum field the system switches on, plus a cancellable/mutable event for the fully + custom case. Ludic has no closures; the enum-field-plus-event pair is the honest, + deterministic substitute. + +## The packages + +| Package | Issue | What it gives you | +| --- | --- | --- | +| **ludic.gameplay** | #57 | The shared foundation: `Cooldown` timer, `Stats` + a timed modifier stack, a `Faction` friend/enemy/neutral table, and a `Combat` damage pipeline (`cancellable DamageAboutToApply` with a mutable amount, `Damaged`/`Died`/`Healed`). | +| **ludic.platformer** | #58 | Jump-feel `Platformer` (apex/coyote/buffer/variable-height/multi-jump), decomposed input/move/gravity/jump/anim sub-systems, `JumpRequested`/`Landed`/`StateChanged` events, a gravity policy enum, and opt-in scaffolding (moving/crumble platforms with rider carry, pickups+`Score`, springs, hazards+`Life`, checkpoints/goal). | +| **ludic.shooter** | #60 | `TopDown` decoupled move + aim (mouse / stick / move-dir / nearest-enemy auto-aim), a name-keyed `Weapon` registry (fire-rate/spread/pellets/pattern + data-driven pierce/homing), a deterministic `Projectile` pool (faction-filtered hits through `Combat`, pierce, ring/spiral patterns, homing), and a budgeted wave `Spawner`. | +| **ludic.npcai** | #61 | Perception → decision → action: `Vision`/`Memory` (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. The AI writes the *same intent fields the player controllers read*, so an enemy reuses the shooter's weapon and a companion reuses the mover. | +| **ludic.rpg** | #59 | Seven independently-usable modules: movement (grid/free/tween), inventory + equipment, crafting, event-driven quests + flags, an Ink/Yarn dialog graph, Sokoban puzzles + a switch/plate/gate signal graph, and over-time status effects. Content is name-keyed data registries — a mod adds items/recipes/quests/dialog with zero code. | + +## Using a controller + +The genre packages build on the engine's `Body`/`Collider` + `esys_move` (swept-AABB +tile collision, in `runtime/native/systems_move.ludic`) and on `ludic.gameplay`. In a +real project you `x add` them; in this repo the packages live under `packages/` and a +game compiles against them with `LUDIC_MODULES`: + +``` +LUDIC_MODULES=packages ludicc examples/games/platformer_demo.ludic -o platformer +``` + +Worked, self-checking examples for every controller live in `examples/games/` +(`platformer_demo`, `platformer_scaffolding`, `shooter_demo`, `npcai_demo`, `rpg_demo`) +and `examples/library/gameplay_foundation.ludic`; each is a deterministic regression +case in `x test`. + +## What we deliberately do NOT do + +- **No inheritance / virtual overrides.** Extension is composition + events + policy + fields, not subclassing — determinism-safe and replayable. +- **No hidden global singletons.** Controller state lives in components (per-entity) or + explicit module registries the game owns, so save/load and rollback capture it. +- **No float.** Everything is Q16.16 fixed / integer, so lockstep and replay hold.