docs(api): per-symbol pages, fuzzy search, deep token linking, hover cards
All checks were successful
docs / build-and-deploy (push) Successful in 2s
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:
parent
25f987e30d
commit
3c7ec9b016
172 changed files with 5240 additions and 895 deletions
31
docs/language/ecs/fn-world_attach_dyn.md
Normal file
31
docs/language/ecs/fn-world_attach_dyn.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
id: fn-world_attach_dyn
|
||||
name: world_attach_dyn
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_attach_dyn
|
||||
sig: world_attach_dyn(target, property)
|
||||
tip: Attach a property to an instance by numeric id at runtime (dynamic ECS).
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_attach_dyn</code> adds a property to a live instance by numeric id, the reflection-ABI counterpart of the static `attach Prop on instance`. It is the tool a mod uses to grant a runtime-registered property (from `world_register_prop`) — or any property known only by id — to an instance, after which the instance's fields can be written with `world_set`. It returns nothing and sets the instance's has-flag for that property. Pair it with `world_detach_dyn` to remove. In ordinary compiled code, prefer the `attach` statement, which also fires `@OnAttach` hooks and seeds field overrides.
|
||||
|
||||
Parameters:
|
||||
- `target` — the model instance to modify, an `entity` handle
|
||||
- `property` — the property id to attach, from `world_prop_id` or `world_register_prop`
|
||||
|
||||
```ludic
|
||||
program GrantComponent {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 4, row: 4 } }
|
||||
let stamina_property = world_register_prop("Stamina", 1)
|
||||
for (position) in query [Position, {Player}] {
|
||||
world_attach_dyn(self(), stamina_property)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
31
docs/language/ecs/fn-world_count.md
Normal file
31
docs/language/ecs/fn-world_count.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
id: fn-world_count
|
||||
name: world_count
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_count
|
||||
sig: world_count() -> int
|
||||
tip: The total number of live model instances in the world.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_count</code> returns the total number of live model instances in the world, as an `int` — a quick census without walking the world yourself. It takes no arguments and counts every spawned instance regardless of which properties it carries, so it is handy for a mod or debug overlay that wants a running tally of everything alive using the reflection ABI rather than a `query` loop. To resolve names to numeric ids for the other reflection calls, see `world_prop_id`; to read a specific instance's fields by id, see `world_get`.
|
||||
|
||||
```ludic
|
||||
program Census {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Enemy { Position }
|
||||
|
||||
handler SpawnEnemies phase Start {
|
||||
spawn Enemy { Position { column: 3, row: 3 } }
|
||||
spawn Enemy { Position { column: 9, row: 3 } }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
let alive = world_count()
|
||||
Screen.draw_number(x: 8, y: 8, value: alive, color: Color.Gold, scale: 2)
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
36
docs/language/ecs/fn-world_detach_dyn.md
Normal file
36
docs/language/ecs/fn-world_detach_dyn.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
id: fn-world_detach_dyn
|
||||
name: world_detach_dyn
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_detach_dyn
|
||||
sig: world_detach_dyn(target, property)
|
||||
tip: Detach a property from an instance by numeric id at runtime (dynamic ECS).
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_detach_dyn</code> removes a property from a live instance by numeric id, the reflection-ABI counterpart of the static `detach Prop on instance`. It clears the instance's has-flag for that property so queries and `world_has` no longer see it, and returns nothing. Use it to take back a runtime-registered or id-only property a mod previously granted with `world_attach_dyn`. In ordinary compiled code, prefer the `detach` statement, which also fires the `@OnDetach` hook before the flag clears.
|
||||
|
||||
Parameters:
|
||||
- `target` — the model instance to modify, an `entity` handle
|
||||
- `property` — the property id to detach, from `world_prop_id` or `world_register_prop`
|
||||
|
||||
```ludic
|
||||
program RevokeComponent {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 4, row: 4 } }
|
||||
}
|
||||
|
||||
handler DropStamina phase Update {
|
||||
let stamina_property = world_prop_id("Stamina")
|
||||
if stamina_property >= 0 {
|
||||
for (position) in query [Position, {Player}] {
|
||||
world_detach_dyn(self(), stamina_property)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
33
docs/language/ecs/fn-world_field_id.md
Normal file
33
docs/language/ecs/fn-world_field_id.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
id: fn-world_field_id
|
||||
name: world_field_id
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_field_id
|
||||
sig: world_field_id(property, name) -> int
|
||||
tip: Resolve a field name within a property to its numeric index.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_field_id</code> resolves a field name <b>within</b> a given property to its numeric index, which is what `world_get` and `world_set` use to pick which field to read or write. You pass a property id (from `world_prop_id`) and the field's name, and get back its index (negative if the property has no such field). Together the trio `world_prop_id` → `world_field_id` → `world_get`/`world_set` lets a mod or tool touch any field on any instance entirely by name, without the property being known at compile time.
|
||||
|
||||
Parameters:
|
||||
- `property` — the property id, from `world_prop_id`
|
||||
- `name` — the field's name within that property, e.g. `"column"`
|
||||
|
||||
```ludic
|
||||
program ResolveField {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 3, row: 3 } }
|
||||
}
|
||||
|
||||
handler Inspect phase Update {
|
||||
let position_property = world_prop_id("Position")
|
||||
let column_field = world_field_id(position_property, "column")
|
||||
Screen.status(str(column_field))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,12 +1,37 @@
|
|||
---
|
||||
id: fn-world_get
|
||||
name: world_get / world_set / world_has …
|
||||
name: world_get
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_get world_set world_has world_count world_spawn
|
||||
sig: world_get(entity, prop, field) -> int
|
||||
tip: The reflection ABI: read and write the world by numeric id, for tools and mods.
|
||||
order: 8
|
||||
tokens: world_get
|
||||
sig: world_get(target, property, field) -> int
|
||||
tip: Read one field of a model instance by numeric id (reflection ABI).
|
||||
order: 50
|
||||
---
|
||||
|
||||
The reflection ABI: read and write the world by numeric id, for tools and mods. See also world_count, world_spawn.
|
||||
<code>world_get</code> reads a single field of a model instance entirely by numeric id, returning its value as an `int`. It is the id-based counterpart to the ordinary `position.column` field access that queries give you — used when the property is not known at compile time, as in a mod or a generic tool. You supply the target instance (an `entity`), the property id (from `world_prop_id`), and the field id (from `world_field_id`); the result is the stored value. Pair it with `world_set` to write. Reading a field of an instance that lacks the property is not meaningful, so guard with `world_has` first if unsure.
|
||||
|
||||
Parameters:
|
||||
- `target` — the model instance to read from, an `entity` handle
|
||||
- `property` — the property id, from `world_prop_id`
|
||||
- `field` — the field id within that property, from `world_field_id`
|
||||
|
||||
```ludic
|
||||
program ReadField {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 7, row: 2 } }
|
||||
}
|
||||
|
||||
handler Inspect phase Update {
|
||||
let position_property = world_prop_id("Position")
|
||||
let column_field = world_field_id(position_property, "column")
|
||||
for (position) in query [Position, {Player}] {
|
||||
let value = world_get(self(), position_property, column_field)
|
||||
Screen.status(str(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
35
docs/language/ecs/fn-world_has.md
Normal file
35
docs/language/ecs/fn-world_has.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
id: fn-world_has
|
||||
name: world_has
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_has
|
||||
sig: world_has(target, property) -> bool
|
||||
tip: Test whether a model instance currently carries a property.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_has</code> tests whether a model instance currently carries a given property, returning a boolean. It is the reflection-ABI equivalent of the membership test a `query` performs implicitly, and the safe guard to run before a `world_get`/`world_set` on a property an instance might not have. A property that has been `disable`d reads as absent here (its has-flag is clear), while one that is merely present with default values reads as present. Pass the target instance and the property id from `world_prop_id`.
|
||||
|
||||
Parameters:
|
||||
- `target` — the model instance to test, an `entity` handle
|
||||
- `property` — the property id, from `world_prop_id`
|
||||
|
||||
```ludic
|
||||
program CheckProperty {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Shield { amount: int = 0 }
|
||||
model Player { Position, Shield }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 1, row: 1 }; Shield { amount: 2 } }
|
||||
}
|
||||
|
||||
handler Inspect phase Update {
|
||||
let shield_property = world_prop_id("Shield")
|
||||
for (position) in query [Position, {Player}] {
|
||||
if world_has(self(), shield_property) { Screen.status("shielded") }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
35
docs/language/ecs/fn-world_kind.md
Normal file
35
docs/language/ecs/fn-world_kind.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
id: fn-world_kind
|
||||
name: world_kind
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_kind
|
||||
sig: world_kind(target) -> int
|
||||
tip: The model id of an instance — which kind of thing it is.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_kind</code> returns the model id of a given instance — that is, which kind of thing it is — as an `int`. Compare the result against ids from `world_model_id` to branch generically on an instance's kind without knowing it at compile time, the reflection-ABI stand-in for the `{Model}` tag filter in a static `query`. It is the natural partner of `world_query_next` when you walk instances by id and need to tell players from enemies. Pass the instance handle you want to classify.
|
||||
|
||||
Parameters:
|
||||
- `target` — the model instance to classify, an `entity` handle
|
||||
|
||||
```ludic
|
||||
program Classify {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
model Enemy { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 2, row: 2 } }
|
||||
spawn Grunt { Position { column: 8, row: 2 } }
|
||||
}
|
||||
|
||||
handler Inspect phase Update {
|
||||
let enemy_model = world_model_id("Enemy")
|
||||
for (position) in query [Position] {
|
||||
if world_kind(self()) == enemy_model { Screen.status("found an enemy") }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
35
docs/language/ecs/fn-world_load.md
Normal file
35
docs/language/ecs/fn-world_load.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
id: fn-world_load
|
||||
name: world_load
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_load
|
||||
sig: world_load(buf, len)
|
||||
tip: Restore the whole world from a serialized byte buffer.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_load</code> restores the ECS world from a byte buffer previously produced by `world_save`, replacing the current world with the snapshot's instances and their fields. You pass the buffer and the number of valid bytes in it (the count `world_save` returned), and the world is rebuilt in place; it returns nothing. This is the low-level, buffer-based counterpart of `world_save` — the pair a mod or tool uses to move a whole world across the ABI or between storage. For a simple whole-game load from the default slot, the higher-level `load()` builtin is usually enough.
|
||||
|
||||
Parameters:
|
||||
- `buf` — the source byte buffer holding a snapshot
|
||||
- `len` — the number of valid bytes in `buf`
|
||||
|
||||
```ludic
|
||||
program SnapshotRoundTrip {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Enemy { Position }
|
||||
|
||||
var snapshot_length: int = 0
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Grunt { Position { column: 3, row: 3 } }
|
||||
}
|
||||
|
||||
handler RoundTrip phase Update {
|
||||
let buffer = bytes(4096)
|
||||
if Input.key() == 's' { snapshot_length = world_save(buffer) }
|
||||
if Input.key() == 'l' { world_load(buffer, snapshot_length) }
|
||||
}
|
||||
}
|
||||
```
|
||||
31
docs/language/ecs/fn-world_model_id.md
Normal file
31
docs/language/ecs/fn-world_model_id.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
id: fn-world_model_id
|
||||
name: world_model_id
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_model_id
|
||||
sig: world_model_id(name) -> int
|
||||
tip: Resolve a model's name to its stable numeric id.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_model_id</code> looks up a model by name and returns its stable numeric id, or a negative value if no model of that name exists. It is part of Ludic's reflection ABI — the dynamic, id-based view of the world that mods and tools use alongside the static `spawn`/`query` forms. You typically resolve a name to an id once and then feed that id to id-based calls such as `world_spawn` or `world_kind`. Because the id is stable for a given build, it is safe to cache in a `var`.
|
||||
|
||||
Parameters:
|
||||
- `name` — the model's name, as a string, e.g. `"Player"`
|
||||
|
||||
```ludic
|
||||
program ResolveModel {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 3, row: 3 } }
|
||||
}
|
||||
|
||||
handler Inspect phase Update {
|
||||
let player_model = world_model_id("Player")
|
||||
Screen.status(str(player_model))
|
||||
}
|
||||
}
|
||||
```
|
||||
31
docs/language/ecs/fn-world_prop_id.md
Normal file
31
docs/language/ecs/fn-world_prop_id.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
id: fn-world_prop_id
|
||||
name: world_prop_id
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_prop_id
|
||||
sig: world_prop_id(name) -> int
|
||||
tip: Resolve a property's name to its stable numeric id.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_prop_id</code> resolves a property by name to its stable numeric id, returning a negative value when no such property exists. It is the entry point to the reflection ABI's field access: pass the returned property id to `world_has`, `world_count`, `world_get`/`world_set` (together with a field id from `world_field_id`), and the dynamic attach/detach and query calls. Resolve the id once and cache it in a `var` rather than looking it up every frame. This id-based view complements the static `query [...]`, which is how ordinary handlers reach properties by name.
|
||||
|
||||
Parameters:
|
||||
- `name` — the property's name, as a string, e.g. `"Position"`
|
||||
|
||||
```ludic
|
||||
program ResolveProperty {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 3, row: 3 } }
|
||||
}
|
||||
|
||||
handler Inspect phase Update {
|
||||
let position_property = world_prop_id("Position")
|
||||
Screen.status(str(position_property))
|
||||
}
|
||||
}
|
||||
```
|
||||
38
docs/language/ecs/fn-world_query_next.md
Normal file
38
docs/language/ecs/fn-world_query_next.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
id: fn-world_query_next
|
||||
name: world_query_next
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_query_next
|
||||
sig: world_query_next(property, cursor) -> entity
|
||||
tip: Step to the next instance carrying a property, walking the world by id.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_query_next</code> walks the world by ascending instance id, returning the next live instance at or after the cursor that carries the given property, or a negative value when there are no more. It is the reflection-ABI equivalent of a `for (…) in query [Property]` loop, expressed as an explicit step so a mod or tool can iterate matching instances by id. Drive it with a loop: start the cursor at `0`, and after each hit advance the cursor to one past the returned id before calling again. Pass the property id from `world_prop_id`.
|
||||
|
||||
Parameters:
|
||||
- `property` — the property id to match, from `world_prop_id`
|
||||
- `cursor` — the id to start scanning from; advance it past each hit to continue
|
||||
|
||||
```ludic
|
||||
program WalkInstances {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Enemy { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn GruntA { Position { column: 2, row: 2 } }
|
||||
spawn GruntB { Position { column: 5, row: 2 } }
|
||||
}
|
||||
|
||||
handler Sweep phase Update {
|
||||
let position_property = world_prop_id("Position")
|
||||
var cursor = 0
|
||||
var found = world_query_next(position_property, cursor)
|
||||
while found >= 0 {
|
||||
cursor = found + 1
|
||||
found = world_query_next(position_property, cursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
29
docs/language/ecs/fn-world_register_prop.md
Normal file
29
docs/language/ecs/fn-world_register_prop.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
id: fn-world_register_prop
|
||||
name: world_register_prop
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_register_prop
|
||||
sig: world_register_prop(name, fields) -> int
|
||||
tip: Register a brand-new property at runtime and get its id (dynamic ECS).
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_register_prop</code> declares a <b>brand-new</b> property at runtime — one not present in the compiled program — and returns its property id. This is the dynamic-ECS entry point for mods: a mod can add its own component to the world, then `world_attach_dyn` it onto instances and read and write its fields with `world_get`/`world_set`. You give the property a name and its number of fields; the returned id then works everywhere a compiled property id does. Compiled programs that need no runtime-defined properties never call this — they declare `property` blocks at compile time instead.
|
||||
|
||||
Parameters:
|
||||
- `name` — the new property's name, as a string
|
||||
- `fields` — how many integer fields it has
|
||||
|
||||
```ludic
|
||||
program ModComponent {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 4, row: 4 } }
|
||||
let stamina_property = world_register_prop("Stamina", 1)
|
||||
Screen.status(str(stamina_property))
|
||||
}
|
||||
}
|
||||
```
|
||||
34
docs/language/ecs/fn-world_save.md
Normal file
34
docs/language/ecs/fn-world_save.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
id: fn-world_save
|
||||
name: world_save
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_save
|
||||
sig: world_save(buf) -> int
|
||||
tip: Serialize the whole world into a buffer; returns the number of bytes written.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_save</code> serializes the entire ECS world — every live instance, its kind, and all its property fields — into a byte buffer you provide, and returns the number of bytes written. It is the low-level, buffer-based form of a save: allocate a buffer with `bytes(n)` large enough to hold the snapshot, pass it in, and persist the written bytes (or hand them across the mod ABI). The exact count comes back so you know how much of the buffer is meaningful. Restore a snapshot later with `world_load`. For a simple whole-game save to the default slot, the higher-level `save()` builtin is usually enough.
|
||||
|
||||
Parameters:
|
||||
- `buf` — the destination byte buffer, e.g. from `bytes(n)`
|
||||
|
||||
```ludic
|
||||
program SnapshotOut {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Enemy { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Grunt { Position { column: 3, row: 3 } }
|
||||
}
|
||||
|
||||
handler Persist phase Update {
|
||||
if Input.key() == 's' {
|
||||
let buffer = bytes(4096)
|
||||
let written = world_save(buffer)
|
||||
Screen.status(str(written))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
37
docs/language/ecs/fn-world_set.md
Normal file
37
docs/language/ecs/fn-world_set.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
id: fn-world_set
|
||||
name: world_set
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_set
|
||||
sig: world_set(target, property, field, value)
|
||||
tip: Write one field of a model instance by numeric id (reflection ABI).
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_set</code> writes a single field of a model instance by numeric id — the id-based counterpart to an ordinary `position.column = …` assignment, used when the property is not known at compile time. You give the target instance, the property id (from `world_prop_id`), the field id (from `world_field_id`), and the new value; the value is stored just as a direct assignment would. It returns nothing. Together with `world_get`, `world_has`, and the resolver functions, it lets a mod or tool mutate any field on any instance generically, which is how the reflection ABI mirrors the compiled `query`/field access at runtime.
|
||||
|
||||
Parameters:
|
||||
- `target` — the model instance to write to, an `entity` handle
|
||||
- `property` — the property id, from `world_prop_id`
|
||||
- `field` — the field id within that property, from `world_field_id`
|
||||
- `value` — the new integer value to store
|
||||
|
||||
```ludic
|
||||
program WriteField {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 0, row: 0 } }
|
||||
}
|
||||
|
||||
handler Nudge phase Update {
|
||||
let position_property = world_prop_id("Position")
|
||||
let column_field = world_field_id(position_property, "column")
|
||||
for (position) in query [Position, {Player}] {
|
||||
world_set(self(), position_property, column_field, position.column + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
32
docs/language/ecs/fn-world_size.md
Normal file
32
docs/language/ecs/fn-world_size.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
id: fn-world_size
|
||||
name: world_size
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_size
|
||||
sig: world_size() -> int
|
||||
tip: The number of live model instances in the world.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_size</code> returns the total number of live model instances in the world, across every kind, as an `int`. It takes no arguments and is the cheapest way to answer "how big is the world right now?" — useful for a debug readout, a capacity check before a big spawn, or a mod inspecting the running game through the reflection ABI. For a count restricted to instances carrying a particular property, use `world_count` instead.
|
||||
|
||||
```ludic
|
||||
program WorldSize {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Enemy { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn GruntA { Position { column: 4, row: 4 } }
|
||||
spawn GruntB { Position { column: 6, row: 4 } }
|
||||
spawn GruntC { Position { column: 8, row: 4 } }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
let total = world_size()
|
||||
Screen.draw_number(x: 8, y: 8, value: total, color: Color.White, scale: 2)
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
30
docs/language/ecs/fn-world_spawn.md
Normal file
30
docs/language/ecs/fn-world_spawn.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
id: fn-world_spawn
|
||||
name: world_spawn
|
||||
category: ecs
|
||||
kind: builtin
|
||||
tokens: world_spawn
|
||||
sig: world_spawn(model) -> entity
|
||||
tip: Spawn an instance of a model chosen by numeric id at runtime.
|
||||
order: 50
|
||||
---
|
||||
|
||||
<code>world_spawn</code> creates a new instance of a model chosen by numeric id and returns its `entity` handle — the reflection-ABI counterpart of the static `spawn Label { … }`. Because the model is given as an id (from `world_model_id`) rather than a name written in source, a mod or tool can decide <b>at runtime</b> which kind to create. The new instance is seeded exactly as the static spawn would seed it — every property in the model attached with default field values — after which you can set fields with `world_set`. Use the ordinary `spawn` statement in normal handler code; reach for `world_spawn` when the kind is data, not a literal.
|
||||
|
||||
Parameters:
|
||||
- `model` — the model id to instantiate, from `world_model_id`
|
||||
|
||||
```ludic
|
||||
program Reinforcements {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Enemy { Position }
|
||||
|
||||
handler Reinforce phase Update {
|
||||
if Input.key() == ' ' {
|
||||
let enemy_model = world_model_id("Enemy")
|
||||
let new_enemy = world_spawn(enemy_model)
|
||||
Screen.status(str(new_enemy))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -5,8 +5,28 @@ category: ecs
|
|||
kind: keyword
|
||||
tokens: attach
|
||||
sig: attach Prop on entity { overrides }
|
||||
tip: Add a property to a live entity.
|
||||
tip: Structurally add a property to a live model instance, seeding its fields.
|
||||
order: 6
|
||||
---
|
||||
|
||||
Add a property to a live entity.
|
||||
<code>attach</code> adds a property to a live model instance that did not have it, seeding the fields from their defaults plus any overrides in the `{ … }` block, and fires the property's `@OnAttach(Prop)` hook. It is a <b>structural</b> change — it changes what the instance <i>has</i> — as opposed to `enable`, which merely resumes a property the instance already carries. Attaching a property the instance already has is a no-op, so the hook only runs on a real transition. Use it to grant something new at runtime: give a player a shield pickup, tag an enemy as enraged, or add a component only some instances need.
|
||||
|
||||
```ludic
|
||||
program Pickups {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Shield { amount: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 6, row: 6 } }
|
||||
}
|
||||
|
||||
handler GrabShield phase Update {
|
||||
if Input.key() == ' ' {
|
||||
for (position) in query [Position, {Player}] {
|
||||
attach Shield on self() { amount: 3 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,26 @@ category: ecs
|
|||
kind: keyword
|
||||
tokens: despawn
|
||||
sig: despawn entity
|
||||
tip: Remove an entity.
|
||||
tip: Remove a model instance from the world, firing any @OnDespawn hooks.
|
||||
order: 1
|
||||
---
|
||||
|
||||
Remove an entity. Inside a query, <code>despawn self()</code> removes the current match.
|
||||
A <code>despawn</code> removes a model instance from the world, freeing its slot and firing any `@OnDespawn` lifecycle hooks for its kind. The most common form is `despawn self()` inside a query loop, which removes the instance currently being visited. Because queries iterate lazily by ascending id rather than from a frozen snapshot, despawning the current instance — or one already visited — is safe and will not disturb the walk. Despawning is how the moving parts of a game leave: an enemy that reached zero health, a projectile that flew off screen, or every instance of a kind during a reset.
|
||||
|
||||
```ludic
|
||||
program Cleanup {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Enemy { Position, Health }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Grunt { Position { column: 3, row: 3 }; Health { current: 0 } }
|
||||
}
|
||||
|
||||
handler RemoveDefeated phase LateUpdate {
|
||||
for (health) in query [Health, {Enemy}] {
|
||||
if health.current <= 0 { despawn self() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,26 @@ category: ecs
|
|||
kind: keyword
|
||||
tokens: detach
|
||||
sig: detach Prop on entity
|
||||
tip: Remove a property from a live entity.
|
||||
tip: Structurally remove a property from a live model instance.
|
||||
order: 7
|
||||
---
|
||||
|
||||
Remove a property from a live entity.
|
||||
<code>detach</code> removes a property from a live model instance, firing the property's `@OnDetach(Prop)` hook <b>before</b> the has-flag clears — so the hook can still read the outgoing field values as it tears down. It is the structural opposite of `attach`: where `disable` pauses a property but keeps its data for a later `enable`, `detach` genuinely removes the property, and a later `attach` re-seeds fresh fields from defaults. Detaching a property the instance does not have is a no-op. Reach for it when something is gone for good — a shield consumed, a status effect that has run its course, an ability removed.
|
||||
|
||||
```ludic
|
||||
program ShieldBreak {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Shield { amount: int = 0 }
|
||||
model Player { Position, Shield }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 6, row: 6 }; Shield { amount: 1 } }
|
||||
}
|
||||
|
||||
handler ConsumeShield phase Update {
|
||||
for (shield) in query [Shield, {Player}] {
|
||||
if shield.amount <= 0 { detach Shield on self() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,26 @@ category: ecs
|
|||
kind: keyword
|
||||
tokens: disable
|
||||
sig: disable Prop on entity
|
||||
tip: Deactivate a property without destroying its data.
|
||||
tip: Deactivate a property, model, or handler without destroying its data.
|
||||
order: 5
|
||||
---
|
||||
|
||||
Deactivate a property without destroying its data. Also <code>disable Model</code> / a whole handler.
|
||||
<code>disable</code> switches something off while keeping it intact, so a later `enable` brings it back untouched — a reversible pause rather than a destruction. `disable Prop on instance` clears that instance's has-flag for the property, so queries stop matching it, but the field values stay in storage. It has three scopes: one property on one instance (`disable Shield on self()`), a whole model (`disable Enemy` drops all its instances out of every query), and a handler (`disable AiThink` stops it running each phase until re-enabled). Each toggle is a single flag flip, so nothing is copied or freed. Disabling a property fires any `@OnDisable(Prop)` hook. Contrast with `detach`, which structurally removes a property (a later `attach` re-seeds fresh fields).
|
||||
|
||||
```ludic
|
||||
program Stealth {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
model Enemy { Position, Velocity }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Grunt { Position { column: 4, row: 4 }; Velocity { delta_x: 1 } }
|
||||
}
|
||||
|
||||
handler FreezeEnemies phase Update {
|
||||
if Input.key() == 'p' {
|
||||
for (velocity) in query [Velocity, {Enemy}] { disable Velocity on self() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,27 @@ category: ecs
|
|||
kind: keyword
|
||||
tokens: enable
|
||||
sig: enable Prop on entity
|
||||
tip: Re-activate a disabled property; its stored values are intact.
|
||||
tip: Re-activate a disabled property, model, or handler — its data is intact.
|
||||
order: 4
|
||||
---
|
||||
|
||||
Re-activate a disabled property; its stored values are intact.
|
||||
<code>enable</code> reverses a <code>disable</code>: it turns something back on without recreating it. `enable Prop on instance` restores a property's has-flag so queries match the instance again, with the field values exactly as they were left — nothing was copied or freed, so re-enabling is cheap and lossless. There are three scopes, matching `disable`: a single property on one instance (`enable Shield on self()`), a whole model (`enable Enemy`, all its instances rejoin every query), and a handler (`enable AiThink`, it resumes running each phase). Re-enabling a property fires any `@OnEnable(Prop)` hook at the toggle point. Reach for enable/disable when you want to pause and resume, and for `attach`/`detach` when you want to add or structurally remove.
|
||||
|
||||
```ludic
|
||||
program ShieldToggle {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Shield { amount: int = 0 }
|
||||
model Player { Position, Shield }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 8, row: 8 }; Shield { amount: 3 } }
|
||||
disable Shield on self()
|
||||
}
|
||||
|
||||
handler RestoreShield phase Update {
|
||||
if Input.key() == ' ' {
|
||||
for (position) in query [Position, {Player}] { enable Shield on self() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,12 +5,28 @@ category: ecs
|
|||
kind: keyword
|
||||
tokens: query
|
||||
sig: query [PropA, PropB, {Tag}]
|
||||
tip: Match every entity that carries all listed properties.
|
||||
tip: Match every model instance carrying all the listed properties.
|
||||
order: 2
|
||||
---
|
||||
|
||||
Match every entity that carries all listed properties. Use it in <code>for (a, b) in query […]</code>; a <code>{Tag}</code> filters without binding.
|
||||
A <code>query</code> selects every live model instance that carries <b>all</b> the listed properties, and is the core way handlers reach data. Use it in a `for (…) in query [...]` loop, where you write one loop variable per bound property in declaration order; each pass binds those properties for one matching instance and `self()` gives that instance. A `{Model}` term in tag position filters by model kind without binding a variable, so `query [Position, {Player}]` walks only players. Add a `where` clause to filter on field values (`where health.current > 0`). Matching is lazy and re-checked per instance as the loop reaches each id, not snapshotted, so spawns and despawns during the loop follow well-defined rules.
|
||||
|
||||
```ludic
|
||||
for (p, s) in query [Pos, Seg] { … }
|
||||
program Collisions {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Enemy { Position, Health }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Grunt { Position { column: 5, row: 5 }; Health { current: 10 } }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
for (position, health) in query [Position, Health, {Enemy}] where health.current > 0 {
|
||||
Screen.fill_rectangle(x: position.column * 16, y: position.row * 16, width: 16, height: 16, color: Color.Crimson)
|
||||
}
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,26 @@ category: ecs
|
|||
kind: keyword
|
||||
tokens: self
|
||||
sig: self()
|
||||
tip: The entity currently bound by the enclosing query.
|
||||
tip: The model instance currently bound by the enclosing query loop.
|
||||
order: 3
|
||||
---
|
||||
|
||||
The entity currently bound by the enclosing query.
|
||||
<code>self()</code> yields the model instance currently being visited by the innermost enclosing `query` loop, as an `entity` handle. It is how a handler names "the instance this pass is about" — most often to `despawn self()`, to `attach`/`detach` a property `on self()`, or to pass the current instance to a function. Because it depends on the active query iteration, `self()` is only meaningful inside a `for (…) in query […]` loop (or a `@Queries` handler, which desugars to one). Outside any query it has no instance to refer to.
|
||||
|
||||
```ludic
|
||||
program Reaper {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Enemy { Position, Health }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Grunt { Position { column: 1, row: 1 }; Health { current: 0 } }
|
||||
}
|
||||
|
||||
handler RemoveDefeated phase LateUpdate {
|
||||
for (health) in query [Health, {Enemy}] {
|
||||
if health.current <= 0 { despawn self() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,12 +5,22 @@ category: ecs
|
|||
kind: keyword
|
||||
tokens: spawn
|
||||
sig: spawn Label { Prop { field: v }; … }
|
||||
tip: Create an entity carrying the listed properties (or a model).
|
||||
tip: Create a model instance carrying the listed properties, seeding their fields.
|
||||
order: 0
|
||||
---
|
||||
|
||||
Create an entity carrying the listed properties (or a model). The label is for readability.
|
||||
A <code>spawn</code> creates a new model instance and attaches the listed properties, each field starting from its default and then taking any overrides you give. The `Label` after `spawn` is just a readable name for the instance at that call site — it does not have to be a declared model — while the `{Prop { … }; …}` block is what actually determines which properties the instance carries. If you `spawn` a declared `model`, every property in that model comes along automatically. A newly spawned instance is visible to matching queries immediately, and one spawned mid-loop at a higher id is even visited in the same tick, so spawn into a later phase if you want to defer that.
|
||||
|
||||
```ludic
|
||||
spawn Body { Seg { order: 1 }; Pos { x: 9, y: 7 } }
|
||||
program Waves {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Enemy { Position, Velocity, Health }
|
||||
|
||||
handler SpawnWave phase Start {
|
||||
spawn Grunt { Position { column: 18, row: 4 }; Velocity { delta_x: -1 }; Health { current: 20 } }
|
||||
spawn Brute { Position { column: 18, row: 9 }; Velocity { delta_x: -1 }; Health { current: 60 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue