ludic/packages/ludic.npc/tick.ludic
Orkuncakilkaya 8b85282502 ludic.zones: a disc per player from their fog, merged into clusters; the simulating packages ask whether a place is simulated
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>
2026-09-29 15:53:23 +03:00

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
}