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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 06:14:32 +03:00
parent 0af0ef681f
commit 7f8a7ea7ed
10 changed files with 672 additions and 0 deletions

View file

@ -0,0 +1,90 @@
# 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`](../ludic.base/README.md) and nothing else.
```ludic
import "ludic.things"
```
## A Thing
```ludic
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.
```ludic
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 |
## The port
Every member has a default, so any may be left out of the `bind`:
```ludic
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 |
| `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
```bash
ludic test packages/ludic.things
```

View file

@ -0,0 +1,69 @@
# dispatch.ludic - asking a Thing its kind's questions: what it is called, what using it would do,
# doing it, how close it is used from, and a tick for the kinds that have one
export function thing_kind(t: Thing) -> ThingKind { return ThingKinds[t.kind] }
# its own name, else its kind's, else ""
export function thing_name(t: Thing) -> string {
if t == null or not thing_kind_ok(t.kind) { return "" }
let k = ThingKinds[t.kind]
if k.name_of != null {
let s = k.name_of(t)
if s != "" { return s }
}
return k.name
}
export function thing_prompt(t: Thing) -> string {
if t == null or not thing_kind_ok(t.kind) { return "" }
let k = ThingKinds[t.kind]
if k.prompt == null { return "" }
return k.prompt(t)
}
# the kind does what using it does; the use is a fact whatever the kind made of it
export function thing_use(t: Thing) -> void {
if t == null or not thing_kind_ok(t.kind) { return }
let k = ThingKinds[t.kind]
th_fact(THING_USED, t)
if k.use != null { k.use(t) }
}
export function thing_can_use(t: Thing) -> bool {
return t != null and thing_kind_ok(t.kind) and ThingKinds[t.kind].use != null
}
# the kind's own reach question, else the Thing's reach, else the kind's
export function thing_reach(t: Thing) -> float {
if t == null or not thing_kind_ok(t.kind) { return 0.0 }
let k = ThingKinds[t.kind]
if k.reach_of != null { return k.reach_of(t) }
if t.reach > 0.0 { return t.reach }
return k.reach
}
export function thing_is_personal(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].personal }
# set down by someone rather than grown by the world: the kind says, and the port has the last word
export function thing_is_put_down(t: Thing) -> bool { return ThingsWorld.put_down(t) }
# every active Thing whose kind ticks, `hours` of the world's clock on
export function things_tick(hours: float) -> void {
let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if not t.active or not thing_kind_ok(t.kind) { continue }
let k = ThingKinds[t.kind]
if k.tick != null { k.tick(t, hours) }
}
}
# a restock: every Thing the world grew is back, and loses its `used` unless its kind keeps it;
# what was put down is left as it is
export function things_restock() -> void {
let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if thing_is_put_down(t) { continue }
if not ThingKinds[t.kind].keeps { t.used = 0 }
if not t.active { thing_show(t) }
}
}

View file

@ -0,0 +1,11 @@
# ludic.things - things placed in a world (a tent, a fire ring, a branch on the ground): the game
# names the kinds and draws them; the package keeps them, answers where they are, and saves them
module ludic_things uses ludic_base
numbers float
import "ludic.base"
import "kinds.ludic"
import "port.ludic"
import "store.ludic"
import "queries.ludic"
import "dispatch.ludic"
import "system.ludic"

View file

