Character and movement pass
Full build / Full desktop build + optional release (push) Successful in 3m37s
Full build / Import + headless tests (push) Successful in 33s

NPC characters
- Articulated rig (hips/knees, shoulders/elbows/hands) replaces rigid capsules;
  the walk cycle bends knees and elbows and flees with a longer stride.
- Layered outfits (jacket/vest/coat/dress), extended accessories and hand items
  (bag, umbrella, phone, camera, clipboard, cup), face details and five hair
  silhouettes, all deterministic per profile seed.
- Idle behaviour layers (phone, clipboard, crossed arms, look-around), breathing
  and weight shift; escalation raises hands or points, and noticed NPCs turn
  toward the guard.
- Reaction symbols above the head (!!, ?!, !, ?), door-queue offsets assigned by
  GameManager, three LOD tiers with hysteresis and a billboard impostor.
- smoke_npc_budget.gd: 16 avatars = 534 meshes, ~6.9 ms average (50 ms budget).

Player movement
- Separate accel/decel, step-up assist for kerbs and the entrance steps, and a
  forward-axis fix: W was inverted (get_axis(forward, back) is negative).
- Head bob, landing dip and precision-walk FOV, gated by persisted head-bob and
  invert-Y settings; precision chip and tick; surface-aware footsteps and a
  landing cue forwarded to AudioFeedback.
- Gamepad support (sticks, deadzone, face buttons, bumpers), a control legend
  that follows the active device, and named actions for the remaining keys.
- Key/button remapping in Settings, persisted as input_bindings and applied by
  InputBindings (mouse and gamepad axes are never removed).
- smoke_movement.gd (windowed): climbs the steps, checks precision speed, chip,
  footsteps and landing; new 11-precision-walk capture and catalogue entry.

Verification: headless 208/208, Python tooling 56/56, all 11 smokes pass,
compare 11 good / 0 bad, movement judge match. Version 0.5.0. Only optional
Future ideas remain in TODO.
This commit is contained in:
zwitschi committed 2026-11-01 10:41:17 +01:00
1 parent 2a4e0d1998
commit a27e8cb74c
35 files changed
+1608 -185

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
extends SceneTree
## NPC performance budget smoke: put the documented live-NPC count in the scene,
## let the animations run, then report the average frame time. The budget is
## 50 ms (20 FPS) so slow CI hardware does not fail on noise; the report is the
## useful part when tuning the rig.
##
## Usage: godot --headless --path . --script res://src/test/smoke_npc_budget.gd
const NpcAvatar := preload("res://src/game/npc_avatar.gd")
const NpcProfile := preload("res://src/core/npc_profile.gd")
const Rules := preload("res://src/core/rules.gd")
const COUNT := 16
const WARMUP_FRAMES := 60
const SAMPLE_FRAMES := 180
const BUDGET_MS := 50.0
var frame := 0
var avatars: Array = []
var sample_ms := 0.0
func _initialize() -> void:
root.size = Vector2i(1280, 720)
var stage := Node3D.new()
stage.name = "BudgetStage"
root.add_child(stage)
for index in COUNT:
var profile := NpcProfile.new()
profile.id = "budget_%d" % index
profile.npc_type = Rules.NpcType.TOURIST
profile.display_name = "Budget %d" % index
var avatar := NpcAvatar.new()
avatar.setup(profile, "roam", index + 1,
Vector3(-6.0 + (index % 8) * 1.7, 0.5, 4.0 + (index / 8) * 2.0),
Vector3(0, 0.5, 8.0), Vector3(0, 0.5, 1.2), profile.id)
stage.add_child(avatar)
avatars.append(avatar)
func _process(delta: float) -> bool:
frame += 1
if frame > WARMUP_FRAMES:
sample_ms += delta * 1000.0
if frame >= WARMUP_FRAMES + SAMPLE_FRAMES:
var average := sample_ms / SAMPLE_FRAMES
var meshes := 0
for avatar in avatars:
meshes += avatar.find_children("*", "MeshInstance3D", true, false).size()
print("NPC budget: %d avatars, %d meshes, avg frame %.2f ms (budget %.0f ms)" % [
COUNT, meshes, average, BUDGET_MS])
if average > BUDGET_MS:
printerr("SMOKE FAIL: frame time %.1f ms exceeds the %.0f ms budget" % [
average, BUDGET_MS])
quit(1)
return true
print("SMOKE OK: %d avatars stay inside the frame budget" % COUNT)
quit(0)
return true
return false