Merge branch 'lang/foundations' into lang/ecs
This commit is contained in:
commit
b9d0cca281
32 changed files with 445 additions and 58 deletions
|
|
@ -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_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_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_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_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_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 |
|
| `nav_area_cost(kind, filter, area, cost)` | how dear a kind of ground is to cross by that filter |
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ export function nav_polygons(nav_st: NavState, kind: int) -> int {
|
||||||
return nvc_polys(h)
|
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
|
# a kind's mesh written to a file, and read back - through the asset pack when it is in one - into the
|
||||||
# is never given back), so a mesh is loaded once per map, never per frame
|
# 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 {
|
export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool {
|
||||||
let h = nv_mesh(nav_st, kind)
|
let h = nv_mesh(nav_st, kind)
|
||||||
if h == null { return false }
|
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)
|
return Fs.write_bytes(path, buf, n)
|
||||||
}
|
}
|
||||||
export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool {
|
export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool {
|
||||||
if not Fs.exists(path) { return false }
|
let f = file_open(path, "rb")
|
||||||
let buf = Fs.read_bytes(path)
|
if f == null { return false }
|
||||||
if buf == null or len(buf) == 0 { return false }
|
file_seek(f, 0, 2)
|
||||||
return nv_set(nav_st, kind, nvc_load(buf, len(buf)))
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
Binary file not shown.
|
|
@ -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_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_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_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"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include <DetourNavMeshQuery.h>
|
#include <DetourNavMeshQuery.h>
|
||||||
#include <DetourCommon.h>
|
#include <DetourCommon.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,3 +100,12 @@ NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
|
||||||
}
|
}
|
||||||
return nav_open(mesh);
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
# 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
|
# 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 {
|
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
|
nav_st.nv_count = 0
|
||||||
if h == null { return -1 }
|
if h == null { return -1 }
|
||||||
let n = nvc_path(h, filter, sx, sy, sz, ex, ey, ez, nav_st.nv_out, NAV_CORNERS)
|
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 > 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
|
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 {
|
export function nav_partial(nav_st: NavState, kind: int) -> bool {
|
||||||
let h = nv_mesh(nav_st, kind)
|
let h = nv_mesh(nav_st, kind)
|
||||||
return h != null and nvc_partial(h) == 1
|
return h != null and nvc_partial(h) == 1
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ export state NavState {
|
||||||
nv_near: []float = floats(3)
|
nv_near: []float = floats(3)
|
||||||
nv_cfg: []float = floats(7) # the configuration handed to the shim, filled in place per build
|
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_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 {
|
function nv_none() -> []pointer {
|
||||||
let m = new []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 {
|
export function nav_reset(nav_st: mut NavState) -> void {
|
||||||
for k in 0 .. NAV_KINDS { nv_set(nav_st, k, null) }
|
for k in 0 .. NAV_KINDS { nv_set(nav_st, k, null) }
|
||||||
nav_st.nv_count = 0
|
nav_st.nv_count = 0
|
||||||
|
nav_st.nv_asked = 0
|
||||||
|
nav_st.nv_found = 0
|
||||||
|
nav_st.nv_short = 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,18 @@ program NavTest {
|
||||||
expect_eq(nav_near_z(nav_st), z7)
|
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) {
|
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))
|
expect(meadow(nav_st, NAV_PERSON, 3, false))
|
||||||
nav_area_cost(nav_st, NAV_PERSON, 1, 2, 20.0)
|
nav_area_cost(nav_st, NAV_PERSON, 1, 2, 20.0)
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
taken: fn(string) -> bool # a name the place keeps
|
||||||
born / gone: fn(NpcPerson) -> void # draw it, stop drawing it
|
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 }
|
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
|
## API
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ export property NpcPerson {
|
||||||
via_on: bool = false # a step round whatever is in the way
|
via_on: bool = false # a step round whatever is in the way
|
||||||
vx: float = 0.0
|
vx: float = 0.0
|
||||||
vz: 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
|
detours: int = 0
|
||||||
fails: int = 0
|
fails: int = 0
|
||||||
stall: float = 0.0 # seconds walking without moving
|
stall: float = 0.0 # seconds walking without moving
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import "ground.ludic"
|
||||||
import "spots.ludic"
|
import "spots.ludic"
|
||||||
import "routine.ludic"
|
import "routine.ludic"
|
||||||
import "walk.ludic"
|
import "walk.ludic"
|
||||||
|
import "way.ludic"
|
||||||
import "act.ludic"
|
import "act.ludic"
|
||||||
import "names.ludic"
|
import "names.ludic"
|
||||||
import "census.ludic"
|
import "census.ludic"
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ export port NpcWorld {
|
||||||
taken: fn(string) -> bool = fn nw_free # a name the place already uses
|
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
|
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
|
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
|
# 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 }
|
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 }
|
||||||
|
|
|
||||||
82
packages/ludic.npc/tests/way_test.ludic
Normal file
82
packages/ludic.npc/tests/way_test.ludic
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# walk.ludic - walking to the target: turn toward it, step, slid round a trunk or a tent, never into
|
# walk.ludic - walking to the target by the world's way where it has one (way.ludic): turn toward the
|
||||||
# the water and never up a scramble - step round it instead - and give up on a target that gets no
|
# next corner, step, slid round a trunk or a tent, never into the water and never up a scramble - step
|
||||||
# closer for twenty seconds
|
# 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
|
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 {
|
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
|
gx = p.vx
|
||||||
gz = p.vz
|
gz = p.vz
|
||||||
}
|
}
|
||||||
let dx = gx - p.x
|
if npc_d(p.x, p.z, gx, gz) < 1.2 {
|
||||||
let dz = gz - p.z
|
|
||||||
if Math.sqrt(dx * dx + dz * dz) < 1.2 {
|
|
||||||
np_arrive(npc_st, p)
|
np_arrive(npc_st, p)
|
||||||
return
|
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)
|
let dg = npc_d(p.x, p.z, p.tx, p.tz)
|
||||||
if dg < p.best - 1.0 {
|
if dg < p.best - 1.0 {
|
||||||
p.best = dg
|
p.best = dg
|
||||||
|
|
@ -79,28 +84,3 @@ function np_arrive(npc_st: mut NpcState, p: NpcPerson) -> void {
|
||||||
p.stall = 0.0
|
p.stall = 0.0
|
||||||
if p.act != NPCA_WALK { np_fact(npc_st, NPC_F_ARRIVED, p, -1) }
|
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) }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
45
packages/ludic.npc/way.ludic
Normal file
45
packages/ludic.npc/way.ludic
Normal 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) }
|
||||||
|
}
|
||||||
|
|
@ -163,7 +163,11 @@ function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, no
|
||||||
render3d_st.gltf_dir = dir
|
render3d_st.gltf_dir = dir
|
||||||
let text = Fs.read_text(dir + "/" + file)
|
let text = Fs.read_text(dir + "/" + file)
|
||||||
if text == null { print(`gltf: cannot read {dir}/{file}`); return null }
|
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)
|
render3d_st.gltf_doc = Json.parse(text)
|
||||||
|
free(text)
|
||||||
let buffers = value_get(render3d_st.gltf_doc, "buffers")
|
let buffers = value_get(render3d_st.gltf_doc, "buffers")
|
||||||
let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri"))
|
let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri"))
|
||||||
render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb")
|
render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb")
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ export function save_parse(text: string, f: SaveFormat) -> SaveRead {
|
||||||
if text == null { return read_new(SAVE_NONE, null) }
|
if text == null { return read_new(SAVE_NONE, null) }
|
||||||
if not save_text_intact(text) { return read_new(SAVE_CORRUPT, null) }
|
if not save_text_intact(text) { return read_new(SAVE_CORRUPT, null) }
|
||||||
let v = Json.parse(text)
|
let v = Json.parse(text)
|
||||||
if value_kind(v) != 6 { return read_new(SAVE_CORRUPT, null) } # 6 is an object
|
if value_kind(v) != 6 or (f.is_one != null and not f.is_one(v)) { # 6 is an object
|
||||||
if f.is_one != null and not f.is_one(v) { return read_new(SAVE_CORRUPT, null) }
|
if v != null { Json.free(v) } # a refused tree is let go
|
||||||
|
return read_new(SAVE_CORRUPT, null)
|
||||||
|
}
|
||||||
return read_new(SAVE_OK, v)
|
return read_new(SAVE_OK, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,10 @@ function telemetry__read_obj(path: string) -> Val {
|
||||||
let s = Fs.read_text(path)
|
let s = Fs.read_text(path)
|
||||||
if s == null { return null }
|
if s == null { return null }
|
||||||
let v = Json.parse(s)
|
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
|
return v
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
next_id: fn() -> int # an animal's id across machines
|
||||||
born: fn(WildAnimal) -> void # draw it
|
born: fn(WildAnimal) -> void # draw it
|
||||||
placed: fn(WildRange, int) -> void # a range was set out: put its water and forage down
|
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
|
## API
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ export property WildAnimal {
|
||||||
home_z: float = 0.0
|
home_z: float = 0.0
|
||||||
tx: float = 0.0
|
tx: float = 0.0
|
||||||
tz: 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
|
thirst: float = 0.0 # 0..100: over 60 it goes looking for water
|
||||||
hunger: float = 0.0
|
hunger: float = 0.0
|
||||||
scat_t: float = 0.0 # game minutes until it might leave sign
|
scat_t: float = 0.0 # game minutes until it might leave sign
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export function wildlife_tick_ground(wildlife_st: mut WildlifeState, a: WildAnim
|
||||||
}
|
}
|
||||||
return
|
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
|
a.speed = s.walk
|
||||||
wl_step(wildlife_st, a, dt)
|
wl_step(wildlife_st, a, dt)
|
||||||
if wl_at_target(a, 1.5) or a.timer < 0.0 {
|
if wl_at_target(a, 1.5) or a.timer < 0.0 {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import "ranges.ludic"
|
||||||
import "migrate.ludic"
|
import "migrate.ludic"
|
||||||
import "senses.ludic"
|
import "senses.ludic"
|
||||||
import "steer.ludic"
|
import "steer.ludic"
|
||||||
|
import "way.ludic"
|
||||||
import "lures.ludic"
|
import "lures.ludic"
|
||||||
import "needs.ludic"
|
import "needs.ludic"
|
||||||
import "ground.ludic"
|
import "ground.ludic"
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,13 @@ export port WildlifeWorld {
|
||||||
next_id: fn() -> int = fn wl_next_id
|
next_id: fn() -> int = fn wl_next_id
|
||||||
born: fn(WildAnimal) -> void = fn wl_nothing # a new animal: draw it
|
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
|
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_zero0() -> float { return 0.0 }
|
||||||
function wl_zero1(p: int) -> 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_no_lure(i: int, l: WildLure) -> bool { return false }
|
||||||
function wl_nothing(a: WildAnimal) -> void { }
|
function wl_nothing(a: WildAnimal) -> void { }
|
||||||
function wl_range_nothing(r: WildRange, i: int) -> 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 {
|
function wl_next_id(wildlife_st: mut WildlifeState) -> int {
|
||||||
wildlife_st.wl_ids += 1
|
wildlife_st.wl_ids += 1
|
||||||
return wildlife_st.wl_ids
|
return wildlife_st.wl_ids
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# steer.ludic - turning toward a point at a rate, a step along the facing that turns away from water
|
# 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 {
|
function wl_wrap(a: float) -> float {
|
||||||
var r = a
|
var r = a
|
||||||
while r > 3.1415927 { r = r - WL_TAU }
|
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
|
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 {
|
function wl_at_target(a: WildAnimal, r: float) -> bool {
|
||||||
let dx = a.tx - a.x
|
let dx = a.tx - a.x
|
||||||
let dz = a.tz - a.z
|
let dz = a.tz - a.z
|
||||||
|
|
|
||||||
106
packages/ludic.wildlife/tests/way_test.ludic
Normal file
106
packages/ludic.wildlife/tests/way_test.ludic
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ function wl_seek(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo
|
||||||
a.timer = 2.0
|
a.timer = 2.0
|
||||||
return true
|
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.speed = wl_sp(wildlife_st, a.sp).walk
|
||||||
a.tx = a.spot_x
|
a.tx = a.spot_x
|
||||||
a.tz = a.spot_z
|
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
|
let water = a.thirst > a.hunger
|
||||||
if not WildlifeWorld.spot(a, water, wildlife_st.wl_spot_ask) { return false }
|
if not WildlifeWorld.spot(a, water, wildlife_st.wl_spot_ask) { return false }
|
||||||
a.spot = wildlife_st.wl_spot_ask.uid
|
a.spot = wildlife_st.wl_spot_ask.uid
|
||||||
|
a.way_t = 0.0
|
||||||
a.spot_x = wildlife_st.wl_spot_ask.x
|
a.spot_x = wildlife_st.wl_spot_ask.x
|
||||||
a.spot_z = wildlife_st.wl_spot_ask.z
|
a.spot_z = wildlife_st.wl_spot_ask.z
|
||||||
a.spot_water = wildlife_st.wl_spot_ask.water
|
a.spot_water = wildlife_st.wl_spot_ask.water
|
||||||
|
|
|
||||||
36
packages/ludic.wildlife/way.ludic
Normal file
36
packages/ludic.wildlife/way.ludic
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -478,10 +478,12 @@ namespace Value {
|
||||||
alias as_int(value) = value_as_int
|
alias as_int(value) = value_as_int
|
||||||
alias as_float(value) = value_as_float
|
alias as_float(value) = value_as_float
|
||||||
alias as_str(value) = value_as_str
|
alias as_str(value) = value_as_str
|
||||||
|
alias free(value) = value_free
|
||||||
}
|
}
|
||||||
namespace Json {
|
namespace Json {
|
||||||
alias encode(value) = json_encode
|
alias encode(value) = json_encode
|
||||||
alias parse(text) = json_parse
|
alias parse(text) = json_parse
|
||||||
|
alias free(value) = json_free
|
||||||
}
|
}
|
||||||
namespace Xml {
|
namespace Xml {
|
||||||
alias parse(text) = xml_parse
|
alias parse(text) = xml_parse
|
||||||
|
|
|
||||||
|
|
@ -166,9 +166,23 @@ function jp_skip_ws(p: JP) -> void {
|
||||||
function json_parse(s: pointer) -> Val {
|
function json_parse(s: pointer) -> Val {
|
||||||
let p = new JP
|
let p = new JP
|
||||||
p.s = s; p.i = 0; p.n = len(s)
|
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 {
|
function jp_value(p: JP) -> Val {
|
||||||
jp_skip_ws(p)
|
jp_skip_ws(p)
|
||||||
if p.i >= p.n { return value_null() }
|
if p.i >= p.n { return value_null() }
|
||||||
|
|
@ -182,9 +196,29 @@ function jp_value(p: JP) -> Val {
|
||||||
return jp_number(p)
|
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 {
|
function jp_string(p: JP) -> string {
|
||||||
p.i += 1 # skip opening quote
|
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 = ""
|
var out = ""
|
||||||
while p.i < p.n {
|
while p.i < p.n {
|
||||||
let c = p.s[p.i]
|
let c = p.s[p.i]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue