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
114
packages/ludic.npc/README.md
Normal file
114
packages/ludic.npc/README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# ludic.npc
|
||||
|
||||
The other people in a place: each with a routine by the hour rather than an intelligence - where
|
||||
they start, where they go and what they do there, when they walk out - walking between spots and
|
||||
round whatever is in the way, never into the water, turning to face a player who comes near, and
|
||||
saying one line at a time from a table of lines. Uses [`ludic.base`](../ludic.base/README.md) and
|
||||
nothing else.
|
||||
|
||||
```ludic
|
||||
import "ludic.npc"
|
||||
```
|
||||
|
||||
The package owns the people's state, the census, the walk, the acts, the lines, its own dice and a
|
||||
guest's copy of a host's people. It never draws: the port says a person was `born` (make an actor,
|
||||
key it by the person's `slot`, or `id` across machines) and `gone`, and the game reads `x`, `y`,
|
||||
`z`, `yaw`, `act`, `stalk` and `phase` (2.6 a metre walked) every frame to pose it. Clips are the
|
||||
game's too (`NpcAct.clip` is a hint: `"walk"`, `"idle"`, or `""` for a pose of its own).
|
||||
|
||||
## Four tables the game fills
|
||||
|
||||
- **`NpcKinds`** - a kind of person: `weight` in the census draw (`night`: the only kinds drawn
|
||||
after dark), whether it `stays` the night, `leave_lo` .. `leave_hi` (the hour it walks out),
|
||||
`notice` (m it turns to face a player), `start` (where a new one goes: call
|
||||
`npc_start_near(p, x, z, r0, r1)`), `plan` (the first word on what next; true: decided) and
|
||||
`steps`, its routine.
|
||||
- **`NpcStep`** (inside a kind) - in the hours `from` .. `to` (round midnight when `from > to`),
|
||||
`weight` of the draw, go to `spot` (`near` .. `far` m, `n` the spot's own number) and do `act`
|
||||
for `lo` .. `hi` seconds. A step whose spot cannot be found gives way to the next one in the table.
|
||||
- **`NpcSpots`** - where a step goes; `find(p, step)` sends the person (`npc_go` / `npc_do`, or
|
||||
`npc_step_go`) or says it cannot. The package's own: `here`, `home`, `near_home`, `near_here`,
|
||||
`shore_home`, `shore_here` (facing the water) and `wander`; a game adds its own (a sign, an animal).
|
||||
- **`NpcActs`** - what somebody does; the package's `walk` (`NPCA_WALK`) and `idle` come first.
|
||||
`turns` (false: keeps its eyes on its work), `clip`, and `tick(p, dt)` (the game's while it lasts;
|
||||
true: decided).
|
||||
- **`NpcNames`** (`name`, `body`) and **`NpcLines`** - see below.
|
||||
|
||||
A kind's `.lres` may name functions (`start: fn my_start`), and a step list is `[{ ... }, ...]`.
|
||||
|
||||
## The rules it keeps
|
||||
|
||||
1. **A routine, not a mind.** A person plans on arriving and when its act runs out: the kind's
|
||||
`plan`, then a weighted draw among its steps for this hour, then standing a while.
|
||||
2. **Nobody walks into the lake or up a scramble**: a step into water (0.4 m) or up more than 1.4x
|
||||
the step is refused; eight steps round are tried, and a target that gets no closer for twenty
|
||||
seconds is given up (three times: a few steps anywhere walkable).
|
||||
3. **Nobody stops where the place says not to** (`NpcWorld.allowed`: a player's camp). They may
|
||||
walk through; an act begun there walks out first.
|
||||
4. **They face whoever came over to talk**, within the kind's `notice` - unless creeping or at an
|
||||
act that does not `turns` - and go back to their errand when the player walks off. A player
|
||||
coming within notice is a `Greeted` fact, once, until they have gone half as far again.
|
||||
5. **The census is counted, not tallied**, on the half hour: up to `NpcWorld.want()` (and never
|
||||
past `npc_config`'s cap), plus `NpcWorld.extra()` whatever the crowd.
|
||||
6. **They leave only out of every player's sight**: after their hour at 120 m, at night (by 22:00,
|
||||
except those who `stay`) at 70 m, and a crowd over what is wanted at 150 m - never while out
|
||||
for something (`goal >= 0`).
|
||||
7. **No name twice**: not another person's, not one the place keeps (`NpcWorld.taken`), not one
|
||||
that left lately.
|
||||
8. **Posts** (`npc_place`: a vendor) never walk, are never counted and never leave; they turn to a
|
||||
player within notice and back to their stance.
|
||||
|
||||
## Lines as data
|
||||
|
||||
```
|
||||
hush { prio: 5, act: NPCA_PHOTO, fill: true } # the story's words (NpcTalk.say)
|
||||
sighting { prio: 4, chance: 30, fill: true } # considered three times in ten
|
||||
water { prio: 3, act: NPCA_WATER, text: "Boil it, even up here." }
|
||||
hello { prio: 1, text: "Morning.", choices: [{ text: "Any fish?", line: "fish", code: 7 }] }
|
||||
```
|
||||
|
||||
The highest `prio` with anything to say wins, each line of it in table order given its `chance`;
|
||||
a `fill` line's words are `NpcTalk.say(p, line)`, and "" means it has nothing true to say now.
|
||||
`npc_talk` picks one (a `Talked` fact); `npc_choose` answers a choice with the line it names (a
|
||||
`Talked` fact carrying the choice's `code`). A vendor's greeting is `NpcTalk.greeting(p)`.
|
||||
|
||||
## The ports
|
||||
|
||||
```ludic
|
||||
export port NpcWorld { # every member has a default: a flat dry field at noon, one player, nobody wanted
|
||||
ground, water, slope: fn(float, float) -> float
|
||||
allowed: fn(float, float) -> bool # may a person stop here
|
||||
hour: fn() -> float, day: fn() -> int # the clock
|
||||
players: fn() -> int, player_on: fn(int) -> bool, player_x / player_z: fn(int) -> float
|
||||
want: fn() -> int, extra: fn() -> int # the census: how many, and a kind brought out regardless (-1)
|
||||
origin_x / origin_z: fn() -> float # where anyone goes when nowhere else will do
|
||||
next_id: fn() -> int, body_of: fn(int) -> int # an id across machines; a look seed's body
|
||||
taken: fn(string) -> bool # a name the place keeps
|
||||
born / gone: fn(NpcPerson) -> void # draw it, stop drawing it
|
||||
}
|
||||
export port NpcTalk { say: fn(NpcPerson, int) -> string, greeting: fn(NpcPerson) -> string }
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `NpcPerson`, `npc_walkers()`, `npc_posts()`, `npc_at(slot)`, `npc_post(i)`, `npc_by_id(id)`, `npc_count()`, `npc_max()` | the people (`goal`, `subj`, `note`, `note_h`, `flags` are the game's) |
|
||||
| `npc_config(max)`, `npc_seed(n)`, `npc_rnd()`, `npc_between(lo, hi)`, `npc_secs(lo, hi)` | the cap and the package's own dice |
|
||||
| `npc_tick(dt)`, `npc_posts_tick(dt)`, `npc_census_now()`, `npc_hold(on)`, `npc_reset()`, `npc_clear()` | a step where the place is run (or of the posts alone), the census at once, held still, a new trip, a new place |
|
||||
| `npc_spawn(kind)`, `npc_place(kind, x, z, yaw, name, look)`, `npc_retire(p)`, `npc_start_near(p, x, z, r0, r1)`, `npc_set_at(p, x, z)` | making and leaving |
|
||||
| `npc_go(p, x, z, act, secs)`, `npc_do(p, act, secs)`, `npc_face(p, x, z)`, `npc_face_ahead(p)`, `npc_set_home_face(p, x, z)`, `npc_plan(p)`, `npc_steps(p, steps)`, `npc_step_go(p, step, x, z)`, `npc_wander(p)` | the routine |
|
||||
| `npc_walk(p, dt)`, `npc_act(p, dt)`, `npc_notice(p)` | a person's step |
|
||||
| `npc_walkable(x, z)`, `npc_line_ok(ax, az, bx, bz)`, `npc_spot_near(x, z, d0, d1)`, `npc_toward(ax, az, bx, bz, d)`, `npc_shore(x, z, r)`, `npc_px()`, `npc_pz()`, `npc_wx()`, `npc_wz()` | the ground |
|
||||
| `npc_near(x, z)`, `npc_near_i()`, `npc_d(...)`, `npc_wrap(a)` | the nearest player, distances, angles |
|
||||
| `npc_pick_name(body)`, `npc_name_used(n)`, `npc_name_clashes()` | names |
|
||||
| `npc_talk(p, player)`, `npc_line_for(p)`, `npc_words(p, line)`, `npc_said()`, `npc_choose(p, line, c, player)`, `npc_talked_today(p)`, `npc_greeting(p)` | talk |
|
||||
| `npc_mirror(on)`, `npc_mirroring()`, `npc_put(slot, id, kind, look, seed, name, hx, hz)`, `npc_heard(slot, x, y, z, yaw, speed, act, stalk)`, `npc_heard_lately(p)`, `npc_mirror_tick(dt)` | a guest's copy of the host's people |
|
||||
| `npc_facts() -> Queue<NpcFact>` | `NPC_F_GREETED` (`player`), `NPC_F_TALKED` (`player`, `line`, `choice`), `NPC_F_ARRIVED` (`act`) |
|
||||
| `npc_system() -> System` | `"npc"`, `PH_SIMULATE`, a reset and no tick; nothing saved |
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
ludic test packages/ludic.npc
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue