ludic/packages/ludic.things
Orkuncakilkaya 8a4f3f5ad6 ludic.things: a fact's record kept, in two halves swapped when a fact finds the queue empty
th_fact made a ThingFact per placement, removal and use, and the valley places and removes Things
all day. The records come back two drains after they were handed out, so a reader placing a Thing
while it reads the last drain's list never sees one change (the test holds exactly that).
thing_use takes ThingsState mut, since it pushes a fact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:45:40 +03:00
..
tests ludic.things: a fact's record kept, in two halves swapped when a fact finds the queue empty 2026-09-28 12:45:40 +03:00
chunks.ludic feat(ecs): things_now / things_changed - a consumer keeps the tick it read up to and asks only for the Things written since (untouched 64-row blocks skipped) 2026-09-27 22:13:45 +03:00
dispatch.ludic ludic.things: a fact's record kept, in two halves swapped when a fact finds the queue empty 2026-09-28 12:45:40 +03:00
facts.ludic ludic.things: a fact's record kept, in two halves swapped when a fact finds the queue empty 2026-09-28 12:45:40 +03:00
index.ludic ludic.things: a fact's record kept, in two halves swapped when a fact finds the queue empty 2026-09-28 12:45:40 +03:00
kinds.ludic feat(ecs): ludic.base Table<T> - dense rows of hot columns, generational handles, a spatial grid, kind indexes and an id map kept current by the setters; ludic.things on it 2026-09-27 15:52:51 +03:00
moves.ludic feat(ecs): chunks as a grouping - IntIndex takes sparse values; ludic.things' chunk column and things_drop_chunk 2026-09-27 22:07:26 +03:00
near.ludic feat(ecs): thing_nearest_within - the nearest of a kind strictly within a radius, from the grid or the kind's list 2026-09-27 22:25:34 +03:00
package.ludic feat(packages): ludic.things - placed things in a world: a Thing record, an open registry of kinds whose behaviour is function values, spatial queries, spawn / put / remove / move verbs, facts and a save section by kind key 2026-09-25 06:14:32 +03:00
port.ludic feat(packages): ludic.things - placed things in a world: a Thing record, an open registry of kinds whose behaviour is function values, spatial queries, spawn / put / remove / move verbs, facts and a save section by kind key 2026-09-25 06:14:32 +03:00
queries.ludic feat(ecs): ludic.base Table<T> - dense rows of hot columns, generational handles, a spatial grid, kind indexes and an id map kept current by the setters; ludic.things on it 2026-09-27 15:52:51 +03:00
README.md ludic.things: a fact's record kept, in two halves swapped when a fact finds the queue empty 2026-09-28 12:45:40 +03:00
spare.ludic ludic.things: things_spare_warm - spare records made up front, so play takes them from the first placement 2026-09-28 12:09:52 +03:00
store.ludic ludic.things: a fact's record kept, in two halves swapped when a fact finds the queue empty 2026-09-28 12:45:40 +03:00
system.ludic refactor(packages): 0.R5 - a question no longer starts its state - 64 lazy starts ('if st.x == null { st.x = ... }' inside a getter) are the state's defaults, gear__ensure is gone, and ludic migrate state --tighten took mut off 208 package parameters: things_all, thing_find, gear_charge, gear_level, jobs_state and the rest read 2026-09-27 00:51:03 +03:00
verbs.ludic ludic.things: a removed Thing's record placed again, oldest first past a lag of 32; things_reserve sizes the grid once 2026-09-28 11:52:23 +03:00

ludic.things

The things placed in a world - a tent, a fire ring, a branch on the ground, a snare - and the kinds they are. The game names the kinds and draws them; the package keeps every Thing, answers where they are, asks a kind what it does, reports what happened, and saves the ones worth keeping. Uses ludic.base and nothing else.

import "ludic.things"

A Thing

property Thing {
  uid, kind, id          # unique for the run; its place in ThingKinds; what the kind says it means
  look                   # how the game draws it ("" is not drawn)
  x, y, z, yaw
  owner                  # whoever put it down, -1 the world's own
  active                 # false: taken, hidden, used up for now
  reach                  # how close it is used from (0: its kind's)
  timer, used, n         # state the kind decides the meaning of
  sx, sz, syaw           # where using it puts the user, and which way that faces
  shared                 # the game copies it to others (a party)
}