@ -0,0 +1,63 @@
# kinds.ludic - a Thing, and what a kind of Thing is: plain facts about the kind, and its
# behaviour as function values the dispatcher calls (a null one is a question the kind ignores)
export property Thing {
uid: int = 0 # unique for the run, never reused
kind: int = 0 # its place in ThingKinds
id: int = 0 # what the kind says it means (which sign, which item, which site)
look: string = "" # how the game draws it; "" is not drawn
x: float = 0.0
y: float = 0.0
z: float = 0.0
yaw: float = 0.0
owner: int = -1 # whoever put it down; -1 the world's own
active: bool = true # false: taken, hidden, used up for now
reach: float = 0.0 # how close it is used from; 0 is its kind's
timer: float = 0.0 # a clock the kind decides the meaning of
used: int = 0 # the kind's own state (split for today, which sign, ...)
n: int = 0 # a counter the kind decides the meaning of
sx: float = 0.0 # where using it puts the user, and which way that faces
sz: float = 0.0
syaw: float = 0.0
shared: bool = false # the game copies it to others (a party)
}
export property ThingKind {
key: string = "" # the entry's own name: what the save calls the kind
name: string = "" # what it is called when nothing about the one in front of you changes it
reach: float = 2.2 # how close it is used from, when the Thing does not say
personal: bool = false # one belongs to whoever put it down
saved: bool = false # the save's section keeps a placed one (the port has the last word)
put_down: bool = false # set down by someone rather than grown by the world
keeps: bool = false # keeps its `used` through a restock
name_of: fn(Thing) -> string = null # "" falls back to `name`
prompt: fn(Thing) -> string = null # what using it would do, "" nothing
use: fn(Thing) -> void = null
tick: fn(Thing, float) -> void = null # hours of the world's clock
reach_of: fn(Thing) -> float = null # a reach that depends on the one in front of you
}
# the game fills it: `def ThingKinds tent { name: "The tent", use: fn tent_use }`
export open registry ThingKinds of ThingKind as TH
export const THING_PLACED: int = 0
export const THING_REMOVED: int = 1
export const THING_USED: int = 2
# something happened to a Thing: placed, removed for good, or used
export property ThingFact {
what: int = 0
uid: int = 0
kind: int = 0
id: int = 0
owner: int = -1
x: float = 0.0
z: float = 0.0
}
export function thing_kind_ok(k: int) -> bool { return k >= 0 and k < TH_COUNT }
# the kind the save calls `key`, or -1
export function thing_kind_by_key(key: string) -> int {
for k in 0 .. TH_COUNT { if ThingKinds[k].key == key { return k } }
return -1
}

View file

@ -0,0 +1,5 @@
# ludic.things - the placed things of a world: a record per thing, an open registry of kinds
# whose behaviour is function values, spatial queries, facts and a save section. See README.md.
package "ludic.things"
version "0.1.0"
kind source

View file

@ -0,0 +1,16 @@
# port.ludic - what the things ask the game. Every member has a default: unbound, the ground is at
# 0, nothing is drawn, and the save keeps what its kind says to keep.
export port ThingsWorld {
ground: fn(float, float) -> float = fn th_flat # the height a moved Thing stands at
placed: fn(Thing) -> void = fn th_nothing # a Thing exists now: draw it
removed: fn(Thing) -> void = fn th_nothing # it is gone for good: stop drawing it
shown: fn(Thing) -> void = fn th_nothing # its `active` changed: show or hide it
moved: fn(Thing) -> void = fn th_nothing # it stands somewhere else now
keeps: fn(Thing) -> bool = fn th_kind_saves # the save's section carries it
put_down: fn(Thing) -> bool = fn th_kind_put_down # set down by someone: a restock leaves it be
restored: fn(Thing) -> void = fn th_nothing # a load put it back (after `placed`)
}
function th_flat(x: float, z: float) -> float { return 0.0 }
function th_nothing(t: Thing) -> void { }
function th_kind_saves(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].saved }
function th_kind_put_down(t: Thing) -> bool { return thing_kind_ok(t.kind) and ThingKinds[t.kind].put_down }

View file

@ -0,0 +1,80 @@
# queries.ludic - where the Things are. Only active Things answer, except thing_by_uid and
# things_each_all, which ask about everything that exists. A kind below 0 is any kind.
function th_of(t: Thing, kind: int) -> bool { return kind < 0 or t.kind == kind }
# the first active one of a kind (and of that id, unless id < 0), in the order placed
export function thing_find(kind: int, id: int) -> Thing {
let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if t.active and th_of(t, kind) and (id < 0 or t.id == id) { return t }
}
return null
}
export function thing_by_uid(uid: int) -> Thing {
let all = things_all()
for i in 0 .. len(all) { if all[i].uid == uid { return all[i] } }
return null
}
# the nearest active one of a kind to (x, z), or null
export function thing_nearest(kind: int, x: float, z: float) -> Thing {
var best: Thing = null
var bd = 0.0
let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if not t.active or not th_of(t, kind) { continue }
let d = thing_dist2(t, x, z)
if best == null or d < bd {
best = t
bd = d
}
}
return best
}
# the squared ground distance from a Thing to (x, z)
export function thing_dist2(t: Thing, x: float, z: float) -> float {
let dx = t.x - x
let dz = t.z - z
return dx * dx + dz * dz
}
# every active one within r of (x, z), nearest first
export function things_within(x: float, z: float, r: float) -> []Thing {
let out = new []Thing
let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if t.active and thing_dist2(t, x, z) <= r * r { th_insert(out, t, x, z) }
}
return out
}
function th_insert(out: []Thing, t: Thing, x: float, z: float) -> void {
push(out, t)
var j = len(out) - 1
while j > 0 and thing_dist2(out[j - 1], x, z) > thing_dist2(t, x, z) {
out[j] = out[j - 1]
j -= 1
}
out[j] = t
}
# every active one of a kind, in the order placed
export function things_each(kind: int) -> []Thing {
let out = new []Thing
let all = things_all()
for i in 0 .. len(all) { if all[i].active and th_of(all[i], kind) { push(out, all[i]) } }
return out
}
# every one of a kind, active or not
export function things_each_all(kind: int) -> []Thing {
let out = new []Thing
let all = things_all()
for i in 0 .. len(all) { if th_of(all[i], kind) { push(out, all[i]) } }
return out
}

