wip(0.S3): the packages migrated by ludic migrate state packages - every package test green

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 14:02:21 +03:00
parent 1e8b5b0523
commit 07505e7ef2
294 changed files with 14996 additions and 14675 deletions

View file

@ -1,6 +1,6 @@
# act.ludic - doing the act: turned toward what it is about - or toward whoever came over to talk,
# within the kind's notice - the game's own tick for the act, and a new plan when its time is up
export function npc_act(p: NpcPerson, dt: float) -> void {
export function npc_act(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
p.act_t = p.act_t - dt
var wx = p.fx
var wz = p.fz
@ -8,8 +8,8 @@ export function npc_act(p: NpcPerson, dt: float) -> void {
wx = p.x - Math.sin(p.stance) * 10.0
wz = p.z - Math.cos(p.stance) * 10.0
}
if not p.stalk and np_turns(p.act) and npc_near(p.x, p.z) < NpcKinds[p.kind].notice {
let i = npc_near_i()
if not p.stalk and np_turns(p.act) and npc_near(npc_st, p.x, p.z) < NpcKinds[p.kind].notice {
let i = npc_near_i(npc_st)
wx = NpcWorld.player_x(i)
wz = NpcWorld.player_z(i)
}
@ -21,7 +21,7 @@ export function npc_act(p: NpcPerson, dt: float) -> void {
let tick = NpcActs[p.act].tick
if tick(p, dt) { return }
}
if p.act_t < 0.0 { npc_plan(p) }
if p.act_t < 0.0 { npc_plan(npc_st, p) }
}
function np_turns(act: int) -> bool {
@ -30,7 +30,7 @@ function np_turns(act: int) -> bool {
}
# a player who came within notice since the last look is greeted - once, until they walk off
export function npc_notice(p: NpcPerson) -> void {
export function npc_notice(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson) -> void {
let reach = NpcKinds[p.kind].notice
for i in 0 .. NpcWorld.players() {
if i >= 30 { break }
@ -39,7 +39,7 @@ export function npc_notice(p: NpcPerson) -> void {
if NpcWorld.player_on(i) { d = npc_d(p.x, p.z, NpcWorld.player_x(i), NpcWorld.player_z(i)) }
if d < reach and (p.near & bit) == 0 {
p.near = p.near | bit
np_fact(NPC_F_GREETED, p, i)
np_fact(base_st, npc_st, NPC_F_GREETED, p, i)
}
if d > reach * 1.5 and (p.near & bit) != 0 { p.near = p.near - bit }
}

View file

@ -1,36 +1,36 @@
# census.ludic - making one (a slot reused before a new one, a look, a name, a start on its own
# ground out of the players' sight where that can be had), a post's fixed person, and leaving
export function npc_spawn(kind: int) -> NpcPerson {
np_ensure()
if np_n >= np_max or kind < 0 or kind >= NPCK_COUNT { return null }
export function npc_spawn(npc_st: mut NpcState, kind: int) -> NpcPerson {
np_ensure(npc_st)
if npc_st.np_n >= npc_st.np_max or kind < 0 or kind >= NPCK_COUNT { return null }
var p: NpcPerson = null
for i in 0 .. len(np_walkers) {
if not np_walkers[i].active {
p = np_walkers[i]
for i in 0 .. len(npc_st.np_walkers) {
if not npc_st.np_walkers[i].active {
p = npc_st.np_walkers[i]
break
}
}
if p == null {
if len(np_walkers) >= np_max { return null }
if len(npc_st.np_walkers) >= npc_st.np_max { return null }
p = new NpcPerson
p.slot = len(np_walkers)
push(np_walkers, p)
p.slot = len(npc_st.np_walkers)
push(npc_st.np_walkers, p)
}
np_fresh(p, kind, npc_between(1, 99999))
p.name = npc_pick_name(p.body)
np_fresh(p, kind, npc_between(npc_st, 1, 99999))
p.name = npc_pick_name(npc_st, p.body)
let k = NpcKinds[kind]
p.leg = npc_between(0, 3)
p.seed = npc_rnd() * 40.0
p.out_h = k.leave_lo + npc_rnd() * (k.leave_hi - k.leave_lo)
p.yaw = npc_rnd() * NP_TAU
p.leg = npc_between(npc_st, 0, 3)
p.seed = npc_rnd(npc_st) * 40.0
p.out_h = k.leave_lo + npc_rnd(npc_st) * (k.leave_hi - k.leave_lo)
p.yaw = npc_rnd(npc_st) * NP_TAU
if k.start != null {
let start = k.start
start(p)
} else { npc_start_near(p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) }
} else { npc_start_near(npc_st, p, NpcWorld.origin_x(), NpcWorld.origin_z(), 6.0, 30.0) }
p.active = true
np_n += 1
npc_st.np_n += 1
NpcWorld.born(p)
npc_plan(p)
npc_plan(npc_st, p)
return p
}
@ -62,20 +62,20 @@ function np_fresh(p: NpcPerson, kind: int, look: int) -> void {
# its own ground: a spot r0 .. r1 from (cx, cz), tried a few times for one no player is within 60 m
# of, else near the place's origin; that is home, where it stands and faces
export function npc_start_near(p: NpcPerson, cx: float, cz: float, r0: float, r1: float) -> void {
export function npc_start_near(npc_st: mut NpcState, p: NpcPerson, cx: float, cz: float, r0: float, r1: float) -> void {
var x = NpcWorld.origin_x()
var z = NpcWorld.origin_z()
var ok = false
for t in 0 .. 8 {
if not npc_spot_near(cx, cz, r0, r1) { continue }
x = np_px
z = np_pz
if not npc_spot_near(npc_st, cx, cz, r0, r1) { continue }
x = npc_st.np_px
z = npc_st.np_pz
ok = true
if npc_near(x, z) > 60.0 { break }
if npc_near(npc_st, x, z) > 60.0 { break }
}
if not ok and npc_spot_near(x, z, 10.0, 45.0) {
x = np_px
z = np_pz
if not ok and npc_spot_near(npc_st, x, z, 10.0, 45.0) {
x = npc_st.np_px
z = npc_st.np_pz
}
npc_set_at(p, x, z)
}
@ -93,10 +93,10 @@ export function npc_set_at(p: NpcPerson, x: float, z: float) -> void {
# somebody fixed at a post (a vendor behind the counter): never walks, never counted, never leaves,
# turns to a player within its kind's notice and back to `yaw` when they go
export function npc_place(kind: int, x: float, z: float, yaw: float, name: string, look: int) -> NpcPerson {
np_ensure()
export function npc_place(npc_st: mut NpcState, kind: int, x: float, z: float, yaw: float, name: string, look: int) -> NpcPerson {
np_ensure(npc_st)
let p = new NpcPerson
p.slot = len(np_posts)
p.slot = len(npc_st.np_posts)
np_fresh(p, kind, look)
p.fixed = true
p.name = name
@ -105,16 +105,16 @@ export function npc_place(kind: int, x: float, z: float, yaw: float, name: strin
p.yaw = yaw
npc_set_at(p, x, z)
p.active = true
push(np_posts, p)
push(npc_st.np_posts, p)
NpcWorld.born(p)
npc_do(p, NPCA_IDLE, 3600.0)
npc_do(npc_st, p, NPCA_IDLE, 3600.0)
return p
}
export function npc_retire(p: NpcPerson) -> void {
export function npc_retire(npc_st: mut NpcState, p: NpcPerson) -> void {
if not p.active { return }
p.active = false
if not p.fixed and np_n > 0 { np_n -= 1 }
np_left(p.name)
if not p.fixed and npc_st.np_n > 0 { npc_st.np_n -= 1 }
np_left(npc_st, p.name)
NpcWorld.gone(p)
}

View file

@ -1,15 +1,11 @@
# ground.ludic - where a person can stand and walk: dry, gentle, allowed; a straight walk with no
# water and no scramble on it; a spot in a ring, a spot toward something, and the nearest shore.
# A finder that succeeds leaves its answer in npc_px / npc_pz (and the water in npc_wx / npc_wz).
var np_px: float = 0.0
var np_pz: float = 0.0
var np_wx: float = 0.0
var np_wz: float = 0.0
export function npc_px() -> float { return np_px }
export function npc_pz() -> float { return np_pz }
export function npc_wx() -> float { return np_wx }
export function npc_wz() -> float { return np_wz }
export function npc_px(npc_st: NpcState) -> float { return npc_st.np_px }
export function npc_pz(npc_st: NpcState) -> float { return npc_st.np_pz }
export function npc_wx(npc_st: NpcState) -> float { return npc_st.np_wx }
export function npc_wz(npc_st: NpcState) -> float { return npc_st.np_wz }
export function npc_walkable(x: float, z: float) -> bool {
if not NpcWorld.allowed(x, z) { return false }
@ -34,41 +30,41 @@ export function npc_line_ok(ax: float, az: float, bx: float, bz: float) -> bool
return true
}
function np_found(x: float, z: float) -> bool {
np_px = x
np_pz = z
function np_found(npc_st: mut NpcState, x: float, z: float) -> bool {
npc_st.np_px = x
npc_st.np_pz = z
return true
}
# somewhere to stand d0 .. d1 from a point
export function npc_spot_near(x0: float, z0: float, d0: float, d1: float) -> bool {
export function npc_spot_near(npc_st: mut NpcState, x0: float, z0: float, d0: float, d1: float) -> bool {
for t in 0 .. 40 {
let ang = npc_rnd() * NP_TAU
let d = d0 + npc_rnd() * (d1 - d0)
let ang = npc_rnd(npc_st) * NP_TAU
let d = d0 + npc_rnd(npc_st) * (d1 - d0)
let x = x0 + Math.cos(ang) * d
let z = z0 + Math.sin(ang) * d
if npc_walkable(x, z) { return np_found(x, z) }
if npc_walkable(x, z) { return np_found(npc_st, x, z) }
}
return false
}
# a place to stand d from (ax, az), on the side (bx, bz) is on
export function npc_toward(ax: float, az: float, bx: float, bz: float, d: float) -> bool {
export function npc_toward(npc_st: mut NpcState, ax: float, az: float, bx: float, bz: float, d: float) -> bool {
let dd = Math.max(npc_d(ax, az, bx, bz), 0.5)
let x = ax + (bx - ax) / dd * d
let z = az + (bz - az) / dd * d
if npc_walkable(x, z) { return np_found(x, z) }
if npc_walkable(x, z) { return np_found(npc_st, x, z) }
for k in 0 .. 12 {
let ang = Math.atan2(bz - az, bx - ax) + float((k + 1) / 2 * (1 - 2 * (k % 2))) * 0.25
let x2 = ax + Math.cos(ang) * d
let z2 = az + Math.sin(ang) * d
if npc_walkable(x2, z2) { return np_found(x2, z2) }
if npc_walkable(x2, z2) { return np_found(npc_st, x2, z2) }
}
return npc_spot_near(x, z, 0.0, 14.0)
return npc_spot_near(npc_st, x, z, 0.0, 14.0)
}
# the nearest bit of shore within rmax: where to stand, and the water in front of it
export function npc_shore(x0: float, z0: float, rmax: float) -> bool {
export function npc_shore(npc_st: mut NpcState, x0: float, z0: float, rmax: float) -> bool {
var found = false
var bd = rmax
for k in 0 .. 16 {
@ -83,9 +79,9 @@ export function npc_shore(x0: float, z0: float, rmax: float) -> bool {
if NpcWorld.ground(x, z) < NpcWorld.water(x, z) {
if ld > 0.0 and d - ld < 12.0 {
bd = d
found = np_found(lx, lz)
np_wx = x
np_wz = z
found = np_found(npc_st, lx, lz)
npc_st.np_wx = x
npc_st.np_wz = z
}
break
}

View file

@ -1,44 +1,43 @@
# mirror.ludic - a guest draws the host's people: no routine and no census here, a slot filled or
# emptied as the host says, and each one eased toward where it was last heard to be
var np_clock: float = 0.0
# on: this machine's own walkers leave, and only the host's are drawn until it is turned off
export function npc_mirror(on: bool) -> void {
np_ensure()
if np_mirror == on { return }
np_mirror = on
for i in 0 .. len(np_walkers) { npc_retire(np_walkers[i]) }
if not on { npc_census_now() }
export function npc_mirror(npc_st: mut NpcState, on: bool) -> void {
np_ensure(npc_st)
if npc_st.np_mirror == on { return }
npc_st.np_mirror = on
for i in 0 .. len(npc_st.np_walkers) { npc_retire(npc_st, npc_st.np_walkers[i]) }
if not on { npc_census_now(npc_st) }
}
export function npc_mirroring() -> bool { return np_mirror }
export function npc_mirroring(npc_st: NpcState) -> bool { return npc_st.np_mirror }
# the host's slot: somebody new in it (born), or nobody (gone)
export function npc_put(slot: int, id: int, kind: int, look: int, seed: float, name: string, hx: float, hz: float) -> NpcPerson {
np_ensure()
if slot < 0 or slot >= np_max or kind < 0 or kind >= NPCK_COUNT { return null }
while len(np_walkers) <= slot {
export function npc_put(npc_st: mut NpcState, slot: int, id: int, kind: int, look: int, seed: float, name: string, hx: float, hz: float) -> NpcPerson {
np_ensure(npc_st)
if slot < 0 or slot >= npc_st.np_max or kind < 0 or kind >= NPCK_COUNT { return null }
while len(npc_st.np_walkers) <= slot {
let q = new NpcPerson
q.slot = len(np_walkers)
push(np_walkers, q)
q.slot = len(npc_st.np_walkers)
push(npc_st.np_walkers, q)
}
let p = np_walkers[slot]
let p = npc_st.np_walkers[slot]
if p.active and p.id == id { return p }
if p.active { npc_retire(p) }
if p.active { npc_retire(npc_st, p) }
np_fresh(p, kind, look)
p.id = id
p.seed = seed
p.name = name
npc_set_at(p, hx, hz)
p.active = true
np_n += 1
npc_st.np_n += 1
NpcWorld.born(p)
return p
}
# where the host says it is now, and what it is doing
export function npc_heard(slot: int, x: float, y: float, z: float, yaw: float, speed: float, act: int, stalk: bool) -> void {
let p = npc_at(slot)
export function npc_heard(npc_st: mut NpcState, slot: int, x: float, y: float, z: float, yaw: float, speed: float, act: int, stalk: bool) -> void {
let p = npc_at(npc_st, slot)
if p == null or not p.active { return }
if not p.heard {
p.x = x
@ -47,7 +46,7 @@ export function npc_heard(slot: int, x: float, y: float, z: float, yaw: float, s
p.yaw = yaw
}
p.heard = true
p.heard_t = np_clock
p.heard_t = npc_st.np_clock
p.nx = x
p.ny = y
p.nz = z
@ -58,15 +57,15 @@ export function npc_heard(slot: int, x: float, y: float, z: float, yaw: float, s
}
# heard from in the last six seconds: drawn; not, and it is not drawn until it is again
export function npc_heard_lately(p: NpcPerson) -> bool { return p.heard and np_clock - p.heard_t <= 6.0 }
export function npc_heard_lately(npc_st: NpcState, p: NpcPerson) -> bool { return p.heard and npc_st.np_clock - p.heard_t <= 6.0 }
export function npc_mirror_tick(dt: float) -> void {
np_ensure()
np_clock = np_clock + dt
export function npc_mirror_tick(npc_st: mut NpcState, dt: float) -> void {
np_ensure(npc_st)
npc_st.np_clock = npc_st.np_clock + dt
let k = Math.min(1.0, dt * 8.0)
for i in 0 .. len(np_walkers) {
let p = np_walkers[i]
if not p.active or not npc_heard_lately(p) { continue }
for i in 0 .. len(npc_st.np_walkers) {
let p = npc_st.np_walkers[i]
if not p.active or not npc_heard_lately(npc_st, p) { continue }
let ox = p.x
let oz = p.z
p.x = p.x + (p.nx - p.x) * k

View file

@ -1,44 +1,43 @@
# names.ludic - a name nobody else in the place is using: not another person, not one the place
# keeps for itself (NpcWorld.taken: a vendor), and not somebody who left a little while ago
var np_recent: []string = null # the last few names that left
export function npc_name_used(n: string) -> bool {
export function npc_name_used(npc_st: mut NpcState, n: string) -> bool {
if NpcWorld.taken(n) { return true }
let all = npc_walkers()
let all = npc_walkers(npc_st)
for i in 0 .. len(all) { if all[i].active and all[i].name == n { return true } }
let posts = npc_posts()
let posts = npc_posts(npc_st)
for i in 0 .. len(posts) { if posts[i].active and posts[i].name == n { return true } }
if np_recent != null { for i in 0 .. len(np_recent) { if np_recent[i] == n { return true } } }
if npc_st.np_recent != null { for i in 0 .. len(npc_st.np_recent) { if npc_st.np_recent[i] == n { return true } } }
return false
}
# a free name from the body's pool, from a place in it the dice choose ("" when the pool is empty)
export function npc_pick_name(body: int) -> string {
export function npc_pick_name(npc_st: mut NpcState, body: int) -> string {
let pool = new []string
for i in 0 .. NPCN_COUNT { if NpcNames[i].body == body { push(pool, NpcNames[i].name) } }
let n = len(pool)
if n == 0 { return "" }
let start = npc_between(0, n - 1)
let start = npc_between(npc_st, 0, n - 1)
for k in 0 .. n {
let name = pool[(start + k) % n]
if not npc_name_used(name) { return name }
if not npc_name_used(npc_st, name) { return name }
}
return pool[start]
}
function np_left(name: string) -> void {
if np_recent == null { np_recent = new []string }
push(np_recent, name)
if len(np_recent) <= 8 { return }
function np_left(npc_st: mut NpcState, name: string) -> void {
if npc_st.np_recent == null { npc_st.np_recent = new []string }
push(npc_st.np_recent, name)
if len(npc_st.np_recent) <= 8 { return }
let keep = new []string
for i in len(np_recent) - 8 .. len(np_recent) { push(keep, np_recent[i]) }
np_recent = keep
for i in len(npc_st.np_recent) - 8 .. len(npc_st.np_recent) { push(keep, npc_st.np_recent[i]) }
npc_st.np_recent = keep
}
# how many pairs share a name, the place's own names included
export function npc_name_clashes() -> int {
export function npc_name_clashes(npc_st: mut NpcState) -> int {
var dup = 0
let all = npc_walkers()
let all = npc_walkers(npc_st)
for i in 0 .. len(all) {
let p = all[i]
if not p.active { continue }

View file

@ -1,23 +1,34 @@
# person.ludic - the people: walkers in slots the census fills and empties, and the fixed ones at
# their posts; the package's own dice and its facts
var np_walkers: []NpcPerson = null
var np_posts: []NpcPerson = null
var np_max: int = 10 # walkers at most
var np_n: int = 0 # walkers about
var np_dice: Rng = null
var np_facts: Queue<NpcFact> = null
var np_next_h: float = 0.0 # the hour the census is next looked at
var np_hold: bool = false # held where they stand (a shot of their poses)
var np_mirror: bool = false # a guest drawing a host's: no routine runs here
export state NpcState {
np_px: float = 0.0
np_pz: float = 0.0
np_wx: float = 0.0
np_wz: float = 0.0
np_clock: float = 0.0
np_recent: []string = null # the last few names that left
np_walkers: []NpcPerson = null
np_posts: []NpcPerson = null
np_max: int = 10 # walkers at most
np_n: int = 0 # walkers about
np_dice: Rng = null
np_facts: Queue<NpcFact> = null
np_next_h: float = 0.0 # the hour the census is next looked at
np_hold: bool = false # held where they stand (a shot of their poses)
np_mirror: bool = false # a guest drawing a host's: no routine runs here
nw_ids: int = 0
np_near_i: int = -1
np_said: string = ""
}
const NP_PI: float = 3.1415927
const NP_TAU: float = 6.2831853
export function npc_facts() -> Queue<NpcFact> {
if np_facts == null { np_facts = queue_new("npc.facts") }
return np_facts
export function npc_facts(base_st: mut BaseState, npc_st: mut NpcState) -> Queue<NpcFact> {
if npc_st.np_facts == null { npc_st.np_facts = queue_new(base_st, "npc.facts") }
return npc_st.np_facts
}
function np_fact(what: int, p: NpcPerson, player: int) -> NpcFact {
function np_fact(base_st: mut BaseState, npc_st: mut NpcState, what: int, p: NpcPerson, player: int) -> NpcFact {
let f = new NpcFact
f.what = what
f.person = p.id
@ -25,68 +36,68 @@ function np_fact(what: int, p: NpcPerson, player: int) -> NpcFact {
f.fixed = p.fixed
f.player = player
f.act = p.act
q_push(npc_facts(), f)
q_push(base_st, npc_facts(base_st, npc_st), f)
return f
}
function np_ensure() -> void {
if np_walkers == null { np_walkers = new []NpcPerson }
if np_posts == null { np_posts = new []NpcPerson }
if np_dice == null { np_dice = rng_new(20260925) }
function np_ensure(npc_st: mut NpcState) -> void {
if npc_st.np_walkers == null { npc_st.np_walkers = new []NpcPerson }
if npc_st.np_posts == null { npc_st.np_posts = new []NpcPerson }
if npc_st.np_dice == null { npc_st.np_dice = rng_new(20260925) }
}
# how many walkers the place can hold at once
export function npc_config(max: int) -> void { np_max = max }
export function npc_config(npc_st: mut NpcState, max: int) -> void { npc_st.np_max = max }
export function npc_seed(n: int) -> void {
np_ensure()
rng_seed(np_dice, n)
export function npc_seed(npc_st: mut NpcState, n: int) -> void {
np_ensure(npc_st)
rng_seed(npc_st.np_dice, n)
}
# 0 .. 1, and whole seconds lo .. hi, off the package's dice
export function npc_rnd() -> float {
np_ensure()
return rng_float(np_dice)
export function npc_rnd(npc_st: mut NpcState) -> float {
np_ensure(npc_st)
return rng_float(npc_st.np_dice)
}
export function npc_between(lo: int, hi: int) -> int {
np_ensure()
return rng_between(np_dice, lo, hi)
export function npc_between(npc_st: mut NpcState, lo: int, hi: int) -> int {
np_ensure(npc_st)
return rng_between(npc_st.np_dice, lo, hi)
}
export function npc_secs(lo: int, hi: int) -> float { return float(npc_between(lo, np_hi(lo, hi))) }
export function npc_secs(npc_st: mut NpcState, lo: int, hi: int) -> float { return float(npc_between(npc_st, lo, np_hi(lo, hi))) }
export function npc_walkers() -> []NpcPerson {
np_ensure()
return np_walkers
export function npc_walkers(npc_st: mut NpcState) -> []NpcPerson {
np_ensure(npc_st)
return npc_st.np_walkers
}
export function npc_posts() -> []NpcPerson {
np_ensure()
return np_posts
export function npc_posts(npc_st: mut NpcState) -> []NpcPerson {
np_ensure(npc_st)
return npc_st.np_posts
}
export function npc_count() -> int { return np_n }
export function npc_count(npc_st: NpcState) -> int { return npc_st.np_n }
export function npc_max() -> int { return np_max }
export function npc_max(npc_st: NpcState) -> int { return npc_st.np_max }
# a walker's slot or a post, and anyone by id
export function npc_at(slot: int) -> NpcPerson {
np_ensure()
if slot < 0 or slot >= len(np_walkers) { return null }
return np_walkers[slot]
export function npc_at(npc_st: mut NpcState, slot: int) -> NpcPerson {
np_ensure(npc_st)
if slot < 0 or slot >= len(npc_st.np_walkers) { return null }
return npc_st.np_walkers[slot]
}
export function npc_post(i: int) -> NpcPerson {
np_ensure()
if i < 0 or i >= len(np_posts) { return null }
return np_posts[i]
export function npc_post(npc_st: mut NpcState, i: int) -> NpcPerson {
np_ensure(npc_st)
if i < 0 or i >= len(npc_st.np_posts) { return null }
return npc_st.np_posts[i]
}
export function npc_by_id(id: int) -> NpcPerson {
np_ensure()
for i in 0 .. len(np_walkers) { if np_walkers[i].active and np_walkers[i].id == id { return np_walkers[i] } }
for i in 0 .. len(np_posts) { if np_posts[i].active and np_posts[i].id == id { return np_posts[i] } }
export function npc_by_id(npc_st: mut NpcState, id: int) -> NpcPerson {
np_ensure(npc_st)
for i in 0 .. len(npc_st.np_walkers) { if npc_st.np_walkers[i].active and npc_st.np_walkers[i].id == id { return npc_st.np_walkers[i] } }
for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active and npc_st.np_posts[i].id == id { return npc_st.np_posts[i] } }
return null
}

View file

@ -45,27 +45,25 @@ function nw_free(n: string) -> bool { return false }
function nw_nothing(p: NpcPerson) -> void { }
function nw_silent(p: NpcPerson, line: int) -> string { return "" }
function nw_hello(p: NpcPerson) -> string { return "" }
var nw_ids: int = 0
function nw_next_id() -> int {
nw_ids += 1
return nw_ids
function nw_next_id(npc_st: mut NpcState) -> int {
npc_st.nw_ids += 1
return npc_st.nw_ids
}
# the nearest player on and their distance (npc_near_i: -1 when nobody is on)
var np_near_i: int = -1
export function npc_near(x: float, z: float) -> float {
export function npc_near(npc_st: mut NpcState, x: float, z: float) -> float {
var bd = 1000000.0
np_near_i = -1
npc_st.np_near_i = -1
for i in 0 .. NpcWorld.players() {
if not NpcWorld.player_on(i) { continue }
let d = npc_d(x, z, NpcWorld.player_x(i), NpcWorld.player_z(i))
if d < bd {
bd = d
np_near_i = i
npc_st.np_near_i = i
}
}
return bd
}
export function npc_near_i() -> int { return np_near_i }
export function npc_near_i(npc_st: NpcState) -> int { return npc_st.np_near_i }

View file

@ -1,7 +1,7 @@
# routine.ludic - going somewhere to do something, doing it here, and deciding what is next: the
# kind's own plan first, then a weighted draw among its steps for this hour, falling through to the
# next step whose spot can be found, and standing a while when none can
export function npc_go(p: NpcPerson, x: float, z: float, act: int, dur: float) -> void {
export function npc_go(npc_st: mut NpcState, p: NpcPerson, x: float, z: float, act: int, dur: float) -> void {
p.tx = x
p.tz = z
p.next_act = act
@ -13,16 +13,16 @@ export function npc_go(p: NpcPerson, x: float, z: float, act: int, dur: float) -
p.best = npc_d(p.x, p.z, x, z)
p.fx = x + (x - p.x)
p.fz = z + (z - p.z)
p.speed = 1.1 + npc_rnd() * 0.4
p.speed = 1.1 + npc_rnd(npc_st) * 0.4
}
# whatever it is, it is not done where the place says nobody stops: walk out of there first
export function npc_do(p: NpcPerson, act: int, dur: float) -> void {
export function npc_do(npc_st: mut NpcState, p: NpcPerson, act: int, dur: float) -> void {
if act != NPCA_WALK and not p.fixed and not NpcWorld.allowed(p.x, p.z) {
if npc_spot_near(p.x, p.z, 20.0, 60.0) {
if npc_spot_near(npc_st, p.x, p.z, 20.0, 60.0) {
let fx = p.fx
let fz = p.fz
npc_go(p, np_px, np_pz, act, dur)
npc_go(npc_st, p, npc_st.np_px, npc_st.np_pz, act, dur)
if act != NPCA_IDLE {
p.fx = fx
p.fz = fz
@ -58,10 +58,10 @@ function np_in_hours(s: NpcStep, h: float) -> bool {
}
# what next: the kind's plan, then its steps for this hour
export function npc_plan(p: NpcPerson) -> void {
export function npc_plan(npc_st: mut NpcState, p: NpcPerson) -> void {
p.stalk = false
if p.fixed {
npc_do(p, NPCA_IDLE, 3600.0)
npc_do(npc_st, p, NPCA_IDLE, 3600.0)
return
}
let k = NpcKinds[p.kind]
@ -69,19 +69,19 @@ export function npc_plan(p: NpcPerson) -> void {
let plan = k.plan
if plan(p) { return }
}
if k.steps != null and npc_steps(p, k.steps) { return }
if k.steps != null and npc_steps(npc_st, p, k.steps) { return }
npc_face_ahead(p)
npc_do(p, NPCA_IDLE, 10.0)
npc_do(npc_st, p, NPCA_IDLE, 10.0)
}
# a weighted draw among the steps whose hours hold now; a step whose spot cannot be found
# gives way to the next one in the table, round to the start
export function npc_steps(p: NpcPerson, steps: []NpcStep) -> bool {
export function npc_steps(npc_st: mut NpcState, p: NpcPerson, steps: []NpcStep) -> bool {
let h = NpcWorld.hour()
var total = 0
for i in 0 .. len(steps) { if np_in_hours(steps[i], h) { total += np_pos(steps[i].weight) } }
if total <= 0 { return false }
var r = npc_between(0, total - 1)
var r = npc_between(npc_st, 0, total - 1)
var first = -1
for i in 0 .. len(steps) {
if not np_in_hours(steps[i], h) { continue }

View file

@ -10,71 +10,71 @@ def NpcSpots shore_here { find: fn np_spot_shore_here }
def NpcSpots wander { find: fn np_spot_wander }
# the step's act for the step's time, at (x, z)
export function npc_step_go(p: NpcPerson, s: NpcStep, x: float, z: float) -> void {
npc_go(p, x, z, s.act, npc_secs(s.lo, s.hi))
export function npc_step_go(npc_st: mut NpcState, p: NpcPerson, s: NpcStep, x: float, z: float) -> void {
npc_go(npc_st, p, x, z, s.act, npc_secs(npc_st, s.lo, s.hi))
}
function np_spot_here(p: NpcPerson, s: NpcStep) -> bool {
function np_spot_here(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
npc_face_ahead(p)
npc_do(p, s.act, npc_secs(s.lo, s.hi))
npc_do(npc_st, p, s.act, npc_secs(npc_st, s.lo, s.hi))
return true
}
function np_spot_home(p: NpcPerson, s: NpcStep) -> bool {
function np_spot_home(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
var fx = p.hx - Math.sin(p.yaw) * 5.0
var fz = p.hz - Math.cos(p.yaw) * 5.0
if p.home_face {
fx = p.hfx
fz = p.hfz
}
if npc_d(p.x, p.z, p.hx, p.hz) < 2.0 { npc_do(p, s.act, npc_secs(s.lo, s.hi)) } else { npc_step_go(p, s, p.hx, p.hz) }
if npc_d(p.x, p.z, p.hx, p.hz) < 2.0 { npc_do(npc_st, p, s.act, npc_secs(npc_st, s.lo, s.hi)) } else { npc_step_go(npc_st, p, s, p.hx, p.hz) }
npc_face(p, fx, fz)
return true
}
function np_spot_near_home(p: NpcPerson, s: NpcStep) -> bool {
if not npc_spot_near(p.hx, p.hz, s.near, s.far) { return false }
npc_step_go(p, s, np_px, np_pz)
function np_spot_near_home(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
if not npc_spot_near(npc_st, p.hx, p.hz, s.near, s.far) { return false }
npc_step_go(npc_st, p, s, npc_st.np_px, npc_st.np_pz)
return true
}
function np_spot_near_here(p: NpcPerson, s: NpcStep) -> bool {
if not npc_spot_near(p.x, p.z, s.near, s.far) { return false }
npc_step_go(p, s, np_px, np_pz)
function np_spot_near_here(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
if not npc_spot_near(npc_st, p.x, p.z, s.near, s.far) { return false }
npc_step_go(npc_st, p, s, npc_st.np_px, npc_st.np_pz)
return true
}
# at the water's edge, facing out over it
function np_spot_shore(p: NpcPerson, s: NpcStep, x: float, z: float) -> bool {
if not npc_shore(x, z, s.far) { return false }
let wx = np_wx
let wz = np_wz
npc_step_go(p, s, np_px, np_pz)
function np_spot_shore(npc_st: mut NpcState, p: NpcPerson, s: NpcStep, x: float, z: float) -> bool {
if not npc_shore(npc_st, x, z, s.far) { return false }
let wx = npc_st.np_wx
let wz = npc_st.np_wz
npc_step_go(npc_st, p, s, npc_st.np_px, npc_st.np_pz)
npc_face(p, wx, wz)
return true
}
function np_spot_shore_home(p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(p, s, p.hx, p.hz) }
function np_spot_shore_home(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(npc_st, p, s, p.hx, p.hz) }
function np_spot_shore_here(p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(p, s, p.x, p.z) }
function np_spot_shore_here(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool { return np_spot_shore(npc_st, p, s, p.x, p.z) }
function np_spot_wander(p: NpcPerson, s: NpcStep) -> bool {
npc_wander(p)
function np_spot_wander(npc_st: mut NpcState, p: NpcPerson, s: NpcStep) -> bool {
npc_wander(npc_st, p)
return true
}
# a few steps somewhere that can actually be walked to, after being stuck
export function npc_wander(p: NpcPerson) -> void {
export function npc_wander(npc_st: mut NpcState, p: NpcPerson) -> void {
for t in 0 .. 16 {
let ang = npc_rnd() * NP_TAU
let d = 8.0 + npc_rnd() * 18.0
let ang = npc_rnd(npc_st) * NP_TAU
let d = 8.0 + npc_rnd(npc_st) * 18.0
let x = p.x + Math.cos(ang) * d
let z = p.z + Math.sin(ang) * d
if npc_walkable(x, z) and npc_line_ok(p.x, p.z, x, z) {
npc_go(p, x, z, NPCA_IDLE, npc_secs(5, 12))
npc_go(npc_st, p, x, z, NPCA_IDLE, npc_secs(npc_st, 5, 12))
return
}
}
npc_face_ahead(p)
npc_do(p, NPCA_IDLE, 8.0)
npc_do(npc_st, p, NPCA_IDLE, 8.0)
}

View file

@ -20,7 +20,6 @@ export property NpcLine {
export open registry NpcLines of NpcLine as NPCL
var np_said: string = ""
function np_line_fits(p: NpcPerson, l: NpcLine) -> bool {
return (l.kind < 0 or l.kind == p.kind) and (l.act < 0 or l.act == p.act)
@ -34,8 +33,8 @@ export function npc_words(p: NpcPerson, line: int) -> string {
}
# the line it would say now (-1: none), its words kept for npc_said()
export function npc_line_for(p: NpcPerson) -> int {
np_said = ""
export function npc_line_for(npc_st: mut NpcState, p: NpcPerson) -> int {
npc_st.np_said = ""
var top = -1000000
for i in 0 .. NPCL_COUNT { if NpcLines[i].prio > top { top = NpcLines[i].prio } }
var prio = top
@ -45,10 +44,10 @@ export function npc_line_for(p: NpcPerson) -> int {
let l = NpcLines[i]
if l.prio < prio and l.prio > lower { lower = l.prio }
if l.prio != prio or not np_line_fits(p, l) { continue }
if l.chance < 100 and npc_between(0, 99) >= l.chance { continue }
if l.chance < 100 and npc_between(npc_st, 0, 99) >= l.chance { continue }
let w = npc_words(p, i)
if len(w) > 0 {
np_said = w
npc_st.np_said = w
return i
}
}
@ -57,13 +56,13 @@ export function npc_line_for(p: NpcPerson) -> int {
return -1
}
export function npc_said() -> string { return np_said }
export function npc_said(npc_st: NpcState) -> string { return npc_st.np_said }
# a player talks to them: the line (-1: nothing to say), its words in npc_said(), a Talked fact,
# and `said` marked for the day
export function npc_talk(p: NpcPerson, player: int) -> int {
let line = npc_line_for(p)
let f = np_fact(NPC_F_TALKED, p, player)
export function npc_talk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson, player: int) -> int {
let line = npc_line_for(npc_st, p)
let f = np_fact(base_st, npc_st, NPC_F_TALKED, p, player)
f.line = line
p.said = NpcWorld.day()
return line
@ -71,16 +70,16 @@ export function npc_talk(p: NpcPerson, player: int) -> int {
# the answer to choice c of a line: the line it leads to (-1: the talk is over), its words in
# npc_said(), and a Talked fact carrying the choice's code
export function npc_choose(p: NpcPerson, line: int, c: int, player: int) -> int {
np_said = ""
export function npc_choose(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson, line: int, c: int, player: int) -> int {
npc_st.np_said = ""
if line < 0 or line >= NPCL_COUNT { return -1 }
let cs = NpcLines[line].choices
if cs == null or c < 0 or c >= len(cs) { return -1 }
let next = npc_lines_find(cs[c].line)
let f = np_fact(NPC_F_TALKED, p, player)
let f = np_fact(base_st, npc_st, NPC_F_TALKED, p, player)
f.line = next
f.choice = cs[c].code
if next >= 0 { np_said = npc_words(p, next) }
if next >= 0 { npc_st.np_said = npc_words(p, next) }
return next
}

View file

@ -1,30 +1,32 @@
# fake - a place for the people: dry flat ground east of x = -50 and a lake west of it, a camp at
# (40, 0) nobody may stop in, two players, and what the port was asked to draw
numbers float
var fk_hour: float = 12.0
var fk_day: int = 1
var fk_want: int = 0
var fk_extra: int = -1
var fk_px: []float = null
var fk_pz: []float = null
var fk_on: []bool = null
var fk_born: int = 0
var fk_gone: int = 0
export state NpcTestsFakeState {
fk_hour: float = 12.0
fk_day: int = 1
fk_want: int = 0
fk_extra: int = -1
fk_px: []float = null
fk_pz: []float = null
fk_on: []bool = null
fk_born: int = 0
fk_gone: int = 0
}
function fk_setup() -> void {
fk_hour = 12.0
fk_day = 1
fk_want = 0
fk_extra = -1
fk_px = [500.0, 900.0]
fk_pz = [500.0, 900.0]
fk_on = [true, false]
fk_born = 0
fk_gone = 0
npc_clear()
npc_reset()
npc_seed(7)
npc_config(10)
function fk_setup(base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) -> void {
npc_tests_fake_st.fk_hour = 12.0
npc_tests_fake_st.fk_day = 1
npc_tests_fake_st.fk_want = 0
npc_tests_fake_st.fk_extra = -1
npc_tests_fake_st.fk_px = [500.0, 900.0]
npc_tests_fake_st.fk_pz = [500.0, 900.0]
npc_tests_fake_st.fk_on = [true, false]
npc_tests_fake_st.fk_born = 0
npc_tests_fake_st.fk_gone = 0
npc_clear(npc_st)
npc_reset(base_st, npc_st)
npc_seed(npc_st, 7)
npc_config(npc_st, 10)
}
function fk_ground(x: float, z: float) -> float { return 0.0 }
@ -33,28 +35,28 @@ function fk_water(x: float, z: float) -> float {
return -100.0
}
function fk_allowed(x: float, z: float) -> bool { return npc_d(x, z, 40.0, 0.0) > 15.0 }
function fk_hour_now() -> float { return fk_hour }
function fk_day_now() -> int { return fk_day }
function fk_hour_now(npc_tests_fake_st: NpcTestsFakeState) -> float { return npc_tests_fake_st.fk_hour }
function fk_day_now(npc_tests_fake_st: NpcTestsFakeState) -> int { return npc_tests_fake_st.fk_day }
function fk_players() -> int { return 2 }
function fk_player_on(i: int) -> bool { return fk_on[i] }
function fk_x(i: int) -> float { return fk_px[i] }
function fk_z(i: int) -> float { return fk_pz[i] }
function fk_wanted() -> int { return fk_want }
function fk_extra_kind() -> int { return fk_extra }
function fk_player_on(npc_tests_fake_st: NpcTestsFakeState, i: int) -> bool { return npc_tests_fake_st.fk_on[i] }
function fk_x(npc_tests_fake_st: NpcTestsFakeState, i: int) -> float { return npc_tests_fake_st.fk_px[i] }
function fk_z(npc_tests_fake_st: NpcTestsFakeState, i: int) -> float { return npc_tests_fake_st.fk_pz[i] }
function fk_wanted(npc_tests_fake_st: NpcTestsFakeState) -> int { return npc_tests_fake_st.fk_want }
function fk_extra_kind(npc_tests_fake_st: NpcTestsFakeState) -> int { return npc_tests_fake_st.fk_extra }
function fk_taken(n: string) -> bool { return n == "Ada" }
function fk_on_born(p: NpcPerson) -> void { fk_born += 1 }
function fk_on_gone(p: NpcPerson) -> void { fk_gone += 1 }
function fk_on_born(npc_tests_fake_st: mut NpcTestsFakeState, p: NpcPerson) -> void { npc_tests_fake_st.fk_born += 1 }
function fk_on_gone(npc_tests_fake_st: mut NpcTestsFakeState, p: NpcPerson) -> void { npc_tests_fake_st.fk_gone += 1 }
function fk_player_at(x: float, z: float) -> void {
fk_px[0] = x
fk_pz[0] = z
function fk_player_at(npc_tests_fake_st: mut NpcTestsFakeState, x: float, z: float) -> void {
npc_tests_fake_st.fk_px[0] = x
npc_tests_fake_st.fk_pz[0] = z
}
# seconds of the place, in twentieths
function fk_run(secs: float) -> void { for k in 0 .. int(secs * 20.0) { npc_tick(0.05) } }
function fk_run(base_st: mut BaseState, npc_st: mut NpcState, secs: float) -> void { for k in 0 .. int(secs * 20.0) { npc_tick(base_st, npc_st, 0.05) } }
function fk_count(what: int) -> int {
let fs = q_drain(npc_facts())
function fk_count(base_st: mut BaseState, npc_st: mut NpcState, what: int) -> int {
let fs = q_drain(base_st, npc_facts(base_st, npc_st))
var n = 0
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
return n

View file

@ -30,18 +30,20 @@ program NpcTest {
def NpcNames from "names.lres"
def NpcLines from "lines.lres"
var news_on: bool = false
function tk_say(p: NpcPerson, line: int) -> string {
if news_on { return `{p.name} saw a bear.` }
state NpctestState {
news_on: bool = false
}
function tk_say(npctest_st: NpctestState, p: NpcPerson, line: int) -> string {
if npctest_st.news_on { return `{p.name} saw a bear.` }
return ""
}
function tk_greeting(p: NpcPerson) -> string { return "What'll it be?" }
function tk_start(p: NpcPerson) -> void { npc_start_near(p, 0.0, 0.0, 0.0, 10.0) }
function tk_start(npc_st: mut NpcState, p: NpcPerson) -> void { npc_start_near(npc_st, p, 0.0, 0.0, 0.0, 10.0) }
function one(kind: int) -> NpcPerson {
fk_setup()
fk_want = 1
let p = npc_spawn(kind)
function one(base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState, kind: int) -> NpcPerson {
fk_setup(base_st, npc_st, npc_tests_fake_st)
npc_tests_fake_st.fk_want = 1
let p = npc_spawn(npc_st, kind)
npc_set_at(p, 0.0, 0.0)
return p
}
@ -56,175 +58,175 @@ program NpcTest {
expect_eq(NPCN_COUNT, 14)
}
test "the census fills to what the place wants, by weight, and after dark only who stays out" {
fk_setup()
fk_want = 4
npc_tick(0.05)
expect_eq(npc_count(), 4)
expect_eq(fk_born, 4)
expect_eq(npc_name_clashes(), 0)
for i in 0 .. len(npc_walkers()) { expect(npc_walkers()[i].kind != NPCK_VENDOR) }
fk_setup()
fk_want = 3
fk_hour = 23.0
npc_tick(0.05)
expect_eq(npc_count(), 3)
for i in 0 .. len(npc_walkers()) { expect_eq(npc_walkers()[i].kind, NPCK_CAMPER) }
test "the census fills to what the place wants, by weight, and after dark only who stays out" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
fk_setup(base_st, npc_st, npc_tests_fake_st)
npc_tests_fake_st.fk_want = 4
npc_tick(base_st, npc_st, 0.05)
expect_eq(npc_count(npc_st), 4)
expect_eq(npc_tests_fake_st.fk_born, 4)
expect_eq(npc_name_clashes(npc_st), 0)
for i in 0 .. len(npc_walkers(npc_st)) { expect(npc_walkers(npc_st)[i].kind != NPCK_VENDOR) }
fk_setup(base_st, npc_st, npc_tests_fake_st)
npc_tests_fake_st.fk_want = 3
npc_tests_fake_st.fk_hour = 23.0
npc_tick(base_st, npc_st, 0.05)
expect_eq(npc_count(npc_st), 3)
for i in 0 .. len(npc_walkers(npc_st)) { expect_eq(npc_walkers(npc_st)[i].kind, NPCK_CAMPER) }
}
test "no name twice, and never one the place keeps" {
fk_setup()
fk_want = 6
npc_tick(0.05)
expect_eq(npc_count(), 6)
expect_eq(npc_name_clashes(), 0)
for i in 0 .. 6 { expect(npc_walkers()[i].name != "Ada") }
test "no name twice, and never one the place keeps" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
fk_setup(base_st, npc_st, npc_tests_fake_st)
npc_tests_fake_st.fk_want = 6
npc_tick(base_st, npc_st, 0.05)
expect_eq(npc_count(npc_st), 6)
expect_eq(npc_name_clashes(npc_st), 0)
for i in 0 .. 6 { expect(npc_walkers(npc_st)[i].name != "Ada") }
}
test "it walks to where it is going and begins there, facing what it came for" {
let p = one(NPCK_HIKER)
npc_go(p, 20.0, 0.0, NPCA_SIT, 30.0)
test "it walks to where it is going and begins there, facing what it came for" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
npc_go(npc_st, p, 20.0, 0.0, NPCA_SIT, 30.0)
npc_face(p, 30.0, 0.0)
fk_run(30.0)
fk_run(base_st, npc_st, 30.0)
expect_eq(p.act, NPCA_SIT)
expect(npc_d(p.x, p.z, 20.0, 0.0) < 1.3)
expect(p.phase > 20.0)
expect_eq(fk_count(NPC_F_ARRIVED), 1)
expect_eq(fk_count(base_st, npc_st, NPC_F_ARRIVED), 1)
expect_near(p.fx, 30.0, 0.01)
}
test "nobody walks into the lake" {
let p = one(NPCK_HIKER)
npc_go(p, -80.0, 0.0, NPCA_SIT, 30.0)
test "nobody walks into the lake" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
npc_go(npc_st, p, -80.0, 0.0, NPCA_SIT, 30.0)
var west = 0.0
for k in 0 .. 1200 {
npc_tick(0.05)
npc_tick(base_st, npc_st, 0.05)
west = Math.min(west, p.x)
}
expect(west > -50.5)
}
test "a step's spot is where its hours say: the shore by day, home by night" {
let p = one(NPCK_CAMPER)
test "a step's spot is where its hours say: the shore by day, home by night" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_CAMPER)
npc_set_at(p, -20.0, 0.0)
fk_hour = 12.0
npc_plan(p)
npc_tests_fake_st.fk_hour = 12.0
npc_plan(npc_st, p)
expect_eq(p.act, NPCA_WALK)
expect_eq(p.next_act, NPCA_FISH)
expect(p.fx < -50.0)
fk_hour = 23.0
npc_plan(p)
npc_tests_fake_st.fk_hour = 23.0
npc_plan(npc_st, p)
expect_eq(p.act, NPCA_SIT)
}
test "an act is not begun where the place says nobody stops" {
let p = one(NPCK_HIKER)
test "an act is not begun where the place says nobody stops" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
npc_set_at(p, 40.0, 0.0)
npc_do(p, NPCA_SIT, 30.0)
npc_do(npc_st, p, NPCA_SIT, 30.0)
expect_eq(p.act, NPCA_WALK)
expect(fk_allowed(p.tx, p.tz))
}
test "it turns to a player who comes near, greets them once, and again after they have gone" {
let p = one(NPCK_HIKER)
npc_do(p, NPCA_IDLE, 100.0)
test "it turns to a player who comes near, greets them once, and again after they have gone" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
npc_do(npc_st, p, NPCA_IDLE, 100.0)
p.yaw = 0.0
fk_player_at(0.0, 0.0 - 5.0)
fk_run(2.0)
fk_player_at(npc_tests_fake_st, 0.0, 0.0 - 5.0)
fk_run(base_st, npc_st, 2.0)
expect(Math.abs(p.yaw) < 0.05)
fk_player_at(5.0, 0.0)
fk_run(3.0)
fk_player_at(npc_tests_fake_st, 5.0, 0.0)
fk_run(base_st, npc_st, 3.0)
expect(Math.abs(npc_wrap(p.yaw + 1.5707963)) < 0.05)
fk_run(1.0)
expect_eq(fk_count(NPC_F_GREETED), 1)
fk_player_at(40.0, 0.0)
fk_run(0.5)
fk_player_at(5.0, 0.0)
fk_run(0.5)
expect_eq(fk_count(NPC_F_GREETED), 1)
fk_run(base_st, npc_st, 1.0)
expect_eq(fk_count(base_st, npc_st, NPC_F_GREETED), 1)
fk_player_at(npc_tests_fake_st, 40.0, 0.0)
fk_run(base_st, npc_st, 0.5)
fk_player_at(npc_tests_fake_st, 5.0, 0.0)
fk_run(base_st, npc_st, 0.5)
expect_eq(fk_count(base_st, npc_st, NPC_F_GREETED), 1)
}
test "a fisher keeps its eyes on the water, whoever is near" {
let p = one(NPCK_HIKER)
npc_do(p, NPCA_FISH, 100.0)
test "a fisher keeps its eyes on the water, whoever is near" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
npc_do(npc_st, p, NPCA_FISH, 100.0)
p.yaw = 0.0
npc_face(p, 0.0, -10.0)
fk_player_at(5.0, 0.0)
fk_run(2.0)
fk_player_at(npc_tests_fake_st, 5.0, 0.0)
fk_run(base_st, npc_st, 2.0)
expect(Math.abs(p.yaw) < 0.05)
}
test "the day ends out of sight, and not in it" {
let p = one(NPCK_HIKER)
npc_do(p, NPCA_IDLE, 1000.0)
fk_hour = 22.5
fk_player_at(20.0, 0.0)
fk_run(0.5)
test "the day ends out of sight, and not in it" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_HIKER)
npc_do(npc_st, p, NPCA_IDLE, 1000.0)
npc_tests_fake_st.fk_hour = 22.5
fk_player_at(npc_tests_fake_st, 20.0, 0.0)
fk_run(base_st, npc_st, 0.5)
expect(p.active)
fk_player_at(500.0, 0.0)
fk_run(0.5)
fk_player_at(npc_tests_fake_st, 500.0, 0.0)
fk_run(base_st, npc_st, 0.5)
expect(not p.active)
expect_eq(fk_gone, 1)
let c = one(NPCK_CAMPER)
npc_do(c, NPCA_IDLE, 1000.0)
fk_hour = 22.5
fk_run(0.5)
expect_eq(npc_tests_fake_st.fk_gone, 1)
let c = one(base_st, npc_st, npc_tests_fake_st, NPCK_CAMPER)
npc_do(npc_st, c, NPCA_IDLE, 1000.0)
npc_tests_fake_st.fk_hour = 22.5
fk_run(base_st, npc_st, 0.5)
expect(c.active)
}
test "lines: the highest that has something to say, the story's words, and choices" {
let p = one(NPCK_CAMPER)
npc_do(p, NPCA_FISH, 100.0)
expect_eq(npc_talk(p, 0), NPCL_HUSH)
expect(npc_said() == "Shh.")
test "lines: the highest that has something to say, the story's words, and choices" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState, npctest_st: mut NpctestState) {
let p = one(base_st, npc_st, npc_tests_fake_st, NPCK_CAMPER)
npc_do(npc_st, p, NPCA_FISH, 100.0)
expect_eq(npc_talk(base_st, npc_st, p, 0), NPCL_HUSH)
expect(npc_said(npc_st) == "Shh.")
expect(npc_talked_today(p))
npc_do(p, NPCA_IDLE, 100.0)
expect_eq(npc_line_for(p), NPCL_HELLO)
news_on = true
expect_eq(npc_line_for(p), NPCL_NEWS)
expect(npc_said() == `{p.name} saw a bear.`)
news_on = false
expect_eq(npc_choose(p, NPCL_HELLO, 0, 0), NPCL_FISH_NO)
expect(npc_said() == "Not today.")
expect_eq(npc_choose(p, NPCL_HELLO, 1, 0), -1)
let fs = q_drain(npc_facts())
npc_do(npc_st, p, NPCA_IDLE, 100.0)
expect_eq(npc_line_for(npc_st, p), NPCL_HELLO)
npctest_st.news_on = true
expect_eq(npc_line_for(npc_st, p), NPCL_NEWS)
expect(npc_said(npc_st) == `{p.name} saw a bear.`)
npctest_st.news_on = false
expect_eq(npc_choose(base_st, npc_st, p, NPCL_HELLO, 0, 0), NPCL_FISH_NO)
expect(npc_said(npc_st) == "Not today.")
expect_eq(npc_choose(base_st, npc_st, p, NPCL_HELLO, 1, 0), -1)
let fs = q_drain(base_st, npc_facts(base_st, npc_st))
expect_eq(len(fs), 3)
expect_eq(fs[1].choice, 7)
expect(npc_greeting(p) == "What'll it be?")
}
test "a post stands where it was put, is not counted, and turns back when nobody is near" {
fk_setup()
let v = npc_place(NPCK_VENDOR, 0.0, 0.0, 0.0, "Mateo", 5)
test "a post stands where it was put, is not counted, and turns back when nobody is near" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
fk_setup(base_st, npc_st, npc_tests_fake_st)
let v = npc_place(npc_st, NPCK_VENDOR, 0.0, 0.0, 0.0, "Mateo", 5)
expect(v.fixed)
expect_eq(npc_count(), 0)
fk_player_at(8.0, 0.0)
fk_run(3.0)
expect_eq(npc_count(npc_st), 0)
fk_player_at(npc_tests_fake_st, 8.0, 0.0)
fk_run(base_st, npc_st, 3.0)
expect(Math.abs(npc_wrap(v.yaw + 1.5707963)) < 0.05)
fk_player_at(300.0, 0.0)
fk_run(3.0)
fk_player_at(npc_tests_fake_st, 300.0, 0.0)
fk_run(base_st, npc_st, 3.0)
expect(Math.abs(v.yaw) < 0.05)
expect(npc_d(v.x, v.z, 0.0, 0.0) < 0.001)
expect(npc_by_id(v.id).name == "Mateo")
expect(npc_by_id(npc_st, v.id).name == "Mateo")
}
test "a guest's copy: the host's slot put, heard and eased toward, and not drawn once it goes quiet" {
fk_setup()
fk_want = 2
npc_tick(0.05)
npc_mirror(true)
expect_eq(npc_count(), 0)
let p = npc_put(3, 99, NPCK_HIKER, 12, 1.5, "Wren", 10.0, 10.0)
test "a guest's copy: the host's slot put, heard and eased toward, and not drawn once it goes quiet" (base_st: mut BaseState, npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) {
fk_setup(base_st, npc_st, npc_tests_fake_st)
npc_tests_fake_st.fk_want = 2
npc_tick(base_st, npc_st, 0.05)
npc_mirror(npc_st, true)
expect_eq(npc_count(npc_st), 0)
let p = npc_put(npc_st, 3, 99, NPCK_HIKER, 12, 1.5, "Wren", 10.0, 10.0)
expect(p.active)
expect_eq(npc_count(), 1)
npc_heard(3, 10.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
npc_heard(3, 12.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
for k in 0 .. 40 { npc_mirror_tick(0.05) }
expect_eq(npc_count(npc_st), 1)
npc_heard(npc_st, 3, 10.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
npc_heard(npc_st, 3, 12.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
for k in 0 .. 40 { npc_mirror_tick(npc_st, 0.05) }
expect_near(p.x, 12.0, 0.01)
expect(p.phase > 0.0)
npc_tick(0.05)
npc_tick(base_st, npc_st, 0.05)
expect_near(p.x, 12.0, 0.01)
for k in 0 .. 140 { npc_mirror_tick(0.05) }
expect(not npc_heard_lately(p))
for k in 0 .. 140 { npc_mirror_tick(npc_st, 0.05) }
expect(not npc_heard_lately(npc_st, p))
}
}

View file

@ -1,53 +1,53 @@
# tick.ludic - a step of everyone, on the machine that runs the place: the census on the half hour,
# the day's end (only ever out of every player's sight), the walk or the act, and who noticed whom
export function npc_tick(dt: float) -> void {
np_ensure()
for i in 0 .. len(np_posts) { if np_posts[i].active { np_step_post(np_posts[i], dt) } }
if np_hold or np_mirror { return }
np_census()
for i in 0 .. len(np_walkers) {
let p = np_walkers[i]
if not p.active or np_leaves(p) { continue }
if p.act == NPCA_WALK { npc_walk(p, dt) } else { npc_act(p, dt) }
npc_notice(p)
export function npc_tick(base_st: mut BaseState, npc_st: mut NpcState, dt: float) -> void {
np_ensure(npc_st)
for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active { np_step_post(base_st, npc_st, npc_st.np_posts[i], dt) } }
if npc_st.np_hold or npc_st.np_mirror { return }
np_census(npc_st)
for i in 0 .. len(npc_st.np_walkers) {
let p = npc_st.np_walkers[i]
if not p.active or np_leaves(npc_st, p) { continue }
if p.act == NPCA_WALK { npc_walk(base_st, npc_st, p, dt) } else { npc_act(npc_st, p, dt) }
npc_notice(base_st, npc_st, p)
}
}
# the posts alone: a machine that does not run the place still turns its vendors to whoever comes
export function npc_posts_tick(dt: float) -> void {
np_ensure()
for i in 0 .. len(np_posts) { if np_posts[i].active { np_step_post(np_posts[i], dt) } }
export function npc_posts_tick(base_st: mut BaseState, npc_st: mut NpcState, dt: float) -> void {
np_ensure(npc_st)
for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active { np_step_post(base_st, npc_st, npc_st.np_posts[i], dt) } }
}
function np_step_post(p: NpcPerson, dt: float) -> void {
npc_act(p, dt)
npc_notice(p)
function np_step_post(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
npc_act(npc_st, p, dt)
npc_notice(base_st, npc_st, p)
}
# counted, not tallied, so it cannot drift; the census fills to what the place wants from the kinds
# by weight (after dark, only the kinds that stay out at night), and brings out an extra when asked
function np_census() -> void {
function np_census(npc_st: mut NpcState) -> void {
let h = NpcWorld.hour()
if Math.abs(h - np_next_h) <= 0.5 { return }
np_next_h = h
np_n = 0
for i in 0 .. len(np_walkers) { if np_walkers[i].active { np_n += 1 } }
let want = Math.min(NpcWorld.want(), np_max)
while np_n < want {
let k = np_draw_kind(np_is_night(h))
if k < 0 or npc_spawn(k) == null { break }
if Math.abs(h - npc_st.np_next_h) <= 0.5 { return }
npc_st.np_next_h = h
npc_st.np_n = 0
for i in 0 .. len(npc_st.np_walkers) { if npc_st.np_walkers[i].active { npc_st.np_n += 1 } }
let want = Math.min(NpcWorld.want(), npc_st.np_max)
while npc_st.np_n < want {
let k = np_draw_kind(npc_st, np_is_night(h))
if k < 0 or npc_spawn(npc_st, k) == null { break }
}
let extra = NpcWorld.extra()
if extra >= 0 and np_n < np_max { npc_spawn(extra) }
if extra >= 0 and npc_st.np_n < npc_st.np_max { npc_spawn(npc_st, extra) }
}
function np_is_night(h: float) -> bool { return h >= 21.0 or h < 5.0 }
function np_draw_kind(night: bool) -> int {
function np_draw_kind(npc_st: mut NpcState, night: bool) -> int {
var total = 0
for k in 0 .. NPCK_COUNT { if np_drawn(k, night) { total += NpcKinds[k].weight } }
if total <= 0 { return -1 }
var r = npc_between(0, total - 1)
var r = npc_between(npc_st, 0, total - 1)
for k in 0 .. NPCK_COUNT {
if not np_drawn(k, night) { continue }
r -= NpcKinds[k].weight
@ -61,40 +61,40 @@ function np_drawn(k: int, night: bool) -> bool { return NpcKinds[k].weight > 0 a
# Its day ends: somebody who does not stay walks out after its hour, and by ten at night nobody is
# out but those who stay; a crowd over what the place wants thins. Only ever where no player sees
# it go, and never while it is out for something (`goal`).
function np_leaves(p: NpcPerson) -> bool {
function np_leaves(npc_st: mut NpcState, p: NpcPerson) -> bool {
if p.goal >= 0 { return false }
let away = npc_near(p.x, p.z)
let away = npc_near(npc_st, p.x, p.z)
let h = NpcWorld.hour()
let k = NpcKinds[p.kind]
var go = false
if not k.stays and h > p.out_h and away > 120.0 { go = true }
if not k.stays and (h > 22.0 or h < 4.0) and away > 70.0 { go = true }
if np_n > NpcWorld.want() and away > 150.0 { go = true }
if go { npc_retire(p) }
if npc_st.np_n > NpcWorld.want() and away > 150.0 { go = true }
if go { npc_retire(npc_st, p) }
return go
}
# the census looks again at the next tick
export function npc_census_now() -> void { np_next_h = -10.0 }
export function npc_census_now(npc_st: mut NpcState) -> void { npc_st.np_next_h = -10.0 }
# held where they stand: no routine and no census (a shot of the poses)
export function npc_hold(on: bool) -> void { np_hold = on }
export function npc_hold(npc_st: mut NpcState, on: bool) -> void { npc_st.np_hold = on }
# everyone gone, as a new trip starts; the posts are the place's and stay
export function npc_reset() -> void {
np_ensure()
for i in 0 .. len(np_walkers) { npc_retire(np_walkers[i]) }
np_n = 0
np_next_h = 0.0
np_hold = false
np_recent = new []string
q_clear(npc_facts())
export function npc_reset(base_st: mut BaseState, npc_st: mut NpcState) -> void {
np_ensure(npc_st)
for i in 0 .. len(npc_st.np_walkers) { npc_retire(npc_st, npc_st.np_walkers[i]) }
npc_st.np_n = 0
npc_st.np_next_h = 0.0
npc_st.np_hold = false
npc_st.np_recent = new []string
q_clear(base_st, npc_facts(base_st, npc_st))
}
# forgotten altogether, walkers and posts (the place itself was replaced)
export function npc_clear() -> void {
np_walkers = new []NpcPerson
np_posts = new []NpcPerson
np_n = 0
np_next_h = 0.0
export function npc_clear(npc_st: mut NpcState) -> void {
npc_st.np_walkers = new []NpcPerson
npc_st.np_posts = new []NpcPerson
npc_st.np_n = 0
npc_st.np_next_h = 0.0
}

View file

@ -1,6 +1,6 @@
# walk.ludic - walking to the target: turn toward it, step, 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
export function npc_walk(p: NpcPerson, dt: float) -> void {
export function npc_walk(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
var gx = p.tx
var gz = p.tz
if p.via_on {
@ -10,7 +10,7 @@ export function npc_walk(p: NpcPerson, dt: float) -> void {
let dx = gx - p.x
let dz = gz - p.z
if Math.sqrt(dx * dx + dz * dz) < 1.2 {
np_arrive(p)
np_arrive(base_st, npc_st, p)
return
}
let dg = npc_d(p.x, p.z, p.tx, p.tz)
@ -19,7 +19,7 @@ export function npc_walk(p: NpcPerson, dt: float) -> void {
p.prog_t = 0.0
} else { p.prog_t = p.prog_t + dt }
if p.prog_t > 20.0 {
np_stuck(p)
np_stuck(npc_st, p)
return
}
var sp = p.speed
@ -39,7 +39,7 @@ export function npc_walk(p: NpcPerson, dt: float) -> void {
p.detours += 1
return
}
np_stuck(p)
np_stuck(npc_st, p)
return
}
p.x = nx
@ -50,18 +50,18 @@ export function npc_walk(p: NpcPerson, dt: float) -> void {
}
# there: a step round something ends, or the errand begins, facing what it came for
function np_arrive(p: NpcPerson) -> void {
function np_arrive(base_st: mut BaseState, npc_st: mut NpcState, p: NpcPerson) -> void {
if p.via_on {
p.via_on = false
return
}
let fx = p.fx
let fz = p.fz
npc_do(p, p.next_act, p.next_t)
npc_do(npc_st, p, p.next_act, p.next_t)
p.fx = fx
p.fz = fz
p.stall = 0.0
if p.act != NPCA_WALK { np_fact(NPC_F_ARRIVED, p, -1) }
if p.act != NPCA_WALK { np_fact(base_st, npc_st, NPC_F_ARRIVED, p, -1) }
}
function np_detour(p: NpcPerson, want: float) -> bool {
@ -80,11 +80,11 @@ function np_detour(p: NpcPerson, want: float) -> bool {
return false
}
function np_stuck(p: NpcPerson) -> void {
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(p)
} else { npc_plan(p) }
npc_wander(npc_st, p)
} else { npc_plan(npc_st, p) }
}