feat(packages): ludic.npc - the other people in a place: kinds, spots, acts, names and lines as open registries (a kind's routine is steps by the hour - a spot, an act, how long - with its own start and plan as function values, readable from .lres), a census on the half hour by weight (after dark only who stays out) with an extra the place asks for, walking round what is in the way and never into the water or up a scramble, no act where the place says nobody stops, facing a player within notice and back to the errand, leaving only out of every player's sight, unique names, posts that never walk (a vendor), lines by prio and chance with the story's words through a port and choices, a guest's copy of a host's people; greeted, talked and arrived as facts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 09:59:07 +03:00
parent 73a376ac6c
commit 029a1ebba2
23 changed files with 1664 additions and 0 deletions

View file

@ -0,0 +1,100 @@
# 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)
}
}
# 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) } }
}
function np_step_post(p: NpcPerson, dt: float) -> void {
npc_act(p, dt)
npc_notice(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 {
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 }
}
let extra = NpcWorld.extra()
if extra >= 0 and np_n < np_max { npc_spawn(extra) }
}
function np_is_night(h: float) -> bool { return h >= 21.0 or h < 5.0 }
function np_draw_kind(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)
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(p: NpcPerson) -> bool {
if p.goal >= 0 { return false }
let away = npc_near(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) }
return go
}
# the census looks again at the next tick
export function npc_census_now() -> void { 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 }
# 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())
}
# 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
}