Add NPC theater poses: sit, photo, film, perform
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.
This commit is contained in:
1 parent
1b976bf351
commit
e2c2a0bf4c
9 files changed
+225
-35
No files matched your search
@@ -0,0 +1,98 @@
|
||||
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
|
||||
Reference in new issue
Block a user