Merge branch 'lang/foundations' into lang/ecs

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 22:56:57 +03:00
commit b9d0cca281
32 changed files with 445 additions and 58 deletions

View file

@ -48,8 +48,10 @@ import "ludic.nav"
| `nav_save_file(kind, path)`, `nav_load_file(kind, path)`, `nav_reset()` | a baked mesh written and read; every mesh let go |
| `nav_nearest(kind, x, y, z) -> bool`, `nav_near_x/y/z()` | the nearest walkable point within a couple of metres |
| `nav_path(kind, filter, sx, sy, sz, ex, ey, ez) -> int`, `nav_corners()`, `nav_corner_x/y/z(i)`, `nav_partial(kind)` | a way as corners |
| `nav_next_corner(kind, filter, sx, sy, sz, ex, ey, ez) -> bool` | the next corner of that way, read back as the nearest point is (a walker's one question a frame) |
| `nav_straight(kind, filter, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable |
| `nav_random_near(kind, filter, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed |
| `nav_asked()`, `nav_found()`, `nav_short()` | paths asked for, found and cut short since the meshes loaded (a game's measure of how often its walkers had a way) |
| `nav_area_cost(kind, filter, area, cost)` | how dear a kind of ground is to cross by that filter |
## Tests

View file

@ -47,8 +47,8 @@ export function nav_polygons(nav_st: NavState, kind: int) -> int {
return nvc_polys(h)
}
# a kind's mesh written to a file, and read back. Reading holds the file's bytes for good (a slice
# is never given back), so a mesh is loaded once per map, never per frame
# a kind's mesh written to a file, and read back - through the asset pack when it is in one - into the
# shim's own memory, which it frees once the tiles are made, so a map swap leaves nothing behind
export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool {
let h = nv_mesh(nav_st, kind)
if h == null { return false }
@ -59,8 +59,14 @@ export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool
return Fs.write_bytes(path, buf, n)
}
export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool {
if not Fs.exists(path) { return false }
let buf = Fs.read_bytes(path)
if buf == null or len(buf) == 0 { return false }
return nv_set(nav_st, kind, nvc_load(buf, len(buf)))
let f = file_open(path, "rb")
if f == null { return false }
file_seek(f, 0, 2)
let n = file_tell(f)
file_seek(f, 0, 0)
let buf = nvc_stage(n)
var got = 0
if buf != null { got = file_read(f, buf, n) }
file_close(f)
return nv_set(nav_st, kind, nvc_load_staged(buf, got))
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -13,3 +13,5 @@ extern function nvc_path(h: pointer, f: int, sx: float, sy: float, sz: float, ex
extern function nvc_partial(h: pointer) -> int = "nav_partial"
extern function nvc_raycast(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
extern function nvc_random_near(h: pointer, f: int, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near"
extern function nvc_stage(size: int) -> pointer = "nav_stage"
extern function nvc_load_staged(buf: pointer, size: int) -> pointer = "nav_load_staged"

View file

@ -7,6 +7,7 @@
#include <DetourNavMeshQuery.h>
#include <DetourCommon.h>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <vector>

View file

@ -100,3 +100,12 @@ NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
}
return nav_open(mesh);
}
// a file read straight into the shim's own memory rather than a Ludic slice (which is never given
// back): nav_stage hands out the room, nav_load_staged parses it and frees it, whatever the outcome
NAV_SHIM unsigned char *nav_stage(int size) { return size > 0 ? static_cast<unsigned char *>(malloc(size)) : nullptr; }
NAV_SHIM void *nav_load_staged(unsigned char *buf, int size) {
void *h = buf ? nav_load(buf, size) : nullptr;
free(buf);
return h;
}

View file

@ -19,6 +19,16 @@ export function nav_random_near(nav_st: NavState, kind: int, filter: int, x: flo
return nvc_random_near(h, filter, x, y, z, r, seed, nav_st.nv_near) == 1
}
# the next corner of the way from (sx, sy, sz) toward (ex, ey, ez) - the first after the start - read
# back as the nearest point is (nav_near_x / _z), so a walker's question and a goal share one answer
export function nav_next_corner(nav_st: mut NavState, kind: int, filter: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> bool {
if nav_path(nav_st, kind, filter, sx, sy, sz, ex, ey, ez) < 2 { return false }
nav_st.nv_near[0] = nav_st.nv_out[3]
nav_st.nv_near[1] = nav_st.nv_out[4]
nav_st.nv_near[2] = nav_st.nv_out[5]
return true
}
# a way from one point to another: its corners, first the start and last the end (or as near the end
# as the mesh reaches - nav_partial says which); -1 when either end is off the mesh
export function nav_path(nav_st: mut NavState, kind: int, filter: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> int {
@ -26,9 +36,17 @@ export function nav_path(nav_st: mut NavState, kind: int, filter: int, sx: float
nav_st.nv_count = 0
if h == null { return -1 }
let n = nvc_path(h, filter, sx, sy, sz, ex, ey, ez, nav_st.nv_out, NAV_CORNERS)
nav_st.nv_asked += 1
if n > 0 { nav_st.nv_count = n }
if n >= 2 { nav_st.nv_found += 1 }
if n > 0 and nvc_partial(h) == 1 { nav_st.nv_short += 1 }
return n
}
# how many paths were asked for, found, and cut short (the end out of reach) since the meshes loaded
export function nav_asked(nav_st: NavState) -> int { return nav_st.nv_asked }
export function nav_found(nav_st: NavState) -> int { return nav_st.nv_found }
export function nav_short(nav_st: NavState) -> int { return nav_st.nv_short }
export function nav_partial(nav_st: NavState, kind: int) -> bool {
let h = nv_mesh(nav_st, kind)
return h != null and nvc_partial(h) == 1

View file

@ -42,6 +42,9 @@ export state NavState {
nv_near: []float = floats(3)
nv_cfg: []float = floats(7) # the configuration handed to the shim, filled in place per build
nv_empty: []float = floats(1) # what the shim is handed for no cylinders or no footprints
nv_asked: int = 0 # paths asked for, found (two corners or more), and cut short
nv_found: int = 0
nv_short: int = 0
}
function nv_none() -> []pointer {
let m = new []pointer
@ -66,4 +69,7 @@ export function nav_drop(nav_st: mut NavState, kind: int) -> void { nv_set(nav_s
export function nav_reset(nav_st: mut NavState) -> void {
for k in 0 .. NAV_KINDS { nv_set(nav_st, k, null) }
nav_st.nv_count = 0
nav_st.nv_asked = 0
nav_st.nv_found = 0
nav_st.nv_short = 0
}

View file

@ -76,6 +76,18 @@ program NavTest {
expect_eq(nav_near_z(nav_st), z7)
}
test "the next corner is the path's second, read back as the nearest point" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 0, true))
let n = nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0)
let cx = nav_corner_x(nav_st, 1)
let cz = nav_corner_z(nav_st, 1)
expect(n >= 3)
expect(nav_next_corner(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0))
expect_near(nav_near_x(nav_st), cx, 0.0001)
expect_near(nav_near_z(nav_st), cz, 0.0001)
expect(not nav_next_corner(nav_st, NAV_PERSON, 0, -50.0, 0.0, -50.0, 35.0, 0.0, 20.0))
}
test "a filter that dislikes a band of thicket walks round it; the plain one goes through" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 3, false))
nav_area_cost(nav_st, NAV_PERSON, 1, 2, 20.0)

View file

@ -87,10 +87,18 @@ export port NpcWorld { # every member has a default: a flat dry field at n
next_id: fn() -> int, body_of: fn(int) -> int # an id across machines; a look seed's body
taken: fn(string) -> bool # a name the place keeps
born / gone: fn(NpcPerson) -> void # draw it, stop drawing it
way: fn(NpcPerson, x, z) -> bool, way_x, way_z # the next corner of its way toward (x, z)
}
export port NpcTalk { say: fn(NpcPerson, int) -> string, greeting: fn(NpcPerson) -> string }
```
**A person walks by the world's way where the world has one.** `way` is asked for the next corner
toward where it is going - every half second, on reaching the corner, and at once for a new target -
and the person walks corner to corner, but begins its errand only at the target itself. With no
answer (the default) it walks straight and steps round what is in the way, as before; the step
round and the twenty seconds' give-up stay as the fallback. Maroon Lake binds `way` to its navmesh
by a person's taste.
## API
| | |

View file

@ -47,6 +47,11 @@ export property NpcPerson {
via_on: bool = false # a step round whatever is in the way
vx: float = 0.0
vz: float = 0.0
wx: float = 0.0 # the corner of its way it walks to now (NpcWorld.way)
wz: float = 0.0
wtx: float = 0.0 # the target that corner was asked for: a new target asks again
wtz: float = 0.0
way_t: float = 0.0 # seconds until it asks the way again
detours: int = 0
fails: int = 0
stall: float = 0.0 # seconds walking without moving

View file

@ -10,6 +10,7 @@ import "ground.ludic"
import "spots.ludic"
import "routine.ludic"
import "walk.ludic"
import "way.ludic"
import "act.ludic"
import "names.ludic"
import "census.ludic"

View file

@ -25,6 +25,11 @@ export port NpcWorld {
taken: fn(string) -> bool = fn nw_free # a name the place already uses
born: fn(NpcPerson) -> void = fn nw_nothing # a new one (or a slot reused): draw it
gone: fn(NpcPerson) -> void = fn nw_nothing # left: stop drawing it
# (p, x, z): the next corner of this person's way toward (x, z) into way_x / _z; false, the
# straight line with its steps round (a navmesh's path by a person's taste, in Maroon Lake)
way: fn(NpcPerson, float, float) -> bool = fn nw_no_way
way_x: fn() -> float = fn nw_asked_x
way_z: fn() -> float = fn nw_asked_z
}
# what the story says through them: the words of a line whose `fill` is set ("" when it has nothing
@ -76,3 +81,4 @@ export function npc_near(npc_st: mut NpcState, x: float, z: float) -> float {
}
export function npc_near_i(npc_st: NpcState) -> int { return npc_st.np_near_i }
function nw_no_way(p: NpcPerson, x: float, z: float) -> bool { return false }

View file

@ -0,0 +1,82 @@
# way_test.ludic - a person walks by the world's way where the world has one: round a wall the
# straight line runs into, by the corners NpcWorld.way gives, and arrives only where it was going -
# never at a corner
import "ludic.npc"
import "ludic.base"
import "fake"
program NpcWayTest {
numbers float
bind NpcWorld {
ground: fn fk_ground
water: fn fk_water
allowed: fn fk_allowed
push: fn fk_push
pushed_x: fn fk_pushed_x
pushed_z: fn fk_pushed_z
clear: fn fk_clear
hour: fn fk_hour_now
day: fn fk_day_now
players: fn fk_players
player_on: fn fk_player_on
player_x: fn fk_x
player_z: fn fk_z
want: fn fk_wanted
extra: fn fk_extra_kind
taken: fn fk_taken
born: fn fk_on_born
gone: fn fk_on_gone
way: fn wt_way
way_x: fn wt_x
way_z: fn wt_z
}
bind NpcTalk { say: fn tk_say, greeting: fn tk_greeting }
def NpcActs sit { }
def NpcActs fish { turns: false }
def NpcKinds from "kinds.lres"
def NpcNames from "names.lres"
def NpcLines from "lines.lres"
function tk_say(p: NpcPerson, line: int) -> string { return "" }
function tk_greeting(p: NpcPerson) -> string { return "" }
function tk_start(npc_st: mut NpcState, p: NpcPerson) -> void { npc_start_near(npc_st, p, 0.0, 0.0, 0.0, 10.0) }
state WayTestState {
wx: float = 0.0
wz: float = 0.0
asked: int = 0
}
# a wall across x = 20 for |z| < 15 (a straight walk to (38, -25) meets it): the way goes round its
# north end, then on to the target - which is clear of where the fake place forbids stopping
function wt_way(way_test_st: mut WayTestState, p: NpcPerson, x: float, z: float) -> bool {
way_test_st.asked += 1
way_test_st.wx = x
way_test_st.wz = z
if p.x < 19.0 and p.z < 16.0 {
way_test_st.wx = 20.0
way_test_st.wz = 17.0
}
return true
}
function wt_x(way_test_st: WayTestState) -> float { return way_test_st.wx }
function wt_z(way_test_st: WayTestState) -> float { return way_test_st.wz }
test "it walks round the wall by the corners it is given and begins only where it was going" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState, way_test_st: WayTestState) {
fk_setup(npc_st, npc_tests_fake_st)
npc_tests_fake_st.fk_want = 1
let p = npc_spawn(npc_st, NPCK_HIKER)
npc_set_at(p, 0.0, 0.0)
npc_go(npc_st, p, 38.0, -25.0, NPCA_SIT, 30.0)
var north = -100.0
var k = 0
while p.act == NPCA_WALK and k < 2000 {
npc_tick(npc_st, 0.05)
if p.x > 19.0 and p.x < 21.0 { north = Math.max(north, p.z) }
k += 1
}
expect(north > 14.0)
expect_eq(p.act, NPCA_SIT)
expect(npc_d(p.x, p.z, 38.0, -25.0) < 1.3)
expect(way_test_st.asked > 5)
}
}

View file

@ -1,6 +1,6 @@
# walk.ludic - walking to the target: turn toward it, step, slid round a trunk or a tent, never into
# the water and never up a scramble - step round it instead - and give up on a target that gets no
# closer for twenty seconds
# walk.ludic - walking to the target by the world's way where it has one (way.ludic): turn toward the
# next corner, step, slid round a trunk or a tent, never into the water and never up a scramble - step
# round it instead - and give up on a target that gets no closer for twenty seconds
const NP_BODY_R: float = 0.35 # a person's width, for the still things
export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
@ -10,12 +10,17 @@ export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void
gx = p.vx
gz = p.vz
}
let dx = gx - p.x
let dz = gz - p.z
if Math.sqrt(dx * dx + dz * dz) < 1.2 {
if npc_d(p.x, p.z, gx, gz) < 1.2 {
np_arrive(npc_st, p)
return
}
if not p.via_on {
np_way(p, dt)
gx = p.wx
gz = p.wz
}
let dx = gx - p.x
let dz = gz - p.z
let dg = npc_d(p.x, p.z, p.tx, p.tz)
if dg < p.best - 1.0 {
p.best = dg
@ -79,28 +84,3 @@ function np_arrive(npc_st: mut NpcState, p: NpcPerson) -> void {
p.stall = 0.0
if p.act != NPCA_WALK { np_fact(npc_st, NPC_F_ARRIVED, p, -1) }
}
function np_detour(p: NpcPerson, want: float) -> bool {
for k in 0 .. 8 {
let ang = want + float((k / 2 + 1) * (1 - 2 * (k % 2))) * 0.6
let x = p.x - Math.sin(ang) * 6.0
let z = p.z - Math.cos(ang) * 6.0
if npc_line_ok(p.x, p.z, x, z) {
p.via_on = true
p.vx = x
p.vz = z
p.yaw = ang
return true
}
}
return false
}
function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void {
p.fails += 1
p.via_on = false
if p.fails >= 3 {
p.fails = 0
npc_wander(npc_st, p)
} else { npc_plan(npc_st, p) }
}

View file

@ -0,0 +1,45 @@
# way.ludic - where a person walks when the straight line will not do: the world's way (NpcWorld.way,
# a navmesh by a person's taste in Maroon Lake), and without one a step round what is in the way,
# and giving the target up after three tries
# the next corner toward the target, asked again every half second, on reaching it, or for a new target
const NP_WAY_EVERY: float = 0.5
function np_way(p: NpcPerson, dt: float) -> void {
p.way_t = p.way_t - dt
let fresh = p.wtx != p.tx or p.wtz != p.tz
if p.way_t > 0.0 and not fresh and npc_d(p.x, p.z, p.wx, p.wz) >= 1.0 { return }
p.way_t = NP_WAY_EVERY
p.wtx = p.tx
p.wtz = p.tz
p.wx = p.tx
p.wz = p.tz
if NpcWorld.way(p, p.tx, p.tz) {
p.wx = NpcWorld.way_x()
p.wz = NpcWorld.way_z()
}
}
function np_detour(p: NpcPerson, want: float) -> bool {
for k in 0 .. 8 {
let ang = want + float((k / 2 + 1) * (1 - 2 * (k % 2))) * 0.6
let x = p.x - Math.sin(ang) * 6.0
let z = p.z - Math.cos(ang) * 6.0
if npc_line_ok(p.x, p.z, x, z) {
p.via_on = true
p.vx = x
p.vz = z
p.yaw = ang
return true
}
}
return false
}
function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void {
p.fails += 1
p.via_on = false
if p.fails >= 3 {
p.fails = 0
npc_wander(npc_st, p)
} else { npc_plan(npc_st, p) }
}

View file

@ -163,7 +163,11 @@ function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, no
render3d_st.gltf_dir = dir
let text = Fs.read_text(dir + "/" + file)
if text == null { print(`gltf: cannot read {dir}/{file}`); return null }
# the last file's tree goes first (it is read only until the next load: ludic.anim's read.ludic),
# and the text once parsed - nothing gives memory back on its own (plan 23 of maroon-lake)
if render3d_st.gltf_doc != null { Json.free(render3d_st.gltf_doc) }
render3d_st.gltf_doc = Json.parse(text)
free(text)
let buffers = value_get(render3d_st.gltf_doc, "buffers")
let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri"))
render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb")

View file

@ -6,8 +6,10 @@ export function save_parse(text: string, f: SaveFormat) -> SaveRead {
if text == null { return read_new(SAVE_NONE, null) }
if not save_text_intact(text) { return read_new(SAVE_CORRUPT, null) }
let v = Json.parse(text)
if value_kind(v) != 6 { return read_new(SAVE_CORRUPT, null) } # 6 is an object
if f.is_one != null and not f.is_one(v) { return read_new(SAVE_CORRUPT, null) }
if value_kind(v) != 6 or (f.is_one != null and not f.is_one(v)) { # 6 is an object
if v != null { Json.free(v) } # a refused tree is let go
return read_new(SAVE_CORRUPT, null)
}
return read_new(SAVE_OK, v)
}

View file

@ -92,6 +92,10 @@ function telemetry__read_obj(path: string) -> Val {
let s = Fs.read_text(path)
if s == null { return null }
let v = Json.parse(s)
if v == null or Value.kind(v) != 6 { return null }
if v == null { return null }
if Value.kind(v) != 6 {
Json.free(v)
return null
}
return v
}

View file

@ -46,9 +46,18 @@ export port WildlifeWorld { # every member has a default: a flat dry mead
next_id: fn() -> int # an animal's id across machines
born: fn(WildAnimal) -> void # draw it
placed: fn(WildRange, int) -> void # a range was set out: put its water and forage down
way: fn(WildAnimal, float, float) -> bool # the next corner of its way toward (x, z), into way_x / _z
goal: fn(WildAnimal, x, z, r, seed) -> bool # a spot about r from (x, z) it can walk to, into way_x / _z
}
```
**An animal walks by the world's way where the world has one.** Wandering and going to water or
forage face the next corner `way` gives (asked again every half second, or on reaching the corner),
and a wander's target is a spot `goal` says it can reach, seeded by the same two draws the old
target took - so binding them changes where an animal walks and never the order of the dice. With
no answer (the defaults) it walks straight and turns 75 degrees off water and a climb, as before.
Maroon Lake binds both to its navmesh, walked by the species' habitat's taste.
## API
| | |

View file

@ -41,6 +41,9 @@ export property WildAnimal {
home_z: float = 0.0
tx: float = 0.0
tz: float = 0.0
wx: float = 0.0 # the corner of its way it is walking to now (WildlifeWorld.way)
wz: float = 0.0
way_t: float = 0.0 # seconds until it asks the way again; 0 asks at once
thirst: float = 0.0 # 0..100: over 60 it goes looking for water
hunger: float = 0.0
scat_t: float = 0.0 # game minutes until it might leave sign

View file

@ -31,7 +31,7 @@ export function wildlife_tick_ground(wildlife_st: mut WildlifeState, a: WildAnim
}
return
}
wl_face(a, a.tx, a.tz, 2.0, dt)
wl_head(a, a.tx, a.tz, 2.0, dt)
a.speed = s.walk
wl_step(wildlife_st, a, dt)
if wl_at_target(a, 1.5) or a.timer < 0.0 {

View file

@ -18,6 +18,7 @@ import "ranges.ludic"
import "migrate.ludic"
import "senses.ludic"
import "steer.ludic"
import "way.ludic"
import "lures.ludic"
import "needs.ludic"
import "ground.ludic"

View file

@ -46,6 +46,13 @@ export port WildlifeWorld {
next_id: fn() -> int = fn wl_next_id
born: fn(WildAnimal) -> void = fn wl_nothing # a new animal: draw it
placed: fn(WildRange, int) -> void = fn wl_range_nothing # a range was set out
# (a, x, z): the next corner of this animal's way toward (x, z) into way_x / _z; false, the
# straight line (a navmesh's path by its habitat's taste, in Maroon Lake)
way: fn(WildAnimal, float, float) -> bool = fn wl_no_way
# (a, x, z, r, seed): a spot about r from (x, z) this animal can walk to, into way_x / _z
goal: fn(WildAnimal, float, float, float, int) -> bool = fn wl_no_goal
way_x: fn() -> float = fn wl_asked_x
way_z: fn() -> float = fn wl_asked_z
}
function wl_zero0() -> float { return 0.0 }
function wl_zero1(p: int) -> float { return 0.0 }
@ -73,6 +80,8 @@ function wl_no_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool { return fa
function wl_no_lure(i: int, l: WildLure) -> bool { return false }
function wl_nothing(a: WildAnimal) -> void { }
function wl_range_nothing(r: WildRange, i: int) -> void { }
function wl_no_way(a: WildAnimal, x: float, z: float) -> bool { return false }
function wl_no_goal(a: WildAnimal, x: float, z: float, r: float, seed: int) -> bool { return false }
function wl_next_id(wildlife_st: mut WildlifeState) -> int {
wildlife_st.wl_ids += 1
return wildlife_st.wl_ids

View file

@ -1,5 +1,5 @@
# steer.ludic - turning toward a point at a rate, a step along the facing that turns away from water
# and a climb, and a target somewhere about home; a walker leaves a print every 2.8 m
# and a climb; a walker leaves a print every 2.8 m (the way there and a target about home: way.ludic)
function wl_wrap(a: float) -> float {
var r = a
while r > 3.1415927 { r = r - WL_TAU }
@ -58,13 +58,6 @@ function wl_push(wildlife_st: mut WildlifeState, a: WildAnimal, s: WildSpecies,
return hit
}
function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float) -> void {
let ang = wl_rnd(wildlife_st) * WL_TAU
let d = wl_rnd(wildlife_st) * radius
a.tx = a.home_x + Math.cos(ang) * d
a.tz = a.home_z + Math.sin(ang) * d
}
function wl_at_target(a: WildAnimal, r: float) -> bool {
let dx = a.tx - a.x
let dz = a.tz - a.z

View file

@ -0,0 +1,106 @@
# way_test.ludic - an animal walks by the world's way where the world has one: round a wall the
# straight line runs into (by the corners WildlifeWorld.way gives), and to a wander target the world
# says it can reach (goal); with no answer it walks straight, as before
import "ludic.wildlife"
import "ludic.base"
import "fake"
program WayTest {
numbers float
bind WildlifeWorld {
ground: fn fk_ground
water: fn fk_water
forest: fn fk_forest
sight: fn fk_sight
push: fn fk_push
pushed_x: fn fk_pushed_x
pushed_z: fn fk_pushed_z
players: fn fk_players
player_on: fn fk_player_on
player_x: fn fk_x
player_y: fn fk_y
player_z: fn fk_z
hidden: fn fk_is_hidden
noise: fn fk_noise_of
scent: fn fk_scent_of
stillness: fn fk_still_of
wind: fn fk_wind_of
wind_dir: fn fk_wind_to
winterness: fn fk_winter_of
about: fn fk_about_of
per_range: fn fk_one
lures: fn fk_count
lure: fn fk_lure
present: fn fk_present
born: fn fk_on_born
spot: fn fk_spot
way: fn wt_way
way_x: fn wt_x
way_z: fn wt_z
goal: fn wt_goal
}
state WayTestState {
wx: float = 0.0
wz: float = 0.0
asked: int = 0
}
# a wall across z = -40 for |x| < 30: the way goes round its east end, then on to the target
function wt_way(way_test_st: mut WayTestState, a: WildAnimal, x: float, z: float) -> bool {
way_test_st.asked += 1
way_test_st.wx = x
way_test_st.wz = z
if a.z > -38.0 and a.x < 31.0 {
way_test_st.wx = 32.0
way_test_st.wz = -40.0
}
return true
}
# a wander target a known step from home, whatever the seed
function wt_goal(way_test_st: mut WayTestState, a: WildAnimal, x: float, z: float, r: float, seed: int) -> bool {
way_test_st.wx = x + 7.0
way_test_st.wz = z + 3.0
return true
}
function wt_x(way_test_st: WayTestState) -> float { return way_test_st.wx }
function wt_z(way_test_st: WayTestState) -> float { return way_test_st.wz }
test "it walks round the wall by the corners it is given, and asks again as it goes" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) {
fk_setup(wildlife_st, wildlife_tests_fake_st)
let a = wildlife_make(wildlife_st, 0, 0.0, 0.0, false, 0.0, 0, 1.0, 1)
wildlife_tests_fake_st.fk_px[0] = 400.0
wildlife_tests_fake_st.fk_pz[0] = 0.0
wildlife_tests_fake_st.fk_hidden = true
a.state = WILD_WANDER
a.tx = 0.0
a.tz = -100.0
a.timer = 300.0
var east = -100.0
for k in 0 .. 3000 {
wildlife_tick(wildlife_st, 0.05, 0.0)
if a.z > -42.0 and a.z < -38.0 { east = Math.max(east, a.x) }
}
expect(east > 29.0)
expect(a.z < -60.0)
expect(way_test_st.asked > 10)
}
test "a wander target is where the world says it can walk" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) {
fk_setup(wildlife_st, wildlife_tests_fake_st)
let a = wildlife_make(wildlife_st, 0, 5.0, 5.0, false, 0.0, 0, 1.0, 1)
wildlife_tests_fake_st.fk_px[0] = 400.0
wildlife_tests_fake_st.fk_pz[0] = 0.0
wildlife_tests_fake_st.fk_hidden = true
a.hunger = 0.0
a.thirst = 0.0
a.state = WILD_IDLE
a.timer = -1.0
var k = 0
while a.state != WILD_WANDER and k < 2000 {
wildlife_tick(wildlife_st, 0.05, 0.0)
k += 1
}
expect_eq(a.state, WILD_WANDER)
expect_near(a.tx, a.home_x + 7.0, 0.001)
expect_near(a.tz, a.home_z + 3.0, 0.001)
}
}

View file

@ -10,7 +10,7 @@ function wl_seek(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo
a.timer = 2.0
return true
}
wl_face(a, a.spot_x, a.spot_z, 2.0, dt)
wl_head(a, a.spot_x, a.spot_z, 2.0, dt)
a.speed = wl_sp(wildlife_st, a.sp).walk
a.tx = a.spot_x
a.tz = a.spot_z
@ -44,6 +44,7 @@ function wl_drink(wildlife_st: WildlifeState, a: WildAnimal, dt: float, gh: floa
let water = a.thirst > a.hunger
if not WildlifeWorld.spot(a, water, wildlife_st.wl_spot_ask) { return false }
a.spot = wildlife_st.wl_spot_ask.uid
a.way_t = 0.0
a.spot_x = wildlife_st.wl_spot_ask.x
a.spot_z = wildlife_st.wl_spot_ask.z
a.spot_water = wildlife_st.wl_spot_ask.water

View file

@ -0,0 +1,36 @@
# way.ludic - where an animal walks: by the world's path to where it is going (WildlifeWorld.way, a
# navmesh by its habitat's taste in Maroon Lake), to a target about home it can reach (goal), and
# straight, as before, where the world has no answer
# face the way toward (tx, tz): the next corner of the world's path for it, asked again every
# WL_WAY_EVERY seconds or on reaching the corner; without a path, the straight line as before
const WL_WAY_EVERY: float = 0.5
function wl_head(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> void {
a.way_t = a.way_t - dt
let cx = a.wx - a.x
let cz = a.wz - a.z
if a.way_t <= 0.0 or cx * cx + cz * cz < 1.0 {
a.way_t = WL_WAY_EVERY
a.wx = tx
a.wz = tz
if WildlifeWorld.way(a, tx, tz) {
a.wx = WildlifeWorld.way_x()
a.wz = WildlifeWorld.way_z()
}
}
wl_face(a, a.wx, a.wz, rate, dt)
}
# somewhere about home: a spot the world says it can walk to when it knows, seeded by the same two
# draws the old target took, so the dice are the same whether it knows or not
function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float) -> void {
let ang = wl_rnd(wildlife_st) * WL_TAU
let d = wl_rnd(wildlife_st) * radius
a.tx = a.home_x + Math.cos(ang) * d
a.tz = a.home_z + Math.sin(ang) * d
a.way_t = 0.0
if WildlifeWorld.goal(a, a.home_x, a.home_z, radius, int(ang * 100000.0) + int(d * 1000.0)) {
a.tx = WildlifeWorld.way_x()
a.tz = WildlifeWorld.way_z()
}
}

View file

@ -478,10 +478,12 @@ namespace Value {
alias as_int(value) = value_as_int
alias as_float(value) = value_as_float
alias as_str(value) = value_as_str
alias free(value) = value_free
}
namespace Json {
alias encode(value) = json_encode
alias parse(text) = json_parse
alias free(value) = json_free
}
namespace Xml {
alias parse(text) = xml_parse

View file

@ -166,9 +166,23 @@ function jp_skip_ws(p: JP) -> void {
function json_parse(s: pointer) -> Val {
let p = new JP
p.s = s; p.i = 0; p.n = len(s)
return jp_value(p)
let v = jp_value(p)
free(p)
return v
}
# Let a parsed tree go once what it describes is built: every node and its lists. Its strings stay -
# a caller may keep a name it read out of the tree - and a tree put together by hand, which may share
# a node or hold a literal, is not for this (Ludic has no collector; plan 23 of maroon-lake).
function value_free(v: Val) -> void {
if v == null { return }
for i in 0 .. len(v.kids) { value_free(v.kids[i]) }
free(v.kids)
free(v.keys)
free(v)
}
function json_free(v: Val) -> void { value_free(v) }
function jp_value(p: JP) -> Val {
jp_skip_ws(p)
if p.i >= p.n { return value_null() }
@ -182,9 +196,29 @@ function jp_value(p: JP) -> Val {
return jp_number(p)
}
# read a quoted string (cursor on the opening quote) -> the unescaped contents.
# read a quoted string (cursor on the opening quote) -> the unescaped contents. One allocation for a
# string with no escape in it: built a character at a time, every step was a string never freed
function jp_string(p: JP) -> string {
p.i += 1 # skip opening quote
let a = p.i
while p.i < p.n {
let c = p.s[p.i]
if c == '"' {
let out = p.s[a..p.i]
p.i += 1
return out
}
if c == '\\' {
p.i = a
return jp_string_esc(p)
}
p.i += 1
}
return p.s[a..p.n]
}
# the same with escapes in it, the rare case, a character at a time
function jp_string_esc(p: JP) -> string {
var out = ""
while p.i < p.n {
let c = p.s[p.i]