View file

@ -0,0 +1,110 @@
# store.ludic - the Things, and the only verbs that make, remove, move, show and hide one; each
# placing and removing is a fact, and the port is told at once so the game can draw it
var th_list: []Thing = null
var th_uid: int = 0
var th_facts: Queue<ThingFact> = null
export function things_facts() -> Queue<ThingFact> {
if th_facts == null { th_facts = queue_new("things.facts") }
return th_facts
}
function th_fact(what: int, t: Thing) -> void {
let f = new ThingFact
f.what = what
f.uid = t.uid
f.kind = t.kind
f.id = t.id
f.owner = t.owner
f.x = t.x
f.z = t.z
q_push(things_facts(), f)
}
# whether a world has been set out (things_clear) since the program began
export function things_ready() -> bool { return th_list != null }
# every Thing, in the order placed; the list is the package's, never to be pushed to
export function things_all() -> []Thing {
if th_list == null { th_list = new []Thing }
return th_list
}
export function things_count() -> int { return len(things_all()) }
# a new world: every Thing forgotten, quietly (the game has already dropped its drawings)
export function things_clear() -> void { th_list = new []Thing }
export function thing_spawn(kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
return th_place(new Thing, kind, look, x, y, z, yaw, id)
}
function th_place(t: Thing, kind: int, look: string, x: float, y: float, z: float, yaw: float, id: int) -> Thing {
t.kind = kind
t.look = look
t.x = x
t.y = y
t.z = z
t.yaw = yaw
t.id = id
th_uid += 1
t.uid = th_uid
push(things_all(), t)
ThingsWorld.placed(t)
th_fact(THING_PLACED, t)
return t
}
# a Thing on the ground (the port's), set into it by `sink`, used from `reach`
export function thing_put(kind: int, look: string, x: float, z: float, yaw: float, sink: float, reach: float, id: int) -> Thing {
let t = new Thing
t.reach = reach
return th_place(t, kind, look, x, ThingsWorld.ground(x, z) - sink, z, yaw, id)
}
# gone for good
export function thing_remove(t: Thing) -> void {
if t == null { return }
th_marked = t.uid
things_drop(fn th_is_marked)
th_marked = -1
}
var th_marked: int = -1
function th_is_marked(t: Thing) -> bool { return t.uid == th_marked }
# every Thing `gone` says so is removed for good
export function things_drop(gone: fn(Thing) -> bool) -> void {
let keep = new []Thing
let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if gone(t) {
ThingsWorld.removed(t)
th_fact(THING_REMOVED, t)
} else {
push(keep, t)
}
}
th_list = keep
}
export function thing_move(t: Thing, x: float, z: float, yaw: float) -> void {
thing_move_to(t, x, ThingsWorld.ground(x, z), z, yaw)
}
export function thing_move_to(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
t.x = x
t.y = y
t.z = z
t.yaw = yaw
ThingsWorld.moved(t)
}
export function thing_set_active(t: Thing, on: bool) -> void {
if t == null { return }
t.active = on
ThingsWorld.shown(t)
}
export function thing_hide(t: Thing) -> void { thing_set_active(t, false) }
export function thing_show(t: Thing) -> void { thing_set_active(t, true) }

View file

@ -0,0 +1,72 @@
# system.ludic - the things as a system: its save section carries every active Thing the port
# keeps, by its kind's KEY (so the game may reorder its kinds), and a load puts each back
export function things_save() -> Val {
let l = Value.list()
let all = things_all()
for i in 0 .. len(all) {
let t = all[i]
if t.active and thing_kind_ok(t.kind) and ThingsWorld.keeps(t) { Value.add(l, th_to_json(t)) }
}
let v = Value.object()
Value.put(v, "placed", l)
return v
}
function th_to_json(t: Thing) -> Val {
let o = Value.object()
sv_put_str(o, "kind", ThingKinds[t.kind].key)
sv_put_str(o, "look", t.look)
sv_put_int(o, "id", t.id)
sv_put_int(o, "owner", t.owner)
sv_put_float(o, "x", t.x)
sv_put_float(o, "y", t.y)
sv_put_float(o, "z", t.z)
sv_put_float(o, "yaw", t.yaw)
sv_put_float(o, "reach", t.reach)
sv_put_float(o, "timer", t.timer)
sv_put_int(o, "used", t.used)
sv_put_int(o, "n", t.n)
sv_put_float(o, "sx", t.sx)
sv_put_float(o, "sz", t.sz)
sv_put_float(o, "syaw", t.syaw)
return o
}
# every kept Thing removed for good: what a load or a new trip is about to put back
export function things_reset() -> void { things_drop(fn th_kept) }
function th_kept(t: Thing) -> bool { return ThingsWorld.keeps(t) }
# a kind this build does not know is dropped
export function things_load(v: Val, version: int) -> void {
things_reset()
if v == null or Value.kind(v) != 6 or Value.has(v, "placed") == 0 { return }
let l = Value.get(v, "placed")
for i in 0 .. Value.count(l) { th_from_json(Value.at(l, i)) }
}
function th_from_json(o: Val) -> void {
let k = thing_kind_by_key(sv_str(o, "kind", ""))
if k < 0 { return }
let t = new Thing
t.owner = sv_int(o, "owner", -1)
t.reach = sv_float(o, "reach", 0.0)
t.timer = sv_float(o, "timer", 0.0)
t.used = sv_int(o, "used", 0)
t.n = sv_int(o, "n", 0)
t.sx = sv_float(o, "sx", 0.0)
t.sz = sv_float(o, "sz", 0.0)
t.syaw = sv_float(o, "syaw", 0.0)
let x = sv_float(o, "x", 0.0)
let z = sv_float(o, "z", 0.0)
let y = sv_float(o, "y", ThingsWorld.ground(x, z))
th_place(t, k, sv_str(o, "look", ""), x, y, z, sv_float(o, "yaw", 0.0), sv_int(o, "id", 0))
ThingsWorld.restored(t)
}
export function things_system() -> System {
let s = system_new("things", PH_COMMIT)
s.reset = fn things_reset
s.save = fn things_save
s.load = fn things_load
return s
}

View file

@ -0,0 +1,156 @@
# things_test.ludic - a toy camp: kinds as data with their behaviour as function values, the
# verbs and what the port hears of them, the spatial questions, the facts, and the save by key
import "ludic.things"
import "ludic.base"
program ThingsTest {
numbers float
var drawn: int = 0
var gone: int = 0
var shows: int = 0
var back: int = 0
var wood: int = 0
function lit_prompt(t: Thing) -> string {
if t.used != 0 { return "Add wood" }
return "Light it"
}
function lit_use(t: Thing) -> void { t.used = 1 }
function tent_name(t: Thing) -> string {
if t.owner == 0 { return "Your tent" }
return ""
}
function branch_use(t: Thing) -> void {
wood += 1
thing_hide(t)
}
function lure_tick(t: Thing, hours: float) -> void {
t.timer -= hours
if t.timer < 0.0 { thing_hide(t) }
}
function far_reach(t: Thing) -> float { return 9.0 }
def ThingKinds tent { name: "A tent", name_of: fn tent_name }
def ThingKinds fire { name: "The fire", prompt: fn lit_prompt, use: fn lit_use, keeps: true }
def ThingKinds branch { name: "Firewood", reach: 1.5, use: fn branch_use }
def ThingKinds lure { name: "A lure", tick: fn lure_tick, put_down: true, saved: true }
def ThingKinds sign { name: "A sign", reach_of: fn far_reach }
function hill(x: float, z: float) -> float { return x * 0.5 }
function on_placed(t: Thing) -> void { drawn += 1 }
function on_removed(t: Thing) -> void { gone += 1 }
function on_shown(t: Thing) -> void { shows += 1 }
function on_restored(t: Thing) -> void { back += 1 }
bind ThingsWorld { ground: fn hill, placed: fn on_placed, removed: fn on_removed, shown: fn on_shown, restored: fn on_restored }
function put(kind: int, x: float, z: float) -> Thing { return thing_spawn(kind, "m", x, 0.0, z, 0.0, 0) }
function facts() -> []ThingFact { return q_drain(things_facts()) }
test "a spawn is drawn, numbered and said" {
things_clear()
let a = put(TH_TENT, 1.0, 1.0)
let b = put(TH_FIRE, 2.0, 2.0)
expect_eq(drawn, 2)
expect(a.uid != b.uid)
expect_eq(things_count(), 2)
let fs = facts()
expect_eq(len(fs), 2)
expect_eq(fs[1].what, THING_PLACED)
expect_eq(fs[1].kind, TH_FIRE)
expect(thing_by_uid(b.uid) == b)
}
test "the spatial questions see only what is active" {
things_clear()
let near = put(TH_BRANCH, 1.0, 0.0)
let far = put(TH_BRANCH, 5.0, 0.0)
put(TH_FIRE, 3.0, 0.0)
expect(thing_nearest(TH_BRANCH, 0.0, 0.0) == near)
expect(thing_nearest(-1, 3.2, 0.0).kind == TH_FIRE)
expect_eq(len(things_within(0.0, 0.0, 3.5)), 2)
expect(things_within(6.0, 0.0, 10.0)[0] == far)
thing_hide(near)
expect(thing_nearest(TH_BRANCH, 0.0, 0.0) == far)
expect_eq(len(things_each(TH_BRANCH)), 1)
expect_eq(len(things_each_all(TH_BRANCH)), 2)
expect(thing_find(TH_BRANCH, -1) == far)
expect(thing_find(TH_TENT, -1) == null)
expect_eq(shows, 1)
}
test "remove and move tell the port" {
things_clear()
let a = put(TH_TENT, 1.0, 1.0)
put(TH_TENT, 2.0, 2.0)
facts()
thing_remove(a)
expect_eq(gone, 1)
expect_eq(things_count(), 1)
expect_eq(facts()[0].what, THING_REMOVED)
let b = thing_find(TH_TENT, -1)
thing_move(b, 4.0, 0.0, 1.0)
expect_near(b.y, 2.0, 0.001)
let c = thing_put(TH_BRANCH, "stick", 2.0, 0.0, 0.0, 0.25, 1.9, 7)
expect_near(c.y, 0.75, 0.001)
expect_near(thing_reach(c), 1.9, 0.001)
expect_eq(c.id, 7)
}
test "the kind answers: name, prompt, use, reach" {
things_clear()
let t = put(TH_TENT, 0.0, 0.0)
expect(thing_name(t) == "A tent")
t.owner = 0
expect(thing_name(t) == "Your tent")
let f = put(TH_FIRE, 0.0, 0.0)
expect(thing_prompt(f) == "Light it")
thing_use(f)
expect(thing_prompt(f) == "Add wood")
expect(thing_prompt(t) == "")
expect(not thing_can_use(t))
expect_near(thing_reach(f), 2.2, 0.001)
expect_near(thing_reach(put(TH_BRANCH, 0.0, 0.0)), 1.5, 0.001)
expect_near(thing_reach(put(TH_SIGN, 0.0, 0.0)), 9.0, 0.001)
let fs = facts()
expect_eq(fs[len(fs) - 3].what, THING_USED)
}
test "ticks and a restock" {
things_clear()
let l = put(TH_LURE, 0.0, 0.0)
l.timer = 1.0
let b = put(TH_BRANCH, 0.0, 0.0)
let f = put(TH_FIRE, 0.0, 0.0)
thing_use(b)
thing_use(f)
expect_eq(wood, 1)
things_tick(0.6)
expect(l.active)
things_tick(0.6)
expect(not l.active)
things_restock()
expect(b.active)
expect(not l.active)
expect_eq(f.used, 1)
}
test "the save keeps what the port keeps, by the kind's key" {
things_clear()
let l = put(TH_LURE, 3.0, 4.0)
l.owner = 0
l.timer = 2.5
put(TH_TENT, 0.0, 0.0)
let root = save_tree()
save_section(root, "things", 1, things_save())
let text = save_encode(root)
things_clear()
put(TH_LURE, 9.0, 9.0)
let n = load_section(save_decode(text), "things")
things_load(n.data, n.version)
expect_eq(things_count(), 1)
let t = thing_find(TH_LURE, -1)
expect_near(t.x, 3.0, 0.001)
expect_near(t.timer, 2.5, 0.001)
expect_eq(t.owner, 0)
expect_eq(back, 1)
}
}