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

+85
View File
@@ -6,6 +6,8 @@ extends CanvasLayer
## Emits `applied(settings)` so the game manager can push values into the guard
## and HUD as soon as the player confirms.
const InputBindings := preload("res://src/core/input_bindings.gd")
signal applied(settings: Dictionary)
signal closed()
@@ -14,6 +16,10 @@ var _sensitivity: HSlider = null
var _font_scale: HSlider = null
var _sensitivity_value: Label = null
var _font_scale_value: Label = null
var _invert_y: CheckButton = null
var _head_bob: CheckButton = null
var _bind_buttons := {}
var _listening_action := ""
var settings: Dictionary = {}
func _ready() -> void:
@@ -66,6 +72,37 @@ func _build() -> void:
_font_scale = _add_slider(font_row, 0.75, 1.75, 0.05)
_font_scale.value_changed.connect(_on_font_scale_changed)
_invert_y = CheckButton.new()
_invert_y.text = "Invert vertical look"
_invert_y.toggled.connect(_on_invert_y_toggled)
column.add_child(_invert_y)
_head_bob = CheckButton.new()
_head_bob.text = "Head bob and camera motion"
_head_bob.toggled.connect(_on_head_bob_toggled)
column.add_child(_head_bob)
var controls_title := Label.new()
controls_title.text = "Controls (click a binding, then press a key or pad button)"
controls_title.add_theme_font_size_override("font_size", 13)
controls_title.modulate = Color(0.75, 0.8, 0.9)
column.add_child(controls_title)
var grid := GridContainer.new()
grid.columns = 4
grid.add_theme_constant_override("h_separation", 10)
grid.add_theme_constant_override("v_separation", 6)
column.add_child(grid)
for action in InputBindings.REMAPPABLE:
var label := Label.new()
label.text = str(action).replace("_", " ").capitalize()
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
grid.add_child(label)
var button := Button.new()
button.custom_minimum_size = Vector2(130, 0)
button.pressed.connect(_on_rebind.bind(action))
grid.add_child(button)
_bind_buttons[action] = button
var row := HBoxContainer.new()
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 12)
@@ -112,6 +149,12 @@ func open(current: Dictionary) -> void:
_sensitivity.set_value_no_signal(clampf(sens, _sensitivity.min_value, _sensitivity.max_value))
if _font_scale:
_font_scale.set_value_no_signal(clampf(scale, _font_scale.min_value, _font_scale.max_value))
if _invert_y:
_invert_y.set_pressed_no_signal(bool(settings.get("invert_y", false)))
if _head_bob:
_head_bob.set_pressed_no_signal(bool(settings.get("head_bob", true)))
_listening_action = ""
_refresh_bind_buttons()
_sync_labels()
visible = true
_root.visible = true
@@ -135,6 +178,42 @@ func _on_font_scale_changed(value: float) -> void:
settings["hud_font_scale"] = value
_sync_labels()
func _on_invert_y_toggled(pressed: bool) -> void:
settings["invert_y"] = pressed
func _on_head_bob_toggled(pressed: bool) -> void:
settings["head_bob"] = pressed
func _refresh_bind_buttons() -> void:
var remaps: Dictionary = settings.get("input_bindings", {})
for action in _bind_buttons.keys():
var button: Button = _bind_buttons[action]
var descriptor: Dictionary = remaps.get(action, {})
if descriptor.is_empty():
descriptor = InputBindings.current_descriptor(action)
button.text = InputBindings.label_for(descriptor)
func _on_rebind(action: String) -> void:
_listening_action = action
var button: Button = _bind_buttons.get(action)
if button:
button.text = "Press…"
if button.is_inside_tree():
button.grab_focus()
func _apply_remap(event: InputEvent) -> bool:
if _listening_action == "":
return false
var descriptor := InputBindings.descriptor_from_event(event)
if descriptor.is_empty():
return false
var remaps: Dictionary = settings.get("input_bindings", {})
remaps[_listening_action] = descriptor
settings["input_bindings"] = remaps
_listening_action = ""
_refresh_bind_buttons()
return true
func _on_apply() -> void:
if Save:
Save.save_settings(settings)
@@ -149,6 +228,12 @@ func _on_cancel() -> void:
func _unhandled_input(event: InputEvent) -> void:
if not visible:
return
if _listening_action != "":
var pressed: bool = (event is InputEventKey and event.pressed and not event.echo) \
or (event is InputEventJoypadButton and event.pressed)
if pressed and _apply_remap(event):
get_viewport().set_input_as_handled()
return
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
_on_cancel()
get_viewport().set_input_as_handled()