The kinds

ThingKinds is an open registry, filled by the game - usually one def per kind in one file, so TH_<KEY> is the order they are written in. A kind's behaviour is function values; a null one is a question the kind ignores.

def ThingKinds fire { name: "The campfire", prompt: fn fire_prompt, use: fn fire_use }
def ThingKinds lure { name_of: fn bait_name, tick: fn bait_wear, put_down: true, saved: true }
field
name, name_of: fn(Thing) -> string what it is called (name_of answering "" falls back to name)
prompt: fn(Thing) -> string, use: fn(Thing) what using it would do, and doing it
tick: fn(Thing, float) hours of the world's clock, for every active one
reach, reach_of: fn(Thing) -> float how close it is used from
personal one belongs to whoever put it down
saved the save section keeps a placed one (the port has the last word)
put_down set down by someone rather than grown by the world: a restock leaves it (the port has the last word)
keeps keeps its used through a restock

A removed Thing's record is placed again. Ludic frees nothing, and a world places and takes away Things all day, so thing_remove keeps the record and thing_put / thing_spawn make the oldest one again - set back as new, with a new uid - once 32 are waiting. Hold a Thing across a removal only by its uid (thing_by_uid), never by the record.

A fact's record (things_facts()) is kept too: it comes back two drains after it was handed out, so read a drained list before the drain after next, and copy what must last longer.

The port

Every member has a default, so any may be left out of the bind:

export port ThingsWorld {
  ground: fn(float, float) -> float   # the height a Thing stands at (unbound: 0)
  placed: fn(Thing)                   # it exists now: draw it
  removed: fn(Thing)                  # gone for good: stop drawing it
  shown: fn(Thing)                    # its `active` changed
  moved: fn(Thing)                    # it stands somewhere else
  keeps: fn(Thing) -> bool            # the save carries it (unbound: its kind's `saved`)
  put_down: fn(Thing) -> bool         # a restock leaves it be (unbound: its kind's `put_down`)
  restored: fn(Thing)                 # a load put it back, after `placed`
}

placed, removed, shown and moved are called at once, inside the verb, so the game can make the actor the moment the Thing exists (a caller often scales or tints it on the next line).

API

thing_spawn(kind, look, x, y, z, yaw, id) -> Thing, thing_put(kind, look, x, z, yaw, sink, reach, id) -> Thing a new Thing, at a height or on the ground set into it by sink
thing_remove(t), things_drop(gone: fn(Thing) -> bool), things_clear() gone for good (told to the port); a new world, quietly
things_reserve(n), things_spare_count() room in the grid for n Things, made once at the start; how many removed records wait to be placed again
thing_move(t, x, z, yaw), thing_move_to(t, x, y, z, yaw) on the ground there, or at a height
thing_hide(t), thing_show(t), thing_set_active(t, on) active, told to the port
things_all(), things_count(), things_ready(), thing_by_uid(uid) every Thing, in the order placed; whether a world was ever set out
thing_find(kind, id), thing_nearest(kind, x, z), things_within(x, z, r), things_each(kind), things_each_all(kind), thing_dist2(t, x, z) where they are: only active ones answer except the _all; a kind below 0 is any; within is nearest first
thing_name(t), thing_prompt(t), thing_use(t), thing_can_use(t), thing_reach(t), thing_kind(t), thing_is_personal(t), thing_is_put_down(t) the dispatcher
things_tick(hours), things_restock() every active kind that ticks; every Thing the world grew back and unused
things_facts() -> Queue<ThingFact> { what, uid, kind, id, owner, x, z }: THING_PLACED, THING_REMOVED, THING_USED
thing_kind_ok(k), thing_kind_by_key(key) the kinds
things_system() -> System "things", PH_COMMIT: its save section - every active Thing the port keeps, its kind by KEY, so the game may reorder its kinds - and a reset that removes the kept ones for the load to put back

Tests

ludic test packages/ludic.things