ludic/docs/CONTROLLERS.md
Orkuncakilkaya e175619543 refactor(cli)!: split the contributor tool out of the ludic CLI
`ludic help` ended with a section titled "contributing to the toolchain itself",
listing bootstrap, reseed, docs-gen and release tasks. None of that is available
to someone who installed the language — those tasks need the repository — so the
shipped tool was advertising work its user cannot do, in a namespace they have to
read past to find `new` and `run`.

The tasks move to a second program, dev.ludic -> bin/ludic-dev, built from a
checkout and excluded from every release artifact. `ludic` keeps the project and
package commands and nothing else; `ludic dev …` now explains where the tasks
went instead of failing as an unknown command.

What this shook out: the two programs share prelude/build/project/pkg, so the
helpers each had accreted in whichever file first needed them — cc(),
ensure_ludicc, the string functions, title_case, cmd_version — moved to where
both can see them. The argument-shift indirection added for the `dev` namespace
is gone with the namespace, so commands read argv directly again.

`ludic-dev test` asserts the split rather than trusting it: the staged install
must build a project, and `ludic dev build` there must fail while naming
ludic-dev. install.sh keeps building older tags, whose bootstrap goes through
main.ludic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:15:12 +03:00

85 lines
6.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.core** | #77, #85 | The canonical engine-ABI components the engine systems read by name — `Position`, `Body`, `Collider`, `Solids`, and `Sprite` — so a game imports them instead of hand-declaring the bundles. Declaring `Sprite` (id/offset/scale/flip/tint/hidden) makes the engine sprite-render system auto-draw the entity each Render frame (adding the SpriteAnim frame when present). Extend by composition (attach your own components alongside). |
| **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 `ludic 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 `ludic-dev 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.