mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-15 17:42:09 +00:00
feat(input): full controller support, rebindable controls, and rotation fixes
Playing with a gamepad did not work: all six move_* actions had no joypad event at all, so a pad could yaw/pitch/roll/turbo but could not translate. Nothing caught it because every action existed and the game booted fine — no assertion checked that an action is reachable on *both* devices. Controller layout, on the 6DOF convention (left stick aims, right stick translates), using all six of the pad's analog axes for the ship's six degrees of freedom: left stick yaw + pitch right stick strafe + vertical LB / RB roll RT / LT forward / back L3 turbo R3 ball camera Input is now read with Input.get_axis instead of is_action_pressed, so triggers and sticks are proportional. Keyboard values are unchanged. Three rotation bugs found by measuring a real Ship rather than reading the code: - apply_torque() is world-space and the torque was never rotated into the hull's frame (unlike thrust, which uses -ship_basis.z). Roll input became pitch after a 90 degree turn and inverted at 180, so the controls were correct flying up-field and backwards flying back. - ship.tscn's inertia is Vector3(7, 1, 7) but a flat torque was applied to every axis, giving yaw 7x the angular acceleration of pitch and roll (172 deg/s vs 52). Torque is now scaled per-axis by inertia, so rotation_acceleration means rad/s^2 and all three axes match. Yaw is unchanged. - pitch_down pitched the nose UP: get_axis's arguments were reversed, so the I/K keys and the stick each did the opposite of their label. Menus were unusable on a pad for a separate reason: Godot 4.7 gives ui_up/down/left/right joypad events by default but leaves ui_accept and ui_cancel with none (verified against a pristine project), so a controller could move the highlight and never press anything. A confirms and B goes back. Gameplay exits on a new leave_gameplay action (Escape / Start) rather than ui_cancel, so carrying B for menus cannot abandon a live match. Bindings for both devices are rebindable in Settings -> Controls, persisted to user://input.cfg — a separate file from settings.cfg because VideoSettings.save() rewrites that file wholesale and would drop any section it does not know about. project.godot stays the source of truth for defaults; overrides are only ever a delta on top of a boot-time snapshot. Verified: 268 unit tests, the ENet integration gate, and a 16-sample before/after comparison of networked prediction residuals showing the physics change does not regress them (median 0.083m -> 0.065m). Note for follow-up: every policy in Game/bots/ was trained against the old sluggish, world-axis rotation and will over-rotate until retrained.
This commit is contained in:
@@ -71,7 +71,7 @@ func test_physics_engine_is_jolt() -> void:
|
||||
func test_required_autoloads_are_registered() -> void:
|
||||
# NetworkManager in particular is reached by name from many scripts; losing
|
||||
# it from [autoload] fails only at the point of use, deep in a smoke test.
|
||||
for autoload_name in ["GameSettings", "ControlPlaneClient", "VideoSettings", "NetworkManager", "MatchNet", "MatchSim"]:
|
||||
for autoload_name in ["GameSettings", "ControlPlaneClient", "VideoSettings", "InputSettings", "NetworkManager", "MatchNet", "MatchSim"]:
|
||||
assert_true(
|
||||
ProjectSettings.has_setting("autoload/" + autoload_name),
|
||||
"autoload/%s registered" % autoload_name
|
||||
@@ -89,7 +89,12 @@ func test_test_hook_autoloads_are_not_shipped() -> void:
|
||||
# when running those scene-level smoke tests, and must be removed again —
|
||||
# see CLAUDE.md. Shipping one registered would run test code in the real
|
||||
# game, so fail here rather than discovering it in a build.
|
||||
for hook_name in ["MainMenuTestHooks", "LobbyTestHooks", "NetworkedMatchTestHooks"]:
|
||||
# McpInteractionServer is registered automatically by the vendored godot-mcp
|
||||
# tooling whenever it launches the project, and is left behind in
|
||||
# project.godot afterwards. It is a debug channel into a running game, so
|
||||
# shipping it registered is worse than a stray test hook, and it arrives
|
||||
# without anyone having typed it.
|
||||
for hook_name in ["MainMenuTestHooks", "LobbyTestHooks", "NetworkedMatchTestHooks", "McpInteractionServer"]:
|
||||
assert_true(
|
||||
not ProjectSettings.has_setting("autoload/" + hook_name),
|
||||
"test hook autoload/%s must not be registered" % hook_name
|
||||
|
||||
Reference in New Issue
Block a user