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

+54
View File
@@ -0,0 +1,54 @@
extends RefCounted
## Headless tests for movement constants, surface lookup, gamepad bindings, and
## the settings that drive the camera feel.
const GuardController := preload("res://src/game/guard_controller.gd")
func test_surface_lookup_covers_the_block() -> bool:
var guard := GuardController.new()
var a := TestUtils.check_eq(guard._surface_at(Vector3(0, 0.2, 2.0)), "stone",
"the entrance steps are stone")
var b := TestUtils.check_eq(guard._surface_at(Vector3(3, 0.2, 8.0)), "concrete",
"the sidewalk is concrete")
var c := TestUtils.check_eq(guard._surface_at(Vector3(2, 0.1, 18.0)), "asphalt",
"the roadway is asphalt")
guard.free()
return a and b and c
func test_step_and_stride_stay_modest() -> bool:
var a := TestUtils.check(GuardController.STEP_HEIGHT <= 0.35,
"step-up stays at kerb height")
var b := TestUtils.check(GuardController.STRIDE_LENGTH >= 0.5
and GuardController.STRIDE_LENGTH <= 1.2, "stride stays human-sized")
var c := TestUtils.check(GuardController.SLOW_WALK_FACTOR < 1.0
and GuardController.SLOW_WALK_FACTOR > 0.3, "precision walk is slower but usable")
return a and b and c
func test_gamepad_bindings_are_added_once() -> bool:
GuardController._ensure_gamepad_bindings()
GuardController._ensure_gamepad_bindings()
var expected := ["look_left", "look_right", "look_up", "look_down",
"move_forward", "move_back", "move_left", "move_right"]
var problems: Array[String] = []
for action in expected:
if not InputMap.has_action(action):
problems.append("%s missing" % action)
continue
var motions := 0
for event in InputMap.action_get_events(action):
if event is InputEventJoypadMotion:
motions += 1
if motions == 0:
problems.append("%s has no joypad axis" % action)
if InputMap.action_get_deadzone(action) > 0.25:
problems.append("%s deadzone too high" % action)
var buttons := {"action_menu": JOY_BUTTON_A, "cancel": JOY_BUTTON_B,
"inspect": JOY_BUTTON_X, "walk_slow": JOY_BUTTON_LEFT_SHOULDER}
for action in buttons.keys():
var found := false
for event in InputMap.action_get_events(action):
if event is InputEventJoypadButton and event.button_index == buttons[action]:
found = true
if not found:
problems.append("%s has no gamepad button" % action)
return TestUtils.check(problems.is_empty(), "gamepad actions bound (%s)" % str(problems))