zones_set / zones_clear / zones_merge, zones_contains / zones_nearest and the clusters' bounding circles, over fixed arrays of ZONES_MAX (16). WildlifeWorld.active freezes an animal outside (no state change, no dice), NpcWorld.active a walker (and no birth outside), VehicleWorld.active a moored boat, and ThingsWorld.restocks keeps a morning's restock inside; every default is "everywhere", so an unbound game is unchanged. Tests: the merge (near players one cluster, far two, a player leaving splits a chain), contains, nearest, the radius; wildlife, npc, vehicles and things pass as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
4 KiB
Text
98 lines
4 KiB
Text
# 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(npc_st: mut NpcState, dt: float) -> void {
|
|
for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active { np_step_post(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 not NpcWorld.active(p.x, p.z) or np_leaves(npc_st, p) { continue }
|
|
p.goal_speed = 0.0
|
|
if p.act == NPCA_WALK { npc_walk(npc_st, p, dt) } else { npc_act(npc_st, p, dt) }
|
|
npc_notice(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(npc_st: mut NpcState, dt: float) -> void {
|
|
for i in 0 .. len(npc_st.np_posts) { if npc_st.np_posts[i].active { np_step_post(npc_st, npc_st.np_posts[i], dt) } }
|
|
}
|
|
|
|
function np_step_post(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
|
|
npc_act(npc_st, p, dt)
|
|
npc_notice(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(npc_st: mut NpcState) -> void {
|
|
let h = NpcWorld.hour()
|
|
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 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(npc_st: 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(npc_st, 0, total - 1)
|
|
for k in 0 .. NPCK_COUNT {
|
|
if not np_drawn(k, night) { continue }
|
|
r -= NpcKinds[k].weight
|
|
if r < 0 { return k }
|
|
}
|
|
return -1
|
|
}
|
|
|
|
function np_drawn(k: int, night: bool) -> bool { return NpcKinds[k].weight > 0 and (NpcKinds[k].night or not night) }
|
|
|
|
# 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(npc_st: mut NpcState, p: NpcPerson) -> bool {
|
|
if p.goal >= 0 { return false }
|
|
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 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(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(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(npc_st: mut NpcState) -> void {
|
|
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
|
|
np_recent_clear(npc_st)
|
|
q_clear(npc_facts(npc_st))
|
|
}
|
|
|
|
# forgotten altogether, walkers and posts (the place itself was replaced)
|
|
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
|
|
}
|