docs(api): per-symbol pages, fuzzy search, deep token linking, hover cards
All checks were successful
docs / build-and-deploy (push) Successful in 2s

Rebuild the API Reference around one page per symbol and richer, verified content.

Pages & navigation
- One HTML page per symbol (kw-*, type-*, phase-*, screen-*, fn-*, annot-*, op-*)
  instead of a single scrolling page; namespace overview pages (ns-screen …
  ns-color) and a searchable index (api.html) with client-side fuzzy search.
- Sticky-header scroll offset (scroll-margin) so a jumped-to entry/param/color is
  never hidden, plus a flash highlight on the scrolled-to target.

Deep linking in every snippet & example
- Namespace members split: `Screen`→namespace page, `fill_rectangle`→method page;
  `Color`→palette page, `Charcoal`→its swatch — separately.
- Named arguments (`width:`) link to that parameter's anchor on the method page.
- Hover any token for a summary card built from the real API data (symbols.json).

Content & coverage
- Full authoritative surface documented from the compiler: every keyword, type,
  the 6 phases (Start/Input/FixedUpdate/Update/LateUpdate/Render, each its own
  page), all 22 annotations, namespace methods with parameter docs, builtins,
  the world_* reflection ABI, networking, operators — 155 symbols.
- Longer, clearer explanations; "model"/"model instance" terminology, not "entity";
  descriptive identifiers in every example (Position{column,row}, Velocity{delta_x,
  delta_y}, Health{current,maximum}, Player/Enemy) — no Pos/Seg/x/dx.
- Accuracy fixes from compiler ground-truth: world_count() takes no arg,
  world_query_next(property, cursor) arg order, event fields bind by name; dropped
  `when` and `module` (not in the self-hosted parser).

Tooling
- inventory.json + check.py: coverage guard (every symbol has a page), duplicate-
  token guard, and broken-link guard — fail CI so docs can't drift.
