Give street props colliders, zoned placements and variants
Props were decoration: the guard and guests walked straight through benches, bins, bollards and planters. They now block movement and steer guests around. Collision - _add_prop_model() fits a StaticBody3D from the model's mesh bounds in holder-local space: a cylinder for bollard/bin/hydrant, a box otherwise, so placement rotation and variant scale carry over. - Fallback primitives go through the same fit, so a missing .glb cannot silently remove collision. - Each prop also publishes a NavigationObstacle3D; NpcAvatar enables agent avoidance and moves with the safe velocity from velocity_computed. The navmesh stays a flat walkable plane and avoidance does the steering. Placement - PROP_PLACEMENTS groups furniture by entrance, mid-block and curb zones, with a static placement_problems() validator for bounds, overlap/min-spacing, duplicates, and the door/steps/guest-lane exclusions. - variant_for(index) gives deterministic yaw jitter, subtle scale, and optional clutter (bag by the bin, crate by the newspaper boxes, cone by the hydrant). Verification - New windowed smoke_props.gd: guard walks into a bench and stops; a guest headed beyond the bench routes around it and never enters the collider. - Headless 182 -> 187: placement validation and its failure modes, variant determinism/range, collider + obstacle on every placed prop, fallback collider. - Other smokes (PLAN verbs, crowd, emergency, timeline) still pass; Python tooling 53/53; screenshots recaptured; compare 9 good / 0 bad. - Version 0.4.0. TODO: collision/placement section moved to DONE; prop pipeline checks remain.
This commit is contained in:
1 parent
a3e90accdd
commit
bb40a98537
18 files changed
+518
-66
No files matched your search
@@ -0,0 +1,165 @@
|
||||
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 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 a couple of metres in front of the bench and walk into it.
|
||||
var start := bench_center + Vector3(0, 0, 2.2)
|
||||
start.y = 0.1
|
||||
gm.guard.global_position = start
|
||||
# Face the bench (-z): with the guard's basis, forward input at yaw PI
|
||||
# walks toward lower z.
|
||||
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(guard_start.z - gm.guard.global_position.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 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_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
|
||||
npc = gm.npcs[0]
|
||||
npc_start = bench_center + Vector3(0, 0.5, 2.5)
|
||||
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)
|
||||
|
||||
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 \
|
||||
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)
|
||||
Reference in new issue
Block a user