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:
parent
73a376ac6c
commit
029a1ebba2
23 changed files with 1664 additions and 0 deletions
100
packages/ludic.npc/tick.ludic
Normal file
100
packages/ludic.npc/tick.ludic
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue