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
|
|
@ -5,8 +5,31 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: const
|
||||
sig: const NAME: T = value
|
||||
tip: A compile-time constant.
|
||||
tip: A compile-time constant, folded into the code with no storage.
|
||||
order: 5
|
||||
---
|
||||
|
||||
A compile-time constant. Folds directly into the code — no storage, no cost.
|
||||
A <code>const</code> binds a name to a value known at compile time. Unlike `var`, it has no storage and cannot be reassigned — the compiler folds it directly into wherever it is used, so it costs nothing at runtime. Reach for `const` for the fixed dimensions and magic numbers of your game — grid sizes, tile pixels, tuning values — so the code reads in names instead of literals. Give constants descriptive, uppercase names, and use them everywhere the value appears so a single edit changes the whole game.
|
||||
|
||||
```ludic
|
||||
program Board {
|
||||
const GRID_WIDTH: int = 20
|
||||
const GRID_HEIGHT: int = 15
|
||||
const TILE_SIZE: int = 16
|
||||
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: GRID_WIDTH / 2, row: GRID_HEIGHT / 2 } }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
for (position) in query [Position, {Player}] {
|
||||
Screen.fill_rectangle(x: position.column * TILE_SIZE, y: position.row * TILE_SIZE, width: TILE_SIZE, height: TILE_SIZE, color: Color.Gold)
|
||||
}
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
37
docs/language/structure/kw-enum.md
Normal file
37
docs/language/structure/kw-enum.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
id: kw-enum
|
||||
name: enum
|
||||
category: structure
|
||||
kind: keyword
|
||||
tokens: enum
|
||||
sig: enum Name { A, B, C }
|
||||
tip: A named set of integer constants — names for a magic-number space.
|
||||
order: 50
|
||||
---
|
||||
|
||||
An <code>enum</code> gives names to a set of related integer values so a magic-number space — a menu selection, a game mode, a machine state — reads as names instead of bare literals. Variants number themselves from `0` in declaration order, and you access one as `Name.Variant`, which is a compile-time `int` usable anywhere an int is: in `match` patterns, comparisons, and assignments. Ludic has no distinct enum runtime type yet — an enum value lives in an ordinary `int` or `var` and is saved with it — so `enum` is best understood as a readable naming layer over `int`.
|
||||
|
||||
```ludic
|
||||
program BattleMenu {
|
||||
enum Action { Attack, Guard, Item, Flee }
|
||||
|
||||
var current_action: int = Action.Attack
|
||||
|
||||
handler ChooseAction phase Input {
|
||||
let pressed = Input.key()
|
||||
if pressed == 'a' { current_action = Action.Attack }
|
||||
if pressed == 'g' { current_action = Action.Guard }
|
||||
if pressed == 'f' { current_action = Action.Flee }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
match current_action {
|
||||
Action.Attack => Screen.draw_text(x: 8, y: 8, text: "ATTACK", color: Color.Crimson, scale: 2)
|
||||
Action.Guard => Screen.draw_text(x: 8, y: 8, text: "GUARD", color: Color.White, scale: 2)
|
||||
_ => Screen.draw_text(x: 8, y: 8, text: "FLEE", color: Color.Gold, scale: 2)
|
||||
}
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -9,4 +9,23 @@ tip: Bind a name to an external C-ABI symbol — the seam for platform and libra
|
|||
order: 12
|
||||
---
|
||||
|
||||
Bind a name to an external C-ABI symbol — the seam for platform and library calls.
|
||||
An <code>extern fn</code> declares a function whose body lives outside Ludic and binds it to a C-ABI symbol resolved at link time. It is the seam through which Ludic reaches anything with a C interface — a system library, a math routine, or even another `.ludic` file compiled as a `module`. You write the Ludic signature you want to call and give the real symbol name after `=`; the linker connects them (pass `-L`/`-l` to `ludicc` to point at the library). Types must match the foreign ABI, so map each parameter and the return to the right Ludic type (`int`, `fixed`, `ptr`, …).
|
||||
|
||||
Parameters:
|
||||
- `a` — a typed argument passed straight through to the foreign symbol
|
||||
|
||||
```ludic
|
||||
program Physics {
|
||||
extern fn c_hypot(a: fixed, b: fixed) -> fixed = "hypot_fx"
|
||||
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
model Projectile { Velocity }
|
||||
|
||||
handler MeasureSpeed phase Update {
|
||||
for (velocity) in query [Velocity, {Projectile}] {
|
||||
let speed = c_hypot(a: fx(velocity.delta_x), b: fx(velocity.delta_y))
|
||||
Screen.status(str(flr(speed)))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,34 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: fn
|
||||
sig: fn name(a: T, b: T) -> R { … }
|
||||
tip: A function.
|
||||
tip: A function — reusable logic called positionally or with named arguments.
|
||||
order: 8
|
||||
---
|
||||
|
||||
A function. Call it positionally or with named arguments: <code>name(a: 1, b: 2)</code>.
|
||||
A <code>fn</code> declares a function: a reusable block of logic with typed parameters and a return type, written `-> R` (use `-> void` for one that returns nothing). Call it positionally, `heal(2, 10)`, or with named arguments, `heal(amount: 2, maximum: 10)`, which reads more clearly at the call site and is the house style. Functions live at program scope alongside handlers and may be called from any handler; they can `spawn`, run queries, and read program-scope state. Use them to factor out logic shared by several handlers so each handler stays short.
|
||||
|
||||
Parameters:
|
||||
- `a`, `b` — the typed inputs; pass them positionally or by name at the call site
|
||||
|
||||
```ludic
|
||||
program Healer {
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Player { Health }
|
||||
|
||||
fn heal(amount: int, maximum: int) -> int {
|
||||
let restored = amount * 2
|
||||
if restored > maximum { return maximum }
|
||||
return restored
|
||||
}
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Health { current: 10, maximum: 100 } }
|
||||
}
|
||||
|
||||
handler Recover phase Update {
|
||||
for (health) in query [Health, {Player}] {
|
||||
health.current = heal(amount: health.current, maximum: health.maximum)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,12 +5,34 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: handler
|
||||
sig: handler Name phase P { … }
|
||||
tip: A block of code the engine runs every frame during phase P.
|
||||
tip: A named block the engine runs each frame during phase P.
|
||||
order: 3
|
||||
---
|
||||
|
||||
A block of code the engine runs every frame during phase <code>P</code>. With a query, the body runs once per matching entity.
|
||||
A <code>handler</code> is a named block of behavior that the engine runs automatically during a given `phase` — the unit that turns your data into a game. A handler with no query runs <b>once per phase tick</b>; a handler with a `@Queries` annotation (or an inline `for (…) in query […]` loop) runs its body <b>once per matching model instance</b>, with each property bound by name and `self()` giving the current instance. Handlers are registered implicitly just by being declared, and you can pause one at runtime with `disable Handler` and bring it back with `enable Handler`. Keep behavior in handlers and keep data plain in properties — that separation is the whole point.
|
||||
|
||||
```ludic
|
||||
handler Move phase Update { … }
|
||||
program Runner {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
model Player { Position, Velocity }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 0, row: 6 }; Velocity { delta_x: 1 } }
|
||||
}
|
||||
|
||||
handler AdvancePositions phase Update {
|
||||
for (position, velocity) in query [Position, Velocity, {Player}] {
|
||||
position.column = position.column + velocity.delta_x
|
||||
}
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
for (position) in query [Position, {Player}] {
|
||||
Screen.fill_rectangle(x: position.column * 16, y: position.row * 16, width: 16, height: 16, color: Color.LimeGreen)
|
||||
}
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,19 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: import
|
||||
sig: import "file.ludic"
|
||||
tip: Splice another Ludic file into this program.
|
||||
tip: Splice another Ludic file's declarations into this program.
|
||||
order: 10
|
||||
---
|
||||
|
||||
Splice another Ludic file into this program. Paths resolve relative to the importer; re-imports are free.
|
||||
An <code>import</code> pulls the declarations of another Ludic file into this program, letting you split a game across many files instead of one giant block. The imported file is a <b>fragment</b> — bare declarations with no `program` wrapper — and its contents are spliced in as if written here. Paths resolve relative to the importing file, imports may nest, and each resolved path is include-guarded, so importing the same file twice (even through different chains) pulls it in exactly once. Diagnostics still point at the real source file, so errors in an imported fragment report that file's name and line.
|
||||
|
||||
```ludic
|
||||
program ChronoRift {
|
||||
import "chronorift/world.ludic" # properties and models
|
||||
import "chronorift/combat.ludic" # the battle handlers
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 4, row: 4 } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,27 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: let
|
||||
sig: let name = value
|
||||
tip: An immutable binding, scoped to the block it appears in.
|
||||
tip: An immutable binding — the default choice for a value that never changes.
|
||||
order: 7
|
||||
---
|
||||
|
||||
An immutable binding, scoped to the block it appears in.
|
||||
A <code>let</code> introduces an <b>immutable</b> binding: once set, reassigning it (`name = …`) is a compile error. Reach for `let` by default and only switch to `var` when a value genuinely needs to change — it makes intent obvious and catches accidental writes. Immutability is of the <b>binding</b>, not the object it points at: a `let` that holds a property record or a slice still lets you mutate through it (`node.current = 5`), it just cannot be repointed at a different object. Bindings inside a body are locals scoped to their block.
|
||||
|
||||
```ludic
|
||||
program Damage {
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Enemy { Health }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Grunt { Health { current: 40 } }
|
||||
}
|
||||
|
||||
handler ApplyHit phase Update {
|
||||
let incoming_damage = 12
|
||||
for (health) in query [Health, {Enemy}] {
|
||||
let survivor = health.current - incoming_damage
|
||||
health.current = survivor
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,12 +5,30 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: model
|
||||
sig: model Name { PropA, PropB, … }
|
||||
tip: A named bundle of properties, so an entity that always travels together is spawned by one name.
|
||||
tip: A named kind of thing — a fixed bundle of properties you spawn by one name.
|
||||
order: 2
|
||||
---
|
||||
|
||||
A named bundle of properties, so an entity that always travels together is spawned by one name.
|
||||
A <code>model</code> names a <b>kind</b> of thing in your game and the fixed set of properties every instance of it carries. Instead of attaching properties one by one, you `spawn` the model by name and every listed property comes with it, seeded from its defaults. A model's name doubles as a query tag: write `{Player}` inside a `query [...]` to match only instances of that model. The model itself has no fields of its own, so you never bind it to a variable — you bind its properties and filter by its tag.
|
||||
|
||||
```ludic
|
||||
model Player { Health, Shield }
|
||||
program Arena {
|
||||
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 Player { Position, Health }
|
||||
model Enemy { Position, Velocity, Health }
|
||||
|
||||
handler Spawn phase Start {
|
||||
spawn Hero { Position { column: 2, row: 8 } }
|
||||
spawn Grunt { Position { column: 18, row: 8 }; Velocity { delta_x: -1 } }
|
||||
}
|
||||
|
||||
handler AdvanceEnemies phase Update {
|
||||
for (position, velocity) in query [Position, Velocity, {Enemy}] {
|
||||
position.column = position.column + velocity.delta_x
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
id: kw-module
|
||||
name: module
|
||||
category: structure
|
||||
kind: keyword
|
||||
tokens: module
|
||||
sig: module Name { @export fn … }
|
||||
tip: Build a shared library of plain C-ABI symbols instead of an executable.
|
||||
order: 11
|
||||
---
|
||||
|
||||
Build a shared library of plain C-ABI symbols instead of an executable.
|
||||
|
|
@ -2,11 +2,45 @@
|
|||
id: kw-phase
|
||||
name: phase
|
||||
category: structure
|
||||
kind: phase
|
||||
tokens: Start Input Update FixedUpdate Render
|
||||
kind: keyword
|
||||
tokens: phase
|
||||
sig: phase Start | Input | Update | FixedUpdate | Render
|
||||
tip: When a handler runs.
|
||||
tip: Names which stage of the frame a handler runs in.
|
||||
order: 4
|
||||
---
|
||||
|
||||
When a handler runs. <b>Start</b> once at boot; <b>Input</b> reads the keyboard; <b>Update</b> is the per-frame step; <b>FixedUpdate</b> is the deterministic fixed-step; <b>Render</b> draws the frame.
|
||||
Every handler declares a <code>phase</code> — the stage of the frame in which the engine calls it. <b>Start</b> runs once at boot, before the first frame, and is where you seed the world. Then every frame the engine runs, in order, <b>Input</b> (read the keyboard), <b>FixedUpdate</b> (the deterministic fixed-timestep step, for physics and anything that must be reproducible), <b>Update</b> (the ordinary per-frame logic), and <b>Render</b> (draw the frame, ending with `Screen.show()`). Handlers in the same phase run in declaration order, so ordering within a phase is under your control. Put drawing only in `Render`; put keyboard reads in `Input`.
|
||||
|
||||
```ludic
|
||||
program PhaseTour {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Velocity { delta_x: int = 0, delta_y: int = 0 }
|
||||
model Player { Position, Velocity }
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 5, row: 5 } }
|
||||
}
|
||||
|
||||
handler ReadKeys phase Input {
|
||||
let pressed = Input.key()
|
||||
for (velocity) in query [Velocity, {Player}] {
|
||||
if pressed == 'd' { velocity.delta_x = 1 }
|
||||
if pressed == 'a' { velocity.delta_x = -1 }
|
||||
}
|
||||
}
|
||||
|
||||
handler AdvancePositions phase Update {
|
||||
for (position, velocity) in query [Position, Velocity, {Player}] {
|
||||
position.column = position.column + velocity.delta_x
|
||||
}
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
for (position) in query [Position, {Player}] {
|
||||
Screen.fill_rectangle(x: position.column * 16, y: position.row * 16, width: 16, height: 16, color: Color.White)
|
||||
}
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,30 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: program
|
||||
sig: program Name { … }
|
||||
tip: The top-level unit.
|
||||
tip: The top-level unit — one program compiles to one native game.
|
||||
order: 0
|
||||
---
|
||||
|
||||
The top-level unit. A program compiles to one native game; everything else lives inside it.
|
||||
A <code>program</code> is the outermost unit of Ludic source, and everything else — properties, models, handlers, functions, constants and your named state — lives inside its braces. Each program compiles to exactly one native game (or, with a `module`, one shared library), so a project has a single top-level `program` block. The name you give it is used for the built binary and for diagnostics, and by convention matches the file. When the engine boots, it runs your `Start` handlers once and then drives the per-frame phase loop until the game quits.
|
||||
|
||||
```ludic
|
||||
program SpaceDrift {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
model Player { Position }
|
||||
|
||||
var score: int = 0
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Position { column: 10, row: 8 } }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
for (position) in query [Position, {Player}] {
|
||||
Screen.fill_rectangle(x: position.column * 16, y: position.row * 16, width: 16, height: 16, color: Color.LimeGreen)
|
||||
}
|
||||
Screen.draw_number(x: 6, y: 4, value: score, color: Color.Gold, scale: 1)
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,12 +5,29 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: property
|
||||
sig: property Name { field: T = default, … }
|
||||
tip: A component: a named record of fields an entity can carry.
|
||||
tip: A named record of typed fields — a per-model component, or a plain heap record.
|
||||
order: 1
|
||||
---
|
||||
|
||||
A component: a named record of fields an entity can carry. Fields have a type and a default.
|
||||
A <code>property</code> declares a named record of typed fields, each with a default value. It is the one record keyword in Ludic, and how you <b>use</b> it decides how it is stored: list it in a `model` (or `attach` it with `spawn`) and it becomes a per-instance component held in the engine's storage and bound in queries; construct it with `new` and it becomes a plain heap record addressed by a pointer. Fields carry a type and a default, so a freshly spawned or `new`-ed property starts fully seeded. Give fields descriptive names — `column`/`row`, not `x`/`y` — because those names are what handlers read and write.
|
||||
|
||||
```ludic
|
||||
property Pos { x: int = 0, y: int = 0 }
|
||||
program Descent {
|
||||
property Position { column: int = 0, row: int = 0 }
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Player { Position, Health }
|
||||
|
||||
handler Spawn phase Start {
|
||||
spawn Hero { Position { column: 4, row: 4 }; Health { current: 80 } }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
for (position, health) in query [Position, Health, {Player}] {
|
||||
let bar_width = health.current / 4
|
||||
Screen.fill_rectangle(x: position.column, y: position.row, width: bar_width, height: 4, color: Color.Crimson)
|
||||
}
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,8 +5,31 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: return
|
||||
sig: return value
|
||||
tip: Return from a function.
|
||||
tip: Hand a value back from a function and stop running it.
|
||||
order: 9
|
||||
---
|
||||
|
||||
Return from a function.
|
||||
A <code>return</code> statement ends the current function and hands its result back to the caller. Its value must match the function's declared return type; in a `-> void` function you write a bare `return` (or simply let the body end) to exit early. `return` is often paired with an early guard — check a condition and return straight away — which keeps the common path unindented. Inside a handler's query loop, prefer `break` or `continue` to control the loop; `return` leaves the whole function.
|
||||
|
||||
```ludic
|
||||
program Clamp {
|
||||
property Health { current: int = 100, maximum: int = 100 }
|
||||
model Player { Health }
|
||||
|
||||
fn clamp_current(value: int, maximum: int) -> int {
|
||||
if value < 0 { return 0 }
|
||||
if value > maximum { return maximum }
|
||||
return value
|
||||
}
|
||||
|
||||
handler Boot phase Start {
|
||||
spawn Hero { Health { current: 250, maximum: 100 } }
|
||||
}
|
||||
|
||||
handler Normalize phase Update {
|
||||
for (health) in query [Health, {Player}] {
|
||||
health.current = clamp_current(value: health.current, maximum: health.maximum)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
43
docs/language/structure/kw-ui.md
Normal file
43
docs/language/structure/kw-ui.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
id: kw-ui
|
||||
name: ui
|
||||
category: structure
|
||||
kind: keyword
|
||||
tokens: ui
|
||||
sig: ui { panel { … } }
|
||||
tip: Declare a retained widget tree as data; the engine lays it out and draws it.
|
||||
order: 50
|
||||
---
|
||||
|
||||
A <code>ui</code> block declares a <b>retained</b> widget tree as data — panels, labels and buttons — and hands layout, drawing and keyboard focus to the engine, so you describe the interface once instead of repainting it every frame. Widget types are `panel` (a container with optional skin/background/border), `col`/`row` (pure stacks), `label`, `button` (focusable), `image` and `spacer`, and their props are evaluated at build time, so `font: title_font` reads a value the program set first. Each `id: Name` mints a `UI_Name` handle you drive from handlers: call `ui_build()` then `ui_open(UI_MainMenu)` at start, `ui_tick(Input.key())` each update to move focus and activate, and `ui_clicked(UI_Name)` to react. Draw it during `Render` with `ui_render()` between `Screen.clear` and `Screen.show()`.
|
||||
|
||||
```ludic
|
||||
program TitleScreen {
|
||||
var title_font: int = 0
|
||||
|
||||
ui MainMenu {
|
||||
panel id: Root w: 288 pad: 16 gap: 6 bg: 0x1a1a2c align: center {
|
||||
label text: "CHRONO RIFT" font: title_font size: 26 fg: 0xffe060 align: center
|
||||
button id: NewGame text: "New Game" font: title_font size: 16 w: 236
|
||||
button id: Quit text: "Quit" font: title_font size: 16 w: 236
|
||||
}
|
||||
}
|
||||
|
||||
handler Boot phase Start {
|
||||
title_font = font_load("/System/Library/Fonts/Supplemental/Arial.ttf")
|
||||
ui_build()
|
||||
ui_open(UI_MainMenu)
|
||||
}
|
||||
|
||||
handler Navigate phase Update {
|
||||
ui_tick(Input.key())
|
||||
if ui_clicked(UI_Quit) { quit() }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
ui_render()
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -5,12 +5,27 @@ category: structure
|
|||
kind: keyword
|
||||
tokens: var
|
||||
sig: var name: T = value
|
||||
tip: A mutable binding.
|
||||
tip: A mutable binding — at program scope, your game's persistent named state.
|
||||
order: 6
|
||||
---
|
||||
|
||||
A mutable binding. At program scope it is your game's persistent, named state — the modern replacement for numeric registers.
|
||||
A <code>var</code> declares a <b>mutable</b> binding: unlike `let`, you may reassign it later with `=`, `+=`, `-=`, and friends. Where it appears decides its lifetime — inside a handler or function body it is a local, and at program scope it is persistent, named game state that every handler shares. Program-scope `var`s are the modern replacement for numeric registers: instead of `reg(0)`, you write `var score: int = 0` and read and write `score` by name. Use `var` for anything that genuinely changes — accumulators, the current score, a mode flag, an elapsed-frame counter — and prefer `let` for values that never change.
|
||||
|
||||
```ludic
|
||||
var score: int = 0
|
||||
program Scorekeeper {
|
||||
var score: int = 0
|
||||
var elapsed_frames: int = 0
|
||||
|
||||
handler Tick phase Update {
|
||||
elapsed_frames = elapsed_frames + 1
|
||||
if elapsed_frames % 60 == 0 { score = score + 1 }
|
||||
}
|
||||
|
||||
handler DrawWorld phase Render {
|
||||
Screen.clear(Color.MidnightBlue)
|
||||
Screen.draw_text(x: 6, y: 4, text: "SCORE", color: Color.White, scale: 1)
|
||||
Screen.draw_number(x: 52, y: 4, value: score, color: Color.Gold, scale: 1)
|
||||
Screen.show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue