Implement control pass: focus cone, action wheel, named inputs, boot fixes
Full build / Import + headless tests (push) Successful in 22s
Full build / Full desktop build + optional release (push) Skipped

- 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.
This commit is contained in:
zwitschi committed 2026-10-31 15:58:01 +01:00
1 parent 69484866db
commit f11b441c07
28 files changed
+1433 -249

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
extends SceneTree
## Windowed smoke helper for the ADR-0007–0010 control pass (focus ring + action
## wheel). Boots the real main scene, stages one NPC in front of the guard, opens
## the wheel, and saves two PNGs to user://screenshots/. Exits 1 if the focus or
## radial state did not come up, so it can be used as a smoke check.
## Usage: godot --path . --script res://src/test/smoke_focus_radial.gd
const OUT_DIR := "user://screenshots"
const MAX_FRAMES := 1800
const STAGE_DISTANCE := 2.4
var frames := 0
var gm: Node = null
var state := 0
var state_frames := 0
var stage_npc: Node3D = null
var failures: Array[String] = []
func _initialize() -> void:
root.size = Vector2i(1280, 720)
var packed: PackedScene = load("res://scenes/main.tscn")
root.add_child(packed.instantiate())
func _process(_delta: float) -> bool:
frames += 1
if gm == null:
gm = root.find_child("GameManager", true, false)
if gm == null:
if frames > 600:
_fail("GameManager not found")
return false
if frames > MAX_FRAMES:
_fail("timeout before the radial state")
return true
_advance()
return false
func _advance() -> void:
state_frames += 1
match state:
0:
# Wait for the guard to settle on the ground, then grab one NPC.
if state_frames >= 40 and gm.npcs.size() > 0:
print("smoke: spawn guard_y=%.2f z=%.2f" % [
(gm.guard.global_position as Vector3).y,
(gm.guard.global_position as Vector3).z,
])
_shoot("smoke-00-spawn.png")
stage_npc = gm.npcs[0]
_hide_other_npcs(stage_npc)
state = 1
state_frames = 0
1:
# Keep the staged NPC in front of the guard so the ring is visible.
_stage_target()
if state_frames >= 20:
gm.call("_cycle_nearby_target")
if gm.targeted_npc == null:
_fail("no NPC focused")
elif gm.radial == null:
_fail("radial layer missing")
else:
print("smoke: guard_y=%.2f npc_y=%.2f" % [
(gm.guard.global_position as Vector3).y,
(gm.targeted_npc.global_position as Vector3).y,
])
_shoot("smoke-01-focus-ring.png")
gm.call("_open_radial")
state = 2
state_frames = 0
2:
if state_frames >= 20:
if not gm.radial.is_open:
_fail("action wheel did not open")
else:
_shoot("smoke-02-action-wheel.png")
gm.call("_cancel_radial")
state = 3
state_frames = 0
3:
if state_frames >= 5:
if gm.radial.is_open:
_fail("action wheel did not close on cancel")
_finish()
func _stage_target() -> void:
if stage_npc == null or not is_instance_valid(stage_npc):
stage_npc = gm.npcs[0] if gm.npcs.size() > 0 else null
if stage_npc == null:
return
_hide_other_npcs(stage_npc)
var guard_pos: Vector3 = gm.guard.global_position
var forward: Vector3 = -gm.guard.global_transform.basis.z
forward.y = 0.0
forward = forward.normalized()
stage_npc.global_position = guard_pos + forward * STAGE_DISTANCE + Vector3(0, 0.4, 0)
stage_npc.rotation.y = gm.guard.rotation.y + PI
func _hide_other_npcs(keep: Node3D) -> void:
for npc in gm.npcs:
if is_instance_valid(npc) and npc is Node3D and npc != keep:
(npc as Node3D).visible = false
func _shoot(file_name: String) -> void:
var dir := DirAccess.open("user://")
if dir == null or not dir.dir_exists("screenshots"):
dir.make_dir_recursive("screenshots")
var img: Image = root.get_texture().get_image()
var err: int = img.save_png(OUT_DIR.path_join(file_name))
print("%s -> err=%d (%dx%d)" % [file_name, err, img.get_width(), img.get_height()])
func _fail(message: String) -> void:
failures.append(message)
_finish()
func _finish() -> void:
if failures.is_empty():
print("SMOKE OK: focus ring + action wheel opened and cancelled")
quit(0)
else:
for message in failures:
printerr("SMOKE FAIL: " + message)
quit(1)