Proposal: builtin RPG systems — movement (grid/free, 4/8-axis), inventory, crafting, quests, dialog, puzzles (base + extensible) #59
Labels
No labels
area:ci
area:docs
area:input
area:net
area:rendering
area:repo
area:stdlib
area:tooling
area:types
cleanup
dx
priority:high
priority:low
priority:medium
proposal
status:in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: workshopsoft/ludic#59
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of the builtin-controllers layer (#57). Conforms to the six-lever extensibility contract. Planning only — no implementation. This is the largest family; each sub-module below can graduate to its own tracking issue once the shape is agreed.
Context
"RPG" is not one controller — it's a suite: movement (grid and free-form, 4- and 8-axis), inventory, crafting, quests, dialog, puzzles, plus the stats/damage backbone. The design goal is that each sub-module is independently usable (a puzzle game wants only the grid-movement + puzzle modules; a walking-sim wants dialog only) and each is extensible in every area via the six levers. Modeled on RPG Maker's event/database model, Godot RPG toolkits, and data-driven engines (LDtk/Tiled entity data, Ink/Yarn for dialog, flecs prefabs for item templates).
We reuse
chronorift(the existing example JRPG) as the proving ground — its hand-written movement/battle/inventory is exactly what these builtins should be able to replace, then extend.Module A — Movement (grid + free-form, 4/8-axis)
The headline "combos" the goal asked for. One component, a
modeselector, so a game picks its movement feel as data.Grid.*): discrete cell moves, walkability viaGrid/tilemap, optional tween between cells (mode 2) so it looks smooth but stays lockstep-discrete (RPG Maker / Pokémon feel).Bodyvelocity, 4- or 8-axis snapping or full analog (top-down Zelda feel), reusing #57 collision.axespicks cardinal (4) vs. 8-way; apolicyslot covers oddities (isometric, hex — hex via aGridlayout flag).esys_mover_input(Input actions → intent),esys_mover_grid/esys_mover_free(only the active one ticks),esys_mover_facing.TileEntered {e, x, y}(encounter checks, traps),cancellable MoveRequested {e, dx, dy}(locked doors, ice-sliding, cutscene lock),Interacted {e, target}(the "action button" raycast infacing).@On(MoveRequested); random encounters =@On(TileEntered)(exactly chronorift'sencounterlogic, now a listener); swap grid↔free per-scene = changemode.Module B — Inventory
Dict.*/Set.*(string→int) for item lookup by name, andReflect/worldstorage per entity. Grid/slot inventories (Diablo/Resident-Evil bag) as alayoutpolicy: flat, slots, weight-limited, grid-cells.Inventory.add(e, "potion", 3),.remove,.count,.move(from,to),.has(e,"key"); equip slots via a companionEquipment { head, body, weapon, ... }component whose stat effects flow through #57 modifier stacks.cancellable ItemPickup,ItemAdded/Removed,cancellable ItemUse {e,item},Equipped/Unequipped,InventoryFull.@On(ItemRemoved)/cancel; weight-affects-speed =@On(InventoryChanged)writing aMover.speedmodifier; custom bag layout =layoutpolicy + own draw handler.Module C — Crafting
Anim.clip's name table), data-driven so games/mods add recipes without code:Craft.recipe("sword", out:"iron_sword", in:[("iron",2),("wood",1)], station:"forge", time:60).Craft.can(e, "sword"),Craft.make(e, "sword")(consumes fromInventory, respectstimeviaCooldown), queue support.cancellable CraftRequested,CraftStarted/Completed,RecipeDiscovered(for recipe-book unlock).@On(CraftRequested)/cancel; crit-crafting (bonus output) =@On(CraftCompleted); procedural/random recipes = register at runtime (EV7-style dynamic data). Mods add recipes over the ABI.Module D — Quests
Objectives). Progress is driven entirely by events, which is the elegant part:esys_questlistens to gameplay events (ItemAdded,Death,TileEntered,DialogChose) and ticks matching objectives — so quests observe the same event bus everything else emits. No bespoke quest-tracking wiring per game.Quest.start("q1"),.advance,.complete,.state("q1"),Quest.flag("met_king")(global flag store,Dict-backed, saved).QuestStarted/Completed/Failed,ObjectiveProgressed,cancellable QuestTurnIn.@On(QuestStarted)/cancel; timed quests =Cooldown+QuestFailed; custom objective kinds = akindpolicy value + a listener that tickshave.Module E — Dialog
property Dialog { tree, node, }+esys_dialogadvances state; alayerrenders the box (reuses the retained-UIuiblocks + TrueType text).DialogStarted/Ended,DialogLine {e, node},cancellable DialogChoice {e, choice},DialogEffect {e, key}(fires quest flags/item grants declaratively).@On(DialogLine); skill-check choices =@On(DialogChoice)/cancelwhen stat too low; localization = swap the tree registry; branching on world state = condition predicates evaluated against the flag/worldstore.Module F — Puzzles
A small toolkit of the recurring 2D puzzle primitives, each a component + engine system + events, composable on the grid mover:
property Pushable(Sokoban blocks) —esys_pushresolves push chains on grid moves;BlockPushed/cancellable PushBlocked.property Pressure/property Switch/property Gate— a signal graph: switches emitSignalChanged, gates listen; wiring is data (Switch.link("s1","gate1")), so logic puzzles need no code.property Pushable+Pressure= classic weighted-plate puzzle out of the box.Grid.a_star-backed "is this solvable" check for procedural puzzle gen.@On(PushBlocked)policy.Module G — Stats / combat backbone (shared)
Pulled from #57:
Stats+ modifier stacks,Damage/Heal/Deathevents, status effects (property Status { kind, stacks, ttl }+esys_statusticking durations viaCooldown). Turn-based and real-time battle are both just policies over this (chronorift's turn machine becomes one shipped policy). Damage formula is apolicyenum + acancellable DamageAboutToApply {e, src, amount*}(mutable) event — the single most-overridden hook in any RPG.Extensibility summary (the whole family)
Every module is: a POD component bundle (lever 1 data) + engine systems each disable-able (lever 5) + a name-keyed data registry for content (items/recipes/quests/dialog — mods add content with zero code, EV7-style) + cancellable events at each decision (lever 4) + a
policyfield where behavior is a formula (lever 6) + free composition with the game's own components (lever 2). A game can adopt one module (just dialog) or all seven, and replace any sub-system without forking.Ludic ties
Anim.clipname-table pattern andDict/Set; flags/journal/inventory persist viaworld_save(N1) → save games + rollback for free.chronorift.ludicis the migration target: re-implement it on these builtins to prove parity, then show a 20-line extension the hand-written version couldn't do cheaply.Phasing
examples/games/rpg.ludic; chronorift parity migration.References
Amendment — ships as an external package
Per the decision on #57, this controller ships as an external, versioned source package (Ludic modules, compiled into the consumer's binary), not as compiler/
runtime/nativestdlib and not as a precompiled OS binary. Rationale (wasm target, determinism, compile-time ECS) and the binary-mod escape hatch are on #57.Depends on #62 (package distribution + package-declarable namespaces/components/engine-systems) — the mechanism that lets this register its component schema and engine-owned systems without a compiler edit. The six-lever extensibility contract it conforms to stays in the language core.
Shipped —
ludic.rpg, all seven modules (commit46c275c)The RPG suite as a source package on the six-lever contract (#57). Each module is independently usable and content is name-keyed data registries, so a game or mod adds items/recipes/quests/dialog with zero code.
Moverwith amodeselector (grid / free / grid-tween) and 4/8-axis, tilemap walkability, andcancellable MoveRequested(locked doors/ice) /TileEntered(encounters) /Interacted(the action-button raycast).ItemUseveto, andEquipmentwhose bonuses flow through the gameplay Stats modifier stack.Craft.can/Craft.makeconsume from the inventory.Quest.notify(route any gameplay signal in), auto-completing when met, plus a global flag store for branching.Dialogcomponent andcancellable DialogChoicefor skill-check gating.Pushable+ a switch / pressure-plate / gate signal graph — logic puzzles with no code.Combatpipeline (armor, veto andDiedall still apply); timed stat buffs are the gameplay modifier stack.Everything is integer-deterministic, so save/load (
world_save), replay and rollback hold. Exampleexamples/games/rpg_demo.ludicexercises all seven modules with 21 self-checks (grid move + wall block + MoveRequested veto + interact, inventory + equipment stat bonus, crafting, event-driven quest auto-complete, a dialog graph walk, a pushable + a pressure-plate/gate signal, a poison status) — a regression case inx test(103/0). Docs:docs/CONTROLLERS.md.Closing as done.