- validate.py: compiles every ```ludic example against bin/ludicc (158 compile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-29 17:53:22 +03:00
parent 25f987e30d
commit 3c7ec9b016
172 changed files with 5240 additions and 895 deletions

View file

@ -5,8 +5,30 @@ category: annotations
kind: annotation
tokens: @Computed
sig: @Computed on a property field
tip: A derived field: an expression expanded inline wherever it is read, never stored.
tip: A derived field expanded inline wherever it is read, never stored.
order: 1
---
A derived field: an expression expanded inline wherever it is read, never stored.
<code>@Computed</code> marks a property field as <b>derived</b>: instead of occupying a slot in the world, its expression is expanded inline at every site that reads it, so it is recomputed on demand and never takes storage. Use it for values that are a pure function of other fields — a total, a midpoint, a scaled amount — where storing them would risk going stale. Because a computed field has no backing storage it cannot be assigned to, and it is not something you replicate: it costs nothing on the wire and is simply recomputed on each peer. Reach for it when a value should always be consistent with its inputs and you would otherwise have to remember to recompute it by hand.
```ludic
program DerivedStats {
property Health {
current: int = 100,
maximum: int = 100,
@Computed missing: int = maximum - current # never stored; expanded where read
}
model Player { Health }
handler ReportDamage phase Update {
for (Health) in query [Health] {
Health.current = 70
print(Health.missing) # 30 — recomputed from current and maximum
}
}
handler SpawnPlayer phase Start {
spawn Player { Health { current: 100, maximum: 100 } }
}
}
```

View file

@ -5,8 +5,24 @@ category: annotations
kind: annotation
tokens: @export
sig: @export fn name(…) -> R
tip: Expose a function as a C-ABI symbol from a module.
tip: Expose a function as a C-ABI symbol so a host can call it.
order: 3
---
Expose a function as a C-ABI symbol from a <code>module</code>.
<code>@export</code> makes a <code>fn</code> visible outside the module as a plain C-ABI symbol, so a host program or another linked object can call it by name. Without it, functions are internal to the compiled unit; with it, the emitted symbol is externally linkable, which is how Ludic hands entry points to a runtime seam or a foreign caller. It is the outbound counterpart to <code>extern fn</code>, which pulls a foreign symbol in. Keep exported signatures to POD scalars and pointers, since they cross a C boundary where Ludic's richer types do not apply.
```ludic
program ScoreModule {
var running_total: int = 0
@export fn add_points(amount: int) -> int { # callable from a C-ABI host
running_total = running_total + amount
return running_total
}
entry {
print(add_points(10)) # 10
print(add_points(5)) # 15
}
}
```

View file

@ -0,0 +1,33 @@
---
id: annot-handles
name: @Handles
category: annotations
kind: annotation
tokens: @Handles
sig: @Handles(Event) handler Name { … }
tip: Declarative hint naming the event or subsystem a handler is responsible for.
order: 50
---
<code>@Handles</code> is a declarative annotation that names the event or subsystem a handler (or a whole program) is responsible for. The parser accepts and records it as metadata for analysis and tooling — it does not by itself wire up dispatch, so it documents intent rather than driving execution. When you want a handler to actually run in response to an event, use <code>@On(Event)</code>, which registers the listener; <code>@Handles</code> is the companion label that makes ownership of an event legible across a modular codebase. Keep the named symbol a real event or system so the annotation stays meaningful.
```ludic
program CombatModule {
property Health { current: int = 100 }
model Enemy { Health }
event DamageDealt { amount: int = 0 }
@Handles(DamageDealt) # documents this handler's responsibility
@On(DamageDealt) handler ApplyDamage { # @On does the actual dispatch wiring
for (Health) in query [Health, {Enemy}] {
Health.current = Health.current - amount
}
}
entry {
spawn Enemy { Health { current: 100 } }
emit DamageDealt(amount: 25)
}
}
```

View file

@ -1,12 +1,31 @@
---
id: annot-on
name: @OnEnable / @OnDisable / @OnDespawn
name: @On
category: annotations
kind: annotation
tokens: @OnEnable @OnDisable @OnDespawn
sig: @OnDisable(Prop) handler … { … }
tip: Lifecycle hooks that fire when a property/model is toggled or an entity is torn down.
tokens: @On
sig: @On(Event) handler Name { … }
tip: Register a compile-time listener that runs whenever an event is emitted.
order: 2
---
Lifecycle hooks that fire when a property/model is toggled or an entity is torn down.
<code>@On(Event)</code> turns a handler into a listener for a named <code>event</code>: whenever any code runs <code>emit Event(...)</code>, this handler's body runs with the event's fields bound by name. It is how systems talk to each other without referencing one another directly — the emitter never knows who is listening, so gameplay, UI and mods can all react to the same moment independently. Several handlers may listen to one event; they run in declaration order. On a <code>cancellable event</code>, a listener may <code>cancel</code> to veto it, and the emitter sees that outcome. Related lifecycle hooks such as <code>@OnSpawn</code>, <code>@OnEnable</code> and <code>@OnDespawn</code> are specialized event listeners the compiler wires up for you.
```ludic
program Events {
property Health { current: int = 100, maximum: int = 100 }
model Player { Health }
event Damaged { amount: int = 0 }
handler SpawnPlayer phase Start {
spawn Player { Health { current: 100, maximum: 100 } }
emit Damaged(amount: 25)
}
@On(Damaged)
handler ReportDamage {
print(amount)
}
}
```

View file

@ -0,0 +1,33 @@
---
id: annot-onattach
name: @OnAttach
category: annotations
kind: annotation
tokens: @OnAttach
sig: @OnAttach(Property) handler Name { … }
tip: Run a handler when a property is structurally attached to a live instance.
order: 50
---
<code>@OnAttach(Property)</code> fires when the named property is <b>structurally added</b> to a live model instance with <code>attach P on e</code> — the property's fields are seeded first, then the hook runs in the context of that instance so it can read and adjust them. This is the structural birth of a property, the counterpart to <code>@OnDetach</code>; it differs from <code>@OnEnable</code>, which only un-pauses a property that already exists. Use it to react when a capability appears on an instance at runtime — a shield goes up, a status effect lands. The property name is a checked reference. Add <code>@Public</code> to also emit a <code>prop_&lt;Property&gt;_attach</code> event.
```ludic
program AttachHook {
property Tag { value: int = 0 }
property Shield { amount: int = 0 }
model Unit { Tag }
@OnAttach(Shield) handler RaiseShield { # fires when Shield is attached
print(Shield.amount + 10) # 15 — reads the seeded amount
}
handler Seed phase Start { spawn Unit { Tag { value: 1 } } }
handler AddShield phase Render {
for (unit) in query [Unit] {
attach Shield on self() { amount: 5 } # triggers @OnAttach
}
quit()
}
}
```

View file

@ -0,0 +1,34 @@
---
id: annot-ondespawn
name: @OnDespawn
category: annotations
kind: annotation
tokens: @OnDespawn
sig: @OnDespawn(Model, reason: r) handler Name { … }
tip: Run a handler when a model instance is torn down, optionally knowing why.
order: 50
---
<code>@OnDespawn(Model)</code> is the teardown counterpart to <code>@OnSpawn</code>: it fires once for each instance of the named model as it is removed, whether by an in-world <code>despawn</code> or at program shutdown, and its body still reads the instance's outgoing field values before they are gone. With the optional <code>reason: r</code> binding it becomes reason-carrying teardown — <code>r</code> is bound to an <code>EndReason</code> the compiler passes at each teardown site (an in-world despawn passes <code>EndReason.Despawned</code>, program exit passes <code>EndReason.Quit</code>) so one hook can branch on <b>why</b> the instance is ending, the way Unreal's <code>EndPlay(reason)</code> does. Every still-live instance's hook fires at quit, which makes "no silent deaths" real: drop loot on a real death but skip it when the app is simply closing. Add <code>@Public</code> to also emit a <code>model_&lt;Model&gt;_despawn</code> event.
```ludic
program DespawnHook {
property Health { current: int = 0 }
property Loot { gold: int = 0 }
model Enemy { Health, Loot }
@OnDespawn(Enemy, reason: teardown_reason) handler DropLoot {
match teardown_reason {
EndReason.Quit => {} # app closing — do not drop loot
_ => { print(Loot.gold) } # died in-world — award the gold
}
}
handler SpawnWave phase Start { spawn Enemy { Health { current: 10 }, Loot { gold: 25 } } }
handler KillOne phase Render {
for (Health) in query [Health, {Enemy}] { despawn self() } # prints 25
quit()
}
}
```

View file

@ -0,0 +1,36 @@
---
id: annot-ondetach
name: @OnDetach
category: annotations
kind: annotation
tokens: @OnDetach
sig: @OnDetach(Property) handler Name { … }
tip: Run a handler when a property is structurally detached from a live instance.
order: 50
---
<code>@OnDetach(Property)</code> is the teardown counterpart to <code>@OnAttach</code>: it fires when the named property is structurally removed from a live instance with <code>detach P on e</code>. The hook runs <b>before</b> the property's presence flag clears, so its body can still read the outgoing field values one last time — useful for releasing whatever the property was tracking or logging its final state. It is distinct from <code>@OnDisable</code>, which pauses a property while keeping its data; detach actually destroys the property's presence on the instance. Add <code>@Public</code> to also emit a <code>prop_&lt;Property&gt;_detach</code> event.
```ludic
program DetachHook {
property Tag { value: int = 0 }
property Shield { amount: int = 0 }
model Unit { Tag }
@OnDetach(Shield) handler DropShield { # fires as Shield is removed
print(Shield.amount + 20) # 25 — reads the outgoing amount
}
handler Seed phase Start {
spawn Unit { Tag { value: 1 } }
for (unit) in query [Unit] { attach Shield on self() { amount: 5 } }
}
handler RemoveShield phase Render {
for (shield) in query [Shield] {
detach Shield on self() # triggers @OnDetach
}
quit()
}
}
```

View file

@ -0,0 +1,33 @@
---
id: annot-ondisable
name: @OnDisable
category: annotations
kind: annotation
tokens: @OnDisable
sig: @OnDisable(Property) handler Name { … }
tip: Run a handler when a property is paused (disabled) on an instance.
order: 50
---
<code>@OnDisable(Property)</code> fires when a present property is paused with <code>disable P on e</code>. Disabling keeps the property's data intact and simply stops it from participating — the logical "off" switch — so this hook is the moment a capability goes dormant, not the moment it is destroyed (that is <code>@OnDetach</code>). The body runs in the instance's context and can read the fields before they go quiet, which is handy for cleaning up an effect the property was driving. It pairs with <code>@OnEnable</code> for capabilities that toggle on and off repeatedly. Add <code>@Public</code> to also emit a <code>prop_&lt;Property&gt;_disable</code> event.
```ludic
program DisableHook {
property Tag { value: int = 0 }
property Shield { amount: int = 0 }
model Unit { Tag, Shield }
@OnDisable(Shield) handler ShieldDown { # fires when Shield is disabled
print(Shield.amount) # 5 — data is retained, just paused
}
handler Seed phase Start { spawn Unit { Tag { value: 1 }, Shield { amount: 5 } } }
handler Deactivate phase Render {
for (unit) in query [Unit] {
disable Shield on self() # triggers @OnDisable
}
quit()
}
}
```

View file

@ -0,0 +1,34 @@
---
id: annot-onenable
name: @OnEnable
category: annotations
kind: annotation
tokens: @OnEnable
sig: @OnEnable(Property) handler Name { … }
tip: Run a handler when a paused property is re-enabled on an instance.
order: 50
---
<code>@OnEnable(Property)</code> fires when a property that is present but disabled is turned back on with <code>enable P on e</code>. Unlike <code>@OnAttach</code>, the property's data was never destroyed — enable/disable only pause and resume a property that already exists, keeping its fields intact — so this hook is about a capability becoming active again rather than being created. The body runs in the instance's context and can read the retained field values. Use the enable/disable pair for a capability that toggles repeatedly (a power-up, a stun) where re-seeding on every toggle would be wrong. Add <code>@Public</code> to also emit a <code>prop_&lt;Property&gt;_enable</code> event.
```ludic
program EnableHook {
property Tag { value: int = 0 }
property Shield { amount: int = 0 }
model Unit { Tag, Shield }
@OnEnable(Shield) handler ShieldBackOnline { # fires when Shield is re-enabled
print(Shield.amount) # 5 — the retained amount, not re-seeded
}
handler Seed phase Start {
spawn Unit { Tag { value: 1 }, Shield { amount: 5 } }
for (unit) in query [Unit] { disable Shield on self() }
}
handler Reactivate phase Render {
for (unit) in query [Unit] { enable Shield on self() } # triggers @OnEnable
quit()
}
}
```

View file

@ -0,0 +1,27 @@
---
id: annot-onquit
name: @OnQuit
category: annotations
kind: annotation
tokens: @OnQuit
sig: @OnQuit handler Name { … }
tip: Run a handler once at shutdown, after the last frame.
order: 50
---
<code>@OnQuit</code> pins a handler to the program's shutdown moment: it runs exactly once as the program exits, after the final frame. Use it for teardown that must happen once — flushing a save, printing a final tally, releasing a host resource. In the lifecycle timeline it runs after every still-live instance's <code>@OnDespawn</code> hook has fired, so the world has already been torn down by the time <code>@OnQuit</code> sees it. It is the boot-time counterpart of <code>@OnStart</code>; add <code>@Public</code> to also emit a <code>program_quit</code> event other modules can hook.
```ludic
program QuitHook {
property Score { total: int = 0 }
model Scoreboard { Score }
@OnQuit handler ReportFinalScore { # runs once at shutdown
for (Score) in query [Score] { print(Score.total) }
}
handler Seed phase Start { spawn Scoreboard { Score { total: 42 } } }
handler Finish phase Render { quit() } # triggers shutdown -> prints 42
}
```

View file

@ -0,0 +1,28 @@
---
id: annot-onspawn
name: @OnSpawn
category: annotations
kind: annotation
tokens: @OnSpawn
sig: @OnSpawn(Model) handler Name { … }
tip: Run a handler once each time an instance of a model is spawned.
order: 50
---
<code>@OnSpawn(Model)</code> turns a handler into a birth hook: it fires once for each newly <code>spawn</code>ed instance of the named model, right after its properties are seeded. The body runs in the context of that fresh instance, so it can read and adjust the instance's fields by property name to finish initialization — seed derived state, register the instance, play a spawn effect. The model name is a checked reference, so a typo is a compile error rather than a silently dead hook. Its teardown counterpart is <code>@OnDespawn</code>; add <code>@Public</code> to also emit a <code>model_&lt;Model&gt;_spawn</code> event other modules can listen for.
```ludic
program SpawnHook {
property Health { current: int = 0, maximum: int = 100 }
model Enemy { Health }
@OnSpawn(Enemy) handler InitEnemy { # fires per new Enemy instance
Health.current = Health.maximum # start every enemy at full health
print(Health.current) # 100
}
handler SpawnWave phase Start {
spawn Enemy { Health { current: 0, maximum: 100 } }
}
}
```

View file

@ -0,0 +1,28 @@
---
id: annot-onstart
name: @OnStart
category: annotations
kind: annotation
tokens: @OnStart
sig: @OnStart handler Name { … }
tip: Run a handler once at boot instead of assigning it a frame phase.
order: 50
---
<code>@OnStart</code> pins a handler to the program's boot moment: it runs exactly once when the program starts, before the per-frame phases begin, instead of being placed in a recurring phase. Use it for one-time setup — spawning the initial world, seeding program vars, loading a scene. It overrides the handler's phase, so you write <code>@OnStart handler Name { … }</code> rather than <code>handler Name phase Start</code> and get the same once-at-boot placement through the annotation channel. Its shutdown counterpart is <code>@OnQuit</code>; add <code>@Public</code> to also emit a <code>program_start</code> event other modules can hook.
```ludic
program BootHook {
property Health { current: int = 0, maximum: int = 100 }
model Player { Health }
var elapsed_frames: int = 0
@OnStart handler CreateWorld { # runs once at boot
spawn Player { Health { current: 100, maximum: 100 } }
elapsed_frames = 0
}
handler CountFrames phase Update { elapsed_frames = elapsed_frames + 1 }
}
```

View file

@ -0,0 +1,29 @@
---
id: annot-owned
name: @Owned
category: annotations
kind: annotation
tokens: @Owned
sig: @Owned model Name { … }
tip: Give a model a network owner slot so its instances can be assigned to a peer.
order: 50
---
<code>@Owned</code> declares that every instance of a model carries a network owner — it adds an owner slot to the model (the runtime's <code>@L_owner</code> array), which the builtins <code>owner</code>, <code>set_owner</code>, and <code>is_owner</code> read and assign. A fresh instance starts unowned (<code>-1</code>) until the authority assigns it. Ownership is what gates who may write <code>@Sync(to: owner)</code> fields and who runs <code>@Predicted</code> control handlers, and it is stored in the world snapshot so it round-trips through replication and rollback. Mark the models that represent a player's avatar or units; leave shared scenery unowned.
```ludic
program OwnedModel {
@Sync property Position { column: int = 0, row: int = 0 }
@Owned model Player { @Sync Position } # each Player instance has an owner slot
entry {
spawn Player { Position { column: 5, row: 6 } }
for (Position) in query [Position, {Player}] {
let player_id = self()
print(owner(player_id)) # -1 — unowned until assigned
set_owner(player_id, 0)
print(is_owner(player_id)) # 1 — the local peer now owns it
}
}
}
```

View file

@ -0,0 +1,30 @@
---
id: annot-predicted
name: @Predicted
category: annotations
kind: annotation
tokens: @Predicted
sig: @Predicted handler Name { … }
tip: Run a control handler on the owning client speculatively and on the server authoritatively.
order: 50
---
<code>@Predicted</code> marks a control handler that runs in two places: speculatively on the client that <b>owns</b> the affected instance, so local input feels instant, and authoritatively on the server, whose result reconciles the client if the two diverge. It is the responsive-control role from Quake-style client-prediction, named for the netcode behavior (owner-predicts plus server-authoritative plus reconcile) rather than the machine, and it matches Unity's <code>GhostMode.Predicted</code> so the concept transfers. Prediction is explicit — Ludic never silently predicts — and it only applies to instances of an <code>@Owned</code> model, since the dispatch reads <code>is_owner</code> to decide whether the local client should run it. Use it for the owning player's movement and actions; leave authority-only rules on <code>@Server</code>.
```ludic
program PredictedMovement {
@Sync property Position { column: int = 0, row: int = 0 }
@Owned model Player { @Sync Position }
@Predicted handler MovePlayer phase Input { # owner predicts, server reconciles
let pressed = Input.key()
for (Position) in query [Position, {Player}] {
if pressed == 'd' { Position.column = Position.column + 1 }
}
}
handler SpawnPlayer phase Start {
spawn Player { Position { column: 0, row: 0 } }
}
}
```

View file

@ -0,0 +1,31 @@
---
id: annot-public
name: @Public
category: annotations
kind: annotation
tokens: @Public
sig: @Public @OnSpawn(Model) handler … { … }
tip: Promote a lifecycle hook to a public event other modules can listen for.
order: 50
---
<code>@Public</code> stacks in front of a lifecycle hook to also emit a well-named event at the same moment the hook fires, so other modules (or mods) can react without touching the original code. It turns a private hook into a public seam: <code>@Public @OnSpawn(Model)</code> emits <code>model_&lt;Model&gt;_spawn</code>, <code>@OnDespawn</code> emits <code>model_&lt;Model&gt;_despawn</code>, the property hooks emit <code>prop_&lt;Property&gt;_attach</code>/<code>_detach</code>/<code>_enable</code>/<code>_disable</code>, and <code>@OnStart</code>/<code>@OnQuit</code> emit <code>program_start</code>/<code>program_quit</code>. This is the same lowering the event system uses, so anyone can <code>@On</code> the generated event to extend behavior at that lifecycle point. Add it when a hook marks an extension point you want to keep open across a modular codebase.
```ludic
program PublicSpawn {
property Health { current: int = 0, maximum: int = 100 }
model Enemy { Health }
@Public @OnSpawn(Enemy) handler InitEnemy { # also emits model_Enemy_spawn
Health.current = Health.maximum
}
@On(model_Enemy_spawn) handler AnnounceEnemy { # another module hooks the public event
print(1) # 1 — reacts to every Enemy spawn
}
handler SpawnWave phase Start {
spawn Enemy { Health { current: 0, maximum: 100 } }
}
}
```

View file

@ -4,9 +4,27 @@ name: @Queries
category: annotations
kind: annotation
tokens: @Queries
sig: @Queries(these: [Pos, Vel])
tip: Declare the properties a handler touches, binding their fields by name in the body.
sig: @Queries(these: [Position, Velocity])
tip: Declare the properties a handler operates on, binding their fields by name in the body.
order: 0
---
Declare the properties a handler touches, binding their fields by name in the body.
<code>@Queries</code> lets a handler declare the properties it operates on up front, and the compiler wraps the whole body in a query loop over the matching model instances. The body then runs once per match with each listed property bound by name — you write <code>Position.column</code> directly instead of opening a <code>for (…) in query […]</code> yourself — and <code>self()</code> gives the current instance. It takes the same shape as a manual query: <code>these: [ … ]</code> lists the required properties (each may carry a filter like <code>Health{current &lt;= 0}</code>), and an optional <code>on: Model</code> restricts the match to instances of one model. Reach for it when a handler's entire job is "for every matching instance, do this"; drop to an explicit <code>query</code> when you need finer control inside the loop.
```ludic
program AdvanceMovement {
property Position { column: int = 0, row: int = 0 }
property Velocity { delta_x: int = 0, delta_y: int = 0 }
model Player { Position, Velocity }
@Queries(these: [Position, Velocity], on: Player) # body runs once per matching Player
handler AdvancePositions phase FixedUpdate {
Position.column = Position.column + Velocity.delta_x
Position.row = Position.row + Velocity.delta_y
}
handler SpawnPlayer phase Start {
spawn Player { Position { column: 0, row: 0 }, Velocity { delta_x: 1, delta_y: 0 } }
}
}
```

View file

@ -0,0 +1,31 @@
---
id: annot-reads
name: @Reads
category: annotations
kind: annotation
tokens: @Reads
sig: @Reads(Property)
tip: Declare that a handler reads a property — an analysis and scheduling hint.
order: 50
---
<code>@Reads(Property)</code> declares that a handler reads the named property. It is an analysis hint: it documents a handler's data dependencies so tooling and scheduling can reason about which handlers touch which state — for example, to tell whether two handlers can run without conflict. It does not itself bind fields the way <code>@Queries</code> does, nor does it grant access; it annotates intent alongside the handler's actual query. Pair it with <code>@Writes</code> to spell out the full read/write footprint of a handler. Keep the named property real so the declared footprint matches what the body does.
```ludic
program ReadFootprint {
property Health { current: int = 100 }
property Score { total: int = 0 }
model Player { Health, Score }
@Reads(Health) # declares the read dependency
handler ReportLowHealth phase Update {
for (Health) in query [Health, {Player}] {
if Health.current < 25 { print(Health.current) }
}
}
handler SpawnPlayer phase Start {
spawn Player { Health { current: 20 }, Score { total: 0 } }
}
}
```

View file

@ -0,0 +1,31 @@
---
id: annot-server
name: @Server
category: annotations
kind: annotation
tokens: @Server
sig: @Server handler Name { … }
tip: Run a handler only on the authority; clients receive the result via @Sync.
order: 50
---
<code>@Server</code> marks a handler as server-authoritative: it runs only on the peer acting as the authority, and clients receive its effects through ordinary <code>@Sync</code> replication rather than by running the handler themselves. This is the declarative alternative to sprinkling <code>is_server()</code> branches through gameplay code — the role is a property of the handler, so every line's placement stays legible. Unmarked handlers are the shared, deterministic simulation that runs everywhere; use <code>@Server</code> for decisions that must have a single source of truth, like awarding score, resolving damage, or spawning authoritative entities. Offline the role register defaults to server, so the guard collapses to "run here" and a single-player build is unchanged.
```ludic
program AuthoritativeScore {
property Score { total: int = 0 }
model Scoreboard { Score }
handler Tick phase Update { # unmarked -> runs on every peer
for (Score) in query [Score] { Score.total = Score.total + 1 }
}
@Server handler AwardBonus phase Update { # authority only
for (Score) in query [Score] { Score.total = Score.total + 100 }
}
handler SpawnScoreboard phase Start {
spawn Scoreboard { Score { total: 0 } }
}
}
```

View file

@ -1,12 +1,29 @@
---
id: annot-sync
name: @Sync / @Owned
name: @Sync
category: annotations
kind: annotation
tokens: @Sync @Owned
tokens: @Sync
sig: @Sync property … / @Owned
tip: Mark a property's fields as replicated, and entities as owned, for networking.
tip: Mark fields as replicated and models as owned so the compiler generates networking.
order: 4
---
Mark a property's fields as replicated, and entities as owned, for networking.
These are the two declarative switches at the top of Ludic's networking: <code>@Sync</code> says <b>what</b> replicates and <code>@Owned</code> says <b>who</b> owns an instance. Replication is opt-in at the field level and decided per model use-site — a field crosses the wire only when it is both replicable (marked <code>@Sync</code>, either on the field or via <code>@Sync property P</code> which marks every field of <code>P</code>) <b>and</b> participating (the model marks the component <code>@Sync</code>). So the same property can replicate in one model and not another, and there is no <code>@NoSync</code> because the surface is purely additive. From these marks the compiler generates the per-model <code>serialize</code>/<code>apply</code> codecs; <code>@Sync</code> on a non-POD-scalar field (like a <code>ptr</code>) is a compile error, since a machine-local pointer cannot cross the wire. <code>@Owned</code> adds the owner slot the ownership builtins and <code>@Predicted</code> read. See the dedicated <code>@Owned</code> page for ownership details.
```ludic
program SyncedWorld {
@Sync property Position { column: int = 0, row: int = 0 } # every field replicable
property Health { @Sync current: int = 0, maximum: int = 0 } # only current replicable
@Owned model Player { @Sync Position, @Sync Health } # participates -> column,row,current
model Scenery { Position } # not @Sync here -> never replicates
entry {
spawn Player { Position { column: 3, row: 4 }, Health { current: 50, maximum: 100 } }
for (Position, Health) in query [Position, Health, {Player}] {
print(sync_size(self())) # 12 = Position(8) + Health.current(4)
}
}
}
```

View file

@ -0,0 +1,32 @@
---
id: annot-toclients
name: @ToClients
category: annotations
kind: annotation
tokens: @ToClients
sig: @ToClients event Name { … }
tip: A remote event broadcast from the server to clients — a server→clients notification.
order: 50
---
<code>@ToClients</code> marks an <code>event</code> as a directional remote event that travels from the authority down to clients — a broadcast, again the ordinary event bus with a direction flag. When the server <code>emit</code>s it, the POD payload is serialized as an event id plus its packed fields and sent through the transport seam to the clients; each client's pump re-emits it into normal <code>@On</code> dispatch, so an <code>@On(Name)</code> handler on every client reacts. Use it for authoritative notifications a client should present but not decide — an explosion happened, a round ended, a pickup was granted. Its mirror is <code>@ToServer</code>, which carries client requests the other way.
```ludic
program ExplosionBroadcast {
property EffectLog { count: int = 0 }
model Client { EffectLog }
@ToClients event Boom { column: int = 0, row: int = 0 } # server -> clients broadcast
@On(Boom) handler PlayExplosion { # every client reacts
for (EffectLog) in query [EffectLog, {Client}] {
EffectLog.count = EffectLog.count + 1
}
}
entry {
spawn Client { EffectLog { count: 0 } }
emit Boom(column: 8, row: 3) # serialized to clients, re-emitted on arrival
}
}
```

View file

@ -0,0 +1,32 @@
---
id: annot-toserver
name: @ToServer
category: annotations
kind: annotation
tokens: @ToServer
sig: @ToServer event Name { … }
tip: A remote event sent from a client to the server — a client→server request.
order: 50
---
<code>@ToServer</code> marks an <code>event</code> as a directional remote event that travels from a client up to the authority — an RPC, expressed as the ordinary event bus with a direction flag rather than a new concept. At an <code>emit</code> site the event's POD payload is serialized as an event id followed by its packed fields and sent through the transport seam toward the server; it does <b>not</b> run locally. On the far side, draining the transport (the runtime's pump) re-emits it into the normal <code>@On</code> dispatch, so a <code>@Server @On(Name)</code> handler picks it up. Use it for player intent — a move request, a fire command — that the authority must validate before it affects the world. Its mirror is <code>@ToClients</code>, which broadcasts the other way.
```ludic
program FireRequest {
property AmmoLog { shots_fired: int = 0 }
model Turret { AmmoLog }
@ToServer event Fire { direction: int = 0 } # client -> server request
@Server @On(Fire) handler HandleFire { # authority validates and applies
for (AmmoLog) in query [AmmoLog, {Turret}] {
AmmoLog.shots_fired = AmmoLog.shots_fired + direction
}
}
entry {
spawn Turret { AmmoLog { shots_fired: 0 } }
emit Fire(direction: 1) # serialized onto the wire, not run locally
}
}
```

View file

@ -0,0 +1,30 @@
---
id: annot-writes
name: @Writes
category: annotations
kind: annotation
tokens: @Writes
sig: @Writes(Property)
tip: Declare that a handler writes a property — an analysis and scheduling hint.
order: 50
---
<code>@Writes(Property)</code> declares that a handler writes the named property. Like <code>@Reads</code> it is an analysis hint that documents a handler's data footprint so tooling and scheduling can reason about conflicts — two handlers that write the same property, or one that writes what another reads, cannot be reordered freely. It does not bind fields or grant access on its own; the handler still queries and mutates the property in its body as usual. Declaring reads and writes together makes a handler's effect on the world legible at a glance, which matters most in a large, modular codebase. Keep the named property accurate so the declared footprint matches the code.
```ludic
program WriteFootprint {
property Health { current: int = 100 }
model Enemy { Health }
@Reads(Health) @Writes(Health) # full read/write footprint
handler ApplyPoison phase Update {
for (Health) in query [Health, {Enemy}] {
Health.current = Health.current - 1
}
}
handler SpawnEnemy phase Start {
spawn Enemy { Health { current: 100 } }
}
}
```