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

+67
View File
@@ -0,0 +1,67 @@
class_name InputBindings
extends RefCounted
## Key/button remaps persisted through the settings panel. Games keep working
## with the defaults in `project.godot`; only actions the player rebinds are
## rewritten, and remaps never remove mouse or gamepad axis events.
const REMAPPABLE := [
"move_forward", "move_back", "move_left", "move_right",
"walk_slow", "inspect", "wand", "radio_emergency", "action_menu",
]
## Turn an input event into a storable descriptor, or {} when it is not a key or
## pad button (mouse buttons and axes are deliberately not remappable here).
static func descriptor_from_event(event: InputEvent) -> Dictionary:
if event is InputEventKey:
var key := event as InputEventKey
var code := key.physical_keycode if key.physical_keycode != 0 else key.keycode
if code == 0:
return {}
return {"type": "key", "code": int(code)}
if event is InputEventJoypadButton:
return {"type": "joy", "button": int((event as InputEventJoypadButton).button_index)}
return {}
static func label_for(descriptor: Dictionary) -> String:
match str(descriptor.get("type", "")):
"key":
return OS.get_keycode_string(int(descriptor.get("code", 0)))
"joy":
return "Pad %d" % int(descriptor.get("button", 0))
_:
return ""
## First key/button currently bound to the action, used to label the UI.
static func current_descriptor(action: String) -> Dictionary:
if not InputMap.has_action(action):
return {}
for event in InputMap.action_get_events(action):
var descriptor := descriptor_from_event(event)
if not descriptor.is_empty():
return descriptor
return {}
## Rewrite the actions present in `bindings`. Existing key/button events for
## those actions are replaced; mouse and joypad-axis events are left alone.
static func apply(bindings: Dictionary) -> void:
for action in bindings.keys():
if not InputMap.has_action(action):
continue
var descriptor: Dictionary = bindings[action]
if descriptor.is_empty():
continue
for event in InputMap.action_get_events(action):
if event is InputEventKey or event is InputEventJoypadButton:
InputMap.action_erase_event(action, event)
InputMap.action_add_event(action, event_for(descriptor))
static func event_for(descriptor: Dictionary) -> InputEvent:
match str(descriptor.get("type", "")):
"joy":
var button := InputEventJoypadButton.new()
button.button_index = int(descriptor.get("button", 0))
return button
_:
var key := InputEventKey.new()
key.physical_keycode = int(descriptor.get("code", 0))
return key