ludic/docs/CONTROLLERS.md
Orkuncakilkaya 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

6.3 KiB
Raw Blame History

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). 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.