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:
parent
0af0ef681f
commit
7f8a7ea7ed
10 changed files with 672 additions and 0 deletions
90
packages/ludic.things/README.md
Normal file
90
packages/ludic.things/README.md
Normal 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
|
||||||
|
```
|
||||||
69
packages/ludic.things/dispatch.ludic
Normal file
69
packages/ludic.things/dispatch.ludic
Normal 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/ludic.things/index.ludic
Normal file
11
packages/ludic.things/index.ludic
Normal 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"
|
||||||
63
packages/ludic.things/kinds.ludic
Normal file
63
packages/ludic.things/kinds.ludic
Normal 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
|
||||||
|
}
|
||||||
5
packages/ludic.things/package.ludic
Normal file
5
packages/ludic.things/package.ludic
Normal 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
|
||||||
16
packages/ludic.things/port.ludic
Normal file
16
packages/ludic.things/port.ludic
Normal 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 }
|
||||||
80
packages/ludic.things/queries.ludic
Normal file
80
packages/ludic.things/queries.ludic
Normal 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
|
||||||
|
}
|
||||||
110
packages/ludic.things/store.ludic
Normal file
110
packages/ludic.things/store.ludic
Normal 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) }
|
||||||
72
packages/ludic.things/system.ludic
Normal file
72
packages/ludic.things/system.ludic
Normal 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
|
||||||
|
}
|
||||||
156
packages/ludic.things/tests/things_test.ludic
Normal file
156
packages/ludic.things/tests/things_test.ludic
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue