Give street props colliders, zoned placements and variants
Full build / Full desktop build + optional release (push) Successful in 4m2s
Full build / Import + headless tests (push) Successful in 31s

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:
zwitschi committed 2026-11-01 10:16:53 +01:00
1 parent a3e90accdd
commit bb40a98537
18 files changed
+518 -66

No files matched your search

+33 -3
View File
@@ -73,6 +73,15 @@ func setup(profile: NpcProfile, intent: String, seed_value: int, p_spawn: Vector
agent.target_desired_distance = 4.0
agent.radius = 0.35
agent.height = 1.2
# Steer around street furniture instead of walking into it (props publish a
# NavigationObstacle3D; the navmesh itself stays a flat walkable plane).
agent.avoidance_enabled = true
agent.neighbor_distance = 4.0
agent.max_neighbors = 8
agent.time_horizon_agents = 0.6
agent.time_horizon_obstacles = 1.2
agent.max_speed = FLEE_SPEED
agent.velocity_computed.connect(_on_safe_velocity)
add_child(agent)
# Normalize intent targets: approach the entrance, stop near the door.
@@ -98,6 +107,7 @@ var _sign_prop: MeshInstance3D = null
var _legs: Array[MeshInstance3D] = []
var _arms: Array[MeshInstance3D] = []
var _theater_t := 0.0
var _avoidance_active := false
var _photo_prop_base_y := 0.0
var _sign_prop_base_y := 0.0
const SKIN_TONE := Color(0.88, 0.72, 0.60)
@@ -527,6 +537,7 @@ func _advance_fsm(delta: float) -> void:
var leave_requested_by_lifetime := false
func _move_toward_target(delta: float) -> void:
_avoidance_active = false
if agent == null:
_move_direct(delta)
return
@@ -575,14 +586,33 @@ func _move_toward_target(delta: float) -> void:
return
dir = dir.normalized()
var speed := FLEE_SPEED if reaction_mode == "flee" else 1.3
velocity.x = dir.x * speed
velocity.z = dir.z * speed
velocity.y -= 9.8 * delta
look_at(global_position + Vector3(dir.x, 0, dir.z), Vector3.UP)
if agent.avoidance_enabled:
# The navigation server returns the obstacle-aware velocity through
# velocity_computed; never dash straight at the point in the meantime.
_avoidance_active = true
agent.velocity = Vector3(dir.x * speed, 0.0, dir.z * speed)
else:
velocity.x = dir.x * speed
velocity.z = dir.z * speed
velocity.y -= 9.8 * delta
move_and_slide()
## Safe velocity from NavigationServer avoidance: move with the steered vector.
func _on_safe_velocity(safe: Vector3) -> void:
if not _avoidance_active:
return
velocity.x = safe.x
velocity.z = safe.z
if is_on_floor():
velocity.y = 0.0
else:
velocity.y -= 9.8 * get_physics_process_delta_time()
move_and_slide()
## Fallback used when no navigation mesh is available (e.g. minimal scenes).
func _move_direct(delta: float) -> void:
_avoidance_active = false
var to_target := target_position - global_position
to_target.y = 0.0
var dist := to_target.length()