- Venue-correct access artifacts (AccessLoadout) and screened-aware verify - Cone focus on NPC heads with billboarded ring + display name (ADR-0008) - Action wheel with de-escalation-first slices (ADR-0009); Esc/RMB cancel - Procedural NPC face/hair/neck/shoes (ADR-0010); named input actions - Fix GameManager parse error (radial typed as ActionRadial) - Fix first-person camera height and guard spawning inside the building - Move building volume back so the door/awning/marquee are visible - Pull the event scheduler once per simulated second; cap live NPCs - Regenerate README screenshots; update TODO/DONE/MEMORY/README; 77 tests Adds src/test/smoke_focus_radial.gd for the windowed focus/radial smoke.
34 lines
1.1 KiB
GDScript
34 lines
1.1 KiB
GDScript
extends RefCounted
|
|
## World-space focus cone (ADR-0008). Picks the head closest to the camera
|
|
## forward vector inside range and angle. No viewport needed, so tests run headless.
|
|
|
|
const MAX_RANGE := 10.0
|
|
const MAX_ANGLE_RAD := 0.175 # ~10 degrees
|
|
|
|
static func score_head(cam_origin: Vector3, cam_forward: Vector3, head_pos: Vector3, max_range: float = MAX_RANGE, max_angle: float = MAX_ANGLE_RAD) -> float:
|
|
var to := head_pos - cam_origin
|
|
var dist := to.length()
|
|
if dist < 0.05 or dist > max_range:
|
|
return -1.0
|
|
var dir := to / dist
|
|
var dot := cam_forward.dot(dir)
|
|
if dot <= 0.0:
|
|
return -1.0
|
|
var ang := acos(clampf(dot, -1.0, 1.0))
|
|
if ang > max_angle:
|
|
return -1.0
|
|
return ang
|
|
|
|
static func pick_index(cam_origin: Vector3, cam_forward: Vector3, head_positions: Array, max_range: float = MAX_RANGE, max_angle: float = MAX_ANGLE_RAD) -> int:
|
|
var best_i := -1
|
|
var best_score := 999.0
|
|
for i in head_positions.size():
|
|
var pos: Vector3 = head_positions[i]
|
|
var score := score_head(cam_origin, cam_forward, pos, max_range, max_angle)
|
|
if score < 0.0:
|
|
continue
|
|
if score < best_score:
|
|
best_score = score
|
|
best_i = i
|
|
return best_i
|