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.
175 lines
5.4 KiB
GDScript
175 lines
5.4 KiB
GDScript
extends SceneTree
|
|
## Prop-collision smoke: proves the guard and an NPC cannot walk through street
|
|
## furniture, and that a guest slides around a bench instead of passing through
|
|
## it. Windowed (the guard reads captured-mouse input):
|
|
##
|
|
## godot --path . --script res://src/test/smoke_props.gd
|
|
##
|
|
## Exit code 0 = pass, 1 = failure.
|
|
|
|
const MAX_FRAMES := 1800
|
|
const GUARD_PUSH_FRAMES := 100
|
|
|
|
var frame := 0
|
|
var gm: Node = null
|
|
var started := false
|
|
var step := 0
|
|
var step_frame := 0
|
|
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
|
|
|
|
func _initialize() -> void:
|
|
root.size = Vector2i(1280, 720)
|
|
root.add_child(load("res://scenes/main.tscn").instantiate())
|
|
|
|
func _process(_delta: float) -> bool:
|
|
frame += 1
|
|
if gm == null:
|
|
gm = root.find_child("GameManager", true, false)
|
|
return false
|
|
if not started:
|
|
gm.call("_on_venue_selected", "hollywood_building")
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
started = true
|
|
return false
|
|
if frame > MAX_FRAMES:
|
|
_fail("timeout before the collision checks finished")
|
|
return true
|
|
step_frame += 1
|
|
match step:
|
|
0:
|
|
_find_bench()
|
|
if bench == null:
|
|
return false
|
|
# 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
|
|
gm.guard.rotation.y = PI
|
|
guard_start = start
|
|
Input.action_press("move_forward")
|
|
step = 1
|
|
step_frame = 0
|
|
1:
|
|
if step_frame >= GUARD_PUSH_FRAMES:
|
|
Input.action_release("move_forward")
|
|
_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,
|
|
"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")
|
|
# Step the guard aside so the guest test hits the bench, not the player.
|
|
gm.guard.global_position = bench_center + Vector3(4.0, -0.08, 2.2)
|
|
_stage_npc()
|
|
step = 2
|
|
step_frame = 0
|
|
2:
|
|
if npc == null or not is_instance_valid(npc):
|
|
_stage_npc()
|
|
return false
|
|
if _npc_finished():
|
|
_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()
|
|
return true
|
|
return false
|
|
|
|
func _find_bench() -> void:
|
|
var detail := gm.get_node_or_null("World/StreetDetail")
|
|
if detail == null:
|
|
# The world root name differs between scene builds; search instead.
|
|
detail = root.find_child("StreetDetail", true, false)
|
|
if detail == null:
|
|
_fail("StreetDetail not found")
|
|
return
|
|
for child in detail.get_children():
|
|
if str(child.name).begins_with("Prop_bench"):
|
|
bench = child as Node3D
|
|
break
|
|
if bench == null:
|
|
_fail("no bench prop placed")
|
|
return
|
|
bench_center = bench.global_position
|
|
bench_body = bench.get_node_or_null("Collision") as StaticBody3D
|
|
if bench_body == null:
|
|
_fail("bench has no static collider")
|
|
|
|
func _stage_npc() -> void:
|
|
if gm.npcs.is_empty():
|
|
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 = 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 = ""
|
|
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 > route_center.z + 0.35 \
|
|
or npc.global_position.distance_to(npc.target_position) < 0.9
|
|
|
|
func _inside_bench(point: Vector3) -> bool:
|
|
if bench_body == null:
|
|
return false
|
|
var collider := bench_body.get_node_or_null("Shape") as CollisionShape3D
|
|
if collider == null or not (collider.shape is BoxShape3D):
|
|
return false
|
|
var box := collider.shape as BoxShape3D
|
|
var half := box.size * 0.5
|
|
var center := collider.global_position
|
|
half.x += 0.1
|
|
half.z += 0.1
|
|
return absf(point.x - center.x) < half.x and absf(point.z - center.z) < half.z
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if condition:
|
|
print("ok: " + message)
|
|
else:
|
|
_fail(message)
|
|
|
|
func _fail(message: String) -> void:
|
|
failures.append(message)
|
|
_finish()
|
|
|
|
func _finish() -> void:
|
|
Input.action_release("move_forward")
|
|
if failures.is_empty():
|
|
print("SMOKE OK: props block the guard and guests route around them")
|
|
quit(0)
|
|
else:
|
|
for message in failures:
|
|
printerr("SMOKE FAIL: " + message)
|
|
quit(1)
|