diff --git a/packages/ludic.things/README.md b/packages/ludic.things/README.md new file mode 100644 index 00000000..134acb83 --- /dev/null +++ b/packages/ludic.things/README.md @@ -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_` 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` | `{ 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 +``` diff --git a/packages/ludic.things/dispatch.ludic b/packages/ludic.things/dispatch.ludic new file mode 100644 index 00000000..ebc9bd74 --- /dev/null +++ b/packages/ludic.things/dispatch.ludic @@ -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) } + } +} diff --git a/packages/ludic.things/index.ludic b/packages/ludic.things/index.ludic new file mode 100644 index 00000000..760476c8 --- /dev/null +++ b/packages/ludic.things/index.ludic @@ -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" diff --git a/packages/ludic.things/kinds.ludic b/packages/ludic.things/kinds.ludic new file mode 100644 index 00000000..cad1cacc --- /dev/null +++ b/packages/ludic.things/kinds.ludic @@ -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 +} diff --git a/packages/ludic.things/package.ludic b/packages/ludic.things/package.ludic new file mode 100644 index 00000000..75b22759 --- /dev/null +++ b/packages/ludic.things/package.ludic @@ -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 diff --git a/packages/ludic.things/port.ludic b/packages/ludic.things/port.ludic new file mode 100644 index 00000000..8070e0b6 --- /dev/null +++ b/packages/ludic.things/port.ludic @@ -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 } diff --git a/packages/ludic.things/queries.ludic b/packages/ludic.things/queries.ludic new file mode 100644 index 00000000..22d17e0a --- /dev/null +++ b/packages/ludic.things/queries.ludic @@ -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 +} diff --git a/packages/ludic.things/store.ludic b/packages/ludic.things/store.ludic new file mode 100644 index 00000000..4c5351ad --- /dev/null +++ b/packages/ludic.things/store.ludic @@ -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 = null + +export function things_facts() -> Queue { + 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) } diff --git a/packages/ludic.things/system.ludic b/packages/ludic.things/system.ludic new file mode 100644 index 00000000..290ab3ca --- /dev/null +++ b/packages/ludic.things/system.ludic @@ -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 +} diff --git a/packages/ludic.things/tests/things_test.ludic b/packages/ludic.things/tests/things_test.ludic new file mode 100644 index 00000000..65616462 --- /dev/null +++ b/packages/ludic.things/tests/things_test.ludic @@ -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) + } +}