Once an NPC reaches its intent waypoint it now does something instead of standing in an idle T-pose: - sit: the rig settles 0.34 m into a seat and the legs swing forward on the steps - photo/film: influencers and tourists raise their camera prop briefly (a sine pulse, so it reads as taking a shot rather than a permanently raised arm) - perform: performers sway the sign board they carry Pose selection lives in NpcAvatar.theater_pose() (intent + arrived); the math is in pure helpers (crouch_for_pose, prop_lift_for_pose, sway_for_pose) so it is headless-testable. Prop/leg nodes are captured at build time and animated from their rest offsets. Performers also get a hand-held board and post. Tests/docs - Headless suite 120 -> 127 (test_npc_theater.gd). - Verified visually with a staged sit/photo/perform lineup in front of the entrance. - README, TODO section moved to DONE, MEMORY. Version 0.2.7.
99 lines
3.9 KiB
GDScript
99 lines
3.9 KiB
GDScript
extends RefCounted
|
|
## NPC theater: seated guests, camera raises, and performers playing to the
|
|
## sidewalk once their intent's waypoint is reached (PLAN.md: NPC Behavior Types).
|
|
|
|
const NpcAvatar := preload("res://src/game/npc_avatar.gd")
|
|
const NpcProfile := preload("res://src/core/npc_profile.gd")
|
|
const Rules := preload("res://src/core/rules.gd")
|
|
|
|
func _avatar(intent: String, npc_type: int = Rules.NpcType.TOURIST) -> Node:
|
|
var p := NpcProfile.new()
|
|
p.id = "theater"
|
|
p.display_name = "Theater"
|
|
p.npc_type = npc_type
|
|
var avatar: Node = NpcAvatar.new()
|
|
avatar.setup(p, intent, 1234, Vector3(2, 0.5, 8), Vector3(0, 0.5, 6))
|
|
return avatar
|
|
|
|
func test_pose_per_intent() -> bool:
|
|
var cases := {
|
|
"sit": "sit",
|
|
"photo": "photo",
|
|
"film": "film",
|
|
"perform": "perform",
|
|
"enter": "idle",
|
|
}
|
|
var ok := true
|
|
for intent in cases.keys():
|
|
var avatar := _avatar(str(intent))
|
|
avatar.arrived = true
|
|
if avatar.theater_pose() != cases[intent]:
|
|
ok = false
|
|
avatar.free()
|
|
return TestUtils.check(ok, "each intent maps to its theater pose once arrived")
|
|
|
|
func test_walking_npc_has_no_pose() -> bool:
|
|
var avatar := _avatar("sit")
|
|
var a := TestUtils.check_eq(avatar.theater_pose(), "idle", "a walking NPC does not sit yet")
|
|
avatar.arrived = true
|
|
var b := TestUtils.check_eq(avatar.theater_pose(), "sit", "arriving starts the pose")
|
|
avatar.free()
|
|
return a and b
|
|
|
|
func test_only_sit_crouches() -> bool:
|
|
var a := TestUtils.check(is_equal_approx(NpcAvatar.crouch_for_pose("sit"), -0.34), "sit lowers the rig")
|
|
var b := TestUtils.check_eq(NpcAvatar.crouch_for_pose("photo"), 0.0, "photo keeps standing")
|
|
var c := TestUtils.check_eq(NpcAvatar.crouch_for_pose("perform"), 0.0, "perform keeps standing")
|
|
var d := TestUtils.check_eq(NpcAvatar.crouch_for_pose("idle"), 0.0, "idle keeps standing")
|
|
return a and b and c and d
|
|
|
|
func test_photo_lift_is_brief() -> bool:
|
|
var raised := 0.0
|
|
var flat := 0.0
|
|
for i in 120:
|
|
var phase := float(i) / 60.0
|
|
var lift: float = NpcAvatar.prop_lift_for_pose("photo", phase)
|
|
raised = maxf(raised, lift)
|
|
if i == 0:
|
|
flat = lift
|
|
var a := TestUtils.check(raised > 0.1, "the camera prop lifts while photographing")
|
|
var b := TestUtils.check(raised <= 0.16 + 0.001, "the lift stays small")
|
|
var c := TestUtils.check_eq(flat, 0.0, "the lift starts at rest")
|
|
var d := TestUtils.check_eq(NpcAvatar.prop_lift_for_pose("sit", 0.5), 0.0, "seated guests do not lift a prop")
|
|
return a and b and c and d
|
|
|
|
func test_performer_sways_only_when_performing() -> bool:
|
|
var peak := 0.0
|
|
for i in 120:
|
|
peak = maxf(peak, absf(NpcAvatar.sway_for_pose("perform", float(i) / 60.0)))
|
|
var a := TestUtils.check(peak > 0.05, "the performer's sign sways")
|
|
var b := TestUtils.check_eq(NpcAvatar.sway_for_pose("photo", 1.0), 0.0, "photographers do not sway")
|
|
var c := TestUtils.check_eq(NpcAvatar.sway_for_pose("sit", 1.0), 0.0, "seated guests do not sway")
|
|
return a and b and c
|
|
|
|
func test_photo_prop_is_wired_for_camera_archetypes() -> bool:
|
|
var influencer := _avatar("photo", Rules.NpcType.INFLUENCER)
|
|
var tourist := _avatar("photo", Rules.NpcType.TOURIST)
|
|
var performer := _avatar("perform", Rules.NpcType.PERFORMER)
|
|
var a := TestUtils.check(influencer._photo_prop != null, "influencer carries a camera prop")
|
|
var b := TestUtils.check(tourist._photo_prop != null, "tourist carries a camera prop")
|
|
var c := TestUtils.check(performer._sign_prop != null, "performer carries a sign prop")
|
|
influencer.free()
|
|
tourist.free()
|
|
performer.free()
|
|
return a and b and c
|
|
|
|
func test_sitting_rig_actually_lowers() -> bool:
|
|
var avatar := _avatar("sit")
|
|
avatar.arrived = true
|
|
for i in 40:
|
|
avatar._animate(0.016)
|
|
var a := TestUtils.check(avatar._rig.position.y < -0.2, "the rig settles into the crouch")
|
|
var legs_bent := TestUtils.check(absf(avatar._legs[0].rotation.x) > deg_to_rad(30.0), "legs bend for the seat")
|
|
avatar.arrived = false
|
|
for i in 60:
|
|
avatar._animate(0.016)
|
|
var b := TestUtils.check(avatar._rig.position.y > -0.1, "standing back up lifts the rig")
|
|
avatar.free()
|
|
return a and legs_bent and b
|