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
230
packages/ludic.npc/tests/npc_test.ludic
Normal file
230
packages/ludic.npc/tests/npc_test.ludic
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
# npc_test.ludic - the census by the hour and by weight, unique names, walking to a spot and never
|
||||
# into the water, a routine's steps by the hour, turning to a player and greeting them once, leaving
|
||||
# out of sight, lines and choices, a post, and a guest's copy
|
||||
import "ludic.npc"
|
||||
import "ludic.base"
|
||||
import "fake"
|
||||
program NpcTest {
|
||||
numbers float
|
||||
bind NpcWorld {
|
||||
ground: fn fk_ground
|
||||
water: fn fk_water
|
||||
allowed: fn fk_allowed
|
||||
hour: fn fk_hour_now
|
||||
day: fn fk_day_now
|
||||
players: fn fk_players
|
||||
player_on: fn fk_player_on
|
||||
player_x: fn fk_x
|
||||
player_z: fn fk_z
|
||||
want: fn fk_wanted
|
||||
extra: fn fk_extra_kind
|
||||
taken: fn fk_taken
|
||||
born: fn fk_on_born
|
||||
gone: fn fk_on_gone
|
||||
}
|
||||
bind NpcTalk { say: fn tk_say, greeting: fn tk_greeting }
|
||||
|
||||
def NpcActs sit { }
|
||||
def NpcActs fish { turns: false }
|
||||
def NpcKinds from "kinds.lres"
|
||||
def NpcNames from "names.lres"
|
||||
def NpcLines from "lines.lres"
|
||||
|
||||
var news_on: bool = false
|
||||
function tk_say(p: NpcPerson, line: int) -> string {
|
||||
if news_on { return `{p.name} saw a bear.` }
|
||||
return ""
|
||||
}
|
||||
function tk_greeting(p: NpcPerson) -> string { return "What'll it be?" }
|
||||
function tk_start(p: NpcPerson) -> void { npc_start_near(p, 0.0, 0.0, 0.0, 10.0) }
|
||||
|
||||
function one(kind: int) -> NpcPerson {
|
||||
fk_setup()
|
||||
fk_want = 1
|
||||
let p = npc_spawn(kind)
|
||||
npc_set_at(p, 0.0, 0.0)
|
||||
return p
|
||||
}
|
||||
|
||||
test "the tables: the package's acts and spots, and the game's kinds, names and lines" {
|
||||
expect_eq(NPCA_WALK, 0)
|
||||
expect_eq(NPCA_IDLE, 1)
|
||||
expect_eq(npcspots_find("shore_here"), NPCS_SHORE_HERE)
|
||||
expect_eq(NPCK_COUNT, 3)
|
||||
expect(NpcKinds[NPCK_CAMPER].stays)
|
||||
expect_eq(len(NpcKinds[NPCK_HIKER].steps), 3)
|
||||
expect_eq(NPCN_COUNT, 14)
|
||||
}
|
||||
|
||||
test "the census fills to what the place wants, by weight, and after dark only who stays out" {
|
||||
fk_setup()
|
||||
fk_want = 4
|
||||
npc_tick(0.05)
|
||||
expect_eq(npc_count(), 4)
|
||||
expect_eq(fk_born, 4)
|
||||
expect_eq(npc_name_clashes(), 0)
|
||||
for i in 0 .. len(npc_walkers()) { expect(npc_walkers()[i].kind != NPCK_VENDOR) }
|
||||
fk_setup()
|
||||
fk_want = 3
|
||||
fk_hour = 23.0
|
||||
npc_tick(0.05)
|
||||
expect_eq(npc_count(), 3)
|
||||
for i in 0 .. len(npc_walkers()) { expect_eq(npc_walkers()[i].kind, NPCK_CAMPER) }
|
||||
}
|
||||
|
||||
test "no name twice, and never one the place keeps" {
|
||||
fk_setup()
|
||||
fk_want = 6
|
||||
npc_tick(0.05)
|
||||
expect_eq(npc_count(), 6)
|
||||
expect_eq(npc_name_clashes(), 0)
|
||||
for i in 0 .. 6 { expect(npc_walkers()[i].name != "Ada") }
|
||||
}
|
||||
|
||||
test "it walks to where it is going and begins there, facing what it came for" {
|
||||
let p = one(NPCK_HIKER)
|
||||
npc_go(p, 20.0, 0.0, NPCA_SIT, 30.0)
|
||||
npc_face(p, 30.0, 0.0)
|
||||
fk_run(30.0)
|
||||
expect_eq(p.act, NPCA_SIT)
|
||||
expect(npc_d(p.x, p.z, 20.0, 0.0) < 1.3)
|
||||
expect(p.phase > 20.0)
|
||||
expect_eq(fk_count(NPC_F_ARRIVED), 1)
|
||||
expect_near(p.fx, 30.0, 0.01)
|
||||
}
|
||||
|
||||
test "nobody walks into the lake" {
|
||||
let p = one(NPCK_HIKER)
|
||||
npc_go(p, -80.0, 0.0, NPCA_SIT, 30.0)
|
||||
var west = 0.0
|
||||
for k in 0 .. 1200 {
|
||||
npc_tick(0.05)
|
||||
west = Math.min(west, p.x)
|
||||
}
|
||||
expect(west > -50.5)
|
||||
}
|
||||
|
||||
test "a step's spot is where its hours say: the shore by day, home by night" {
|
||||
let p = one(NPCK_CAMPER)
|
||||
npc_set_at(p, -20.0, 0.0)
|
||||
fk_hour = 12.0
|
||||
npc_plan(p)
|
||||
expect_eq(p.act, NPCA_WALK)
|
||||
expect_eq(p.next_act, NPCA_FISH)
|
||||
expect(p.fx < -50.0)
|
||||
fk_hour = 23.0
|
||||
npc_plan(p)
|
||||
expect_eq(p.act, NPCA_SIT)
|
||||
}
|
||||
|
||||
test "an act is not begun where the place says nobody stops" {
|
||||
let p = one(NPCK_HIKER)
|
||||
npc_set_at(p, 40.0, 0.0)
|
||||
npc_do(p, NPCA_SIT, 30.0)
|
||||
expect_eq(p.act, NPCA_WALK)
|
||||
expect(fk_allowed(p.tx, p.tz))
|
||||
}
|
||||
|
||||
test "it turns to a player who comes near, greets them once, and again after they have gone" {
|
||||
let p = one(NPCK_HIKER)
|
||||
npc_do(p, NPCA_IDLE, 100.0)
|
||||
p.yaw = 0.0
|
||||
fk_player_at(0.0, 0.0 - 5.0)
|
||||
fk_run(2.0)
|
||||
expect(Math.abs(p.yaw) < 0.05)
|
||||
fk_player_at(5.0, 0.0)
|
||||
fk_run(3.0)
|
||||
expect(Math.abs(npc_wrap(p.yaw + 1.5707963)) < 0.05)
|
||||
fk_run(1.0)
|
||||
expect_eq(fk_count(NPC_F_GREETED), 1)
|
||||
fk_player_at(40.0, 0.0)
|
||||
fk_run(0.5)
|
||||
fk_player_at(5.0, 0.0)
|
||||
fk_run(0.5)
|
||||
expect_eq(fk_count(NPC_F_GREETED), 1)
|
||||
}
|
||||
|
||||
test "a fisher keeps its eyes on the water, whoever is near" {
|
||||
let p = one(NPCK_HIKER)
|
||||
npc_do(p, NPCA_FISH, 100.0)
|
||||
p.yaw = 0.0
|
||||
npc_face(p, 0.0, -10.0)
|
||||
fk_player_at(5.0, 0.0)
|
||||
fk_run(2.0)
|
||||
expect(Math.abs(p.yaw) < 0.05)
|
||||
}
|
||||
|
||||
test "the day ends out of sight, and not in it" {
|
||||
let p = one(NPCK_HIKER)
|
||||
npc_do(p, NPCA_IDLE, 1000.0)
|
||||
fk_hour = 22.5
|
||||
fk_player_at(20.0, 0.0)
|
||||
fk_run(0.5)
|
||||
expect(p.active)
|
||||
fk_player_at(500.0, 0.0)
|
||||
fk_run(0.5)
|
||||
expect(not p.active)
|
||||
expect_eq(fk_gone, 1)
|
||||
let c = one(NPCK_CAMPER)
|
||||
npc_do(c, NPCA_IDLE, 1000.0)
|
||||
fk_hour = 22.5
|
||||
fk_run(0.5)
|
||||
expect(c.active)
|
||||
}
|
||||
|
||||
test "lines: the highest that has something to say, the story's words, and choices" {
|
||||
let p = one(NPCK_CAMPER)
|
||||
npc_do(p, NPCA_FISH, 100.0)
|
||||
expect_eq(npc_talk(p, 0), NPCL_HUSH)
|
||||
expect(npc_said() == "Shh.")
|
||||
expect(npc_talked_today(p))
|
||||
npc_do(p, NPCA_IDLE, 100.0)
|
||||
expect_eq(npc_line_for(p), NPCL_HELLO)
|
||||
news_on = true
|
||||
expect_eq(npc_line_for(p), NPCL_NEWS)
|
||||
expect(npc_said() == `{p.name} saw a bear.`)
|
||||
news_on = false
|
||||
expect_eq(npc_choose(p, NPCL_HELLO, 0, 0), NPCL_FISH_NO)
|
||||
expect(npc_said() == "Not today.")
|
||||
expect_eq(npc_choose(p, NPCL_HELLO, 1, 0), -1)
|
||||
let fs = q_drain(npc_facts())
|
||||
expect_eq(len(fs), 3)
|
||||
expect_eq(fs[1].choice, 7)
|
||||
expect(npc_greeting(p) == "What'll it be?")
|
||||
}
|
||||
|
||||
test "a post stands where it was put, is not counted, and turns back when nobody is near" {
|
||||
fk_setup()
|
||||
let v = npc_place(NPCK_VENDOR, 0.0, 0.0, 0.0, "Mateo", 5)
|
||||
expect(v.fixed)
|
||||
expect_eq(npc_count(), 0)
|
||||
fk_player_at(8.0, 0.0)
|
||||
fk_run(3.0)
|
||||
expect(Math.abs(npc_wrap(v.yaw + 1.5707963)) < 0.05)
|
||||
fk_player_at(300.0, 0.0)
|
||||
fk_run(3.0)
|
||||
expect(Math.abs(v.yaw) < 0.05)
|
||||
expect(npc_d(v.x, v.z, 0.0, 0.0) < 0.001)
|
||||
expect(npc_by_id(v.id).name == "Mateo")
|
||||
}
|
||||
|
||||
test "a guest's copy: the host's slot put, heard and eased toward, and not drawn once it goes quiet" {
|
||||
fk_setup()
|
||||
fk_want = 2
|
||||
npc_tick(0.05)
|
||||
npc_mirror(true)
|
||||
expect_eq(npc_count(), 0)
|
||||
let p = npc_put(3, 99, NPCK_HIKER, 12, 1.5, "Wren", 10.0, 10.0)
|
||||
expect(p.active)
|
||||
expect_eq(npc_count(), 1)
|
||||
npc_heard(3, 10.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
|
||||
npc_heard(3, 12.0, 0.0, 10.0, 0.0, 1.2, NPCA_WALK, false)
|
||||
for k in 0 .. 40 { npc_mirror_tick(0.05) }
|
||||
expect_near(p.x, 12.0, 0.01)
|
||||
expect(p.phase > 0.0)
|
||||
npc_tick(0.05)
|
||||
expect_near(p.x, 12.0, 0.01)
|
||||
for k in 0 .. 140 { npc_mirror_tick(0.05) }
|
||||
expect(not npc_heard_lately(p))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue