Integrate the Blender character, gear and prop library

NpcAvatar
- select one of 8 library character variants by role/seed (with npc_base and the
  procedural rig as fallbacks), keep Rig/Head and the existing bone animation
- tint only skin/cloth/pants/accessory/hair; fixed detail materials keep their
  colour, npc_vest keeps its hi-vis vest, firefighter/guard use dark uniform tints
- attach caps, hoods, helmets, backpacks, SCBA tanks, lanyards and hand items at
  the documented head/back/chest/hand points (primitive fallbacks retained)

WorldBuilder
- prefer assets/models/blender/props/ for the six replacements and the new
  street dressing (street lamp/tree, palms, planters, A-frame, cone, barrier,
  stanchion, podium, handrail, wall lantern, double door, marquee, sedan)
- fit the same colliders/navigation obstacles and keep the placement validator;
  collect library emissives (bulbs, marquee face, wall lanterns, door LED, sedan
  lights) for set_night_mode()
- add a library triangle counter + 60k budget check (current ~27,986); toggle
  prop gallery to the library models; fix the marquee label placement

Tests/smoke
- character selection/gear attach/fixed-material tint tests; library prop load,
  emissive and triangle-budget tests; smoke_props routes a guest around a
  stanchion in open sidewalk (the old bench route is now crowded by trees)
- headless 222/222, Python 68/68, all smokes pass, compare 11 good / 0 bad

Docs: TODO Phase 3.5 moved to DONE, README/architecture/blender-assets/plan
describe the integrated runtime, MEMORY records the integration rules.
This commit is contained in:
zwitschi committed 2026-11-01 14:57:02 +01:00
1 parent d513e0b82c
commit d8cfddbf89
23 files changed
+531 -165

No files matched your search

+27 -17
View File
@@ -19,6 +19,7 @@ var failures: Array[String] = []
var bench: Node3D = null
var bench_center := Vector3.ZERO
var bench_body: StaticBody3D = null
var route_center := Vector3.ZERO
var guard_start := Vector3.ZERO
var npc: Node3D = null
var npc_start := Vector3.ZERO
@@ -46,12 +47,12 @@ func _process(_delta: float) -> bool:
_find_bench()
if bench == null:
return false
# Stand a couple of metres in front of the bench and walk into it.
var start := bench_center + Vector3(0, 0, 2.2)
# Stand on the door side of the bench and walk into it (+z). The
# street side is crowded with trees/planters now.
var start := bench_center - Vector3(0, 0, 2.0)
start.y = 0.1
gm.guard.global_position = start
# Face the bench (-z): yaw 0 looks down -z, so W walks toward it.
gm.guard.rotation.y = 0.0
gm.guard.rotation.y = PI
guard_start = start
Input.action_press("move_forward")
step = 1
@@ -59,9 +60,9 @@ func _process(_delta: float) -> bool:
1:
if step_frame >= GUARD_PUSH_FRAMES:
Input.action_release("move_forward")
_check(guard_start.z - gm.guard.global_position.z > 0.5,
_check(gm.guard.global_position.z - guard_start.z > 0.5,
"guard walked toward the bench")
_check(gm.guard.global_position.z > bench_center.z + 0.25,
_check(gm.guard.global_position.z < bench_center.z - 0.25,
"guard stopped at the bench instead of passing through it")
_check(not _inside_bench(gm.guard.global_position),
"guard is not inside the bench collider")
@@ -71,16 +72,14 @@ func _process(_delta: float) -> bool:
step = 2
step_frame = 0
2:
if step_frame % 120 == 0 and npc != null and is_instance_valid(npc):
print("npc dbg pos=%s vel=%s arrived=%s wait=%s" % [str(npc.global_position), str(npc.velocity), str(npc.arrived), str(npc.awaiting_interaction)])
if npc == null or not is_instance_valid(npc):
_stage_npc()
return false
if _npc_finished():
_check(npc.global_position.z < bench_center.z - 0.4,
"guest reached the far side of the bench")
_check(not _inside_bench(npc.global_position),
"guest is not inside the bench collider")
_check(npc.global_position.z > route_center.z + 0.35,
"guest reached the far side of the stanchion")
_check(npc.global_position.distance_to(route_center) > 0.4,
"guest is not inside the stanchion collider")
_check(npc_start.distance_to(npc.global_position) > 1.0,
"guest moved while routing around the prop")
_finish()
@@ -112,22 +111,33 @@ func _stage_npc() -> void:
step = 0
step_frame = 0
return
var detail := root.find_child("StreetDetail", true, false)
route_center = bench_center
if detail != null:
for child in detail.get_children():
if str(child.name).begins_with("Prop_stanchion"):
route_center = (child as Node3D).global_position
break
npc = gm.npcs[0]
npc_start = bench_center + Vector3(0, 0.5, 2.5)
npc_start = route_center + Vector3(0, 0.5, -2.0)
npc.global_position = npc_start
npc.velocity = Vector3.ZERO
npc.arrived = false
npc.awaiting_interaction = false
npc.lifetime_s = 999.0
npc.reaction_mode = ""
# Target well past the bench forces a straight line through it, and stays
# beyond the controller's 3 m "arrived" shortcut so the guest must route.
npc.target_position = Vector3(bench_center.x, 0.5, bench_center.z - 5.0)
if npc.npc != null:
# "enter" would send the guest to the door once the path ends; keep this
# test focused on routing around the prop.
npc.npc.intent = "roam"
# A stanchion sits in open sidewalk, so the route is not squeezed by trees
# or bins; the target forces a straight line through it.
npc.target_position = Vector3(route_center.x, 0.5, route_center.z + 5.5)
func _npc_finished() -> bool:
if npc == null or not is_instance_valid(npc):
return false
return npc.global_position.z < bench_center.z - 0.4 \
return npc.global_position.z > route_center.z + 0.35 \
or npc.global_position.distance_to(npc.target_position) < 0.9
func _inside_bench(point: Vector3) -> bool: