mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
076d27a564
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.
222 lines
7.9 KiB
GDScript
222 lines
7.9 KiB
GDScript
extends Control
|
|
|
|
# Settings screen root, owning the Video tab and the shared Back button. The
|
|
# Controls tab has its own script (controls_settings.gd) so this file stays
|
|
# video-only; both tabs' state is committed in _on_back_pressed below.
|
|
#
|
|
# Video tab: player-facing video knobs on top of VideoSettings (the
|
|
# autoload holding + persisting them). Preset/AA/vsync/fps-cap/resolution
|
|
# scale apply immediately since they're Viewport- or DisplayServer-wide;
|
|
# glow/brightness/shadow/SDFGI/SSIL/SSAO apply the next time an arena loads
|
|
# an Environment, or instantly to an already-loaded one via
|
|
# VideoSettings.settings_changed (see arena.gd) — task 0.17.
|
|
|
|
const AA_OPTIONS := [
|
|
{"name": "Off", "mode": VideoSettings.AAMode.OFF},
|
|
{"name": "FXAA", "mode": VideoSettings.AAMode.FXAA},
|
|
{"name": "MSAA 2x", "mode": VideoSettings.AAMode.MSAA_2X},
|
|
{"name": "MSAA 4x", "mode": VideoSettings.AAMode.MSAA},
|
|
{"name": "MSAA 4x + FXAA", "mode": VideoSettings.AAMode.MSAA_FXAA},
|
|
]
|
|
|
|
const PRESET_OPTIONS := [
|
|
{"name": "Low", "preset": VideoSettings.Preset.LOW},
|
|
{"name": "Medium", "preset": VideoSettings.Preset.MEDIUM},
|
|
{"name": "High", "preset": VideoSettings.Preset.HIGH},
|
|
{"name": "Custom", "preset": VideoSettings.Preset.CUSTOM},
|
|
]
|
|
|
|
const VSYNC_OPTIONS := [
|
|
{"name": "Disabled", "mode": VideoSettings.VsyncMode.DISABLED},
|
|
{"name": "Enabled", "mode": VideoSettings.VsyncMode.ENABLED},
|
|
{"name": "Adaptive", "mode": VideoSettings.VsyncMode.ADAPTIVE},
|
|
]
|
|
|
|
@onready var preset_dropdown: OptionButton = %PresetDropdown
|
|
@onready var aa_dropdown: OptionButton = %AADropdown
|
|
@onready var glow_slider: HSlider = %GlowSlider
|
|
@onready var glow_value_label: Label = %GlowValueLabel
|
|
@onready var brightness_slider: HSlider = %BrightnessSlider
|
|
@onready var brightness_value_label: Label = %BrightnessValueLabel
|
|
@onready var resolution_slider: HSlider = %ResolutionSlider
|
|
@onready var resolution_value_label: Label = %ResolutionValueLabel
|
|
@onready var vsync_dropdown: OptionButton = %VsyncDropdown
|
|
@onready var fps_cap_dropdown: OptionButton = %FpsCapDropdown
|
|
@onready var fps_readout_label: Label = %FpsReadoutLabel
|
|
|
|
# fps_cap_dropdown item index -> VideoSettings divisor (0 = uncapped). Built
|
|
# in _ready() from the live refresh rate so the menu never hardcodes a
|
|
# specific display's numbers.
|
|
var _fps_cap_divisors: Array[int] = []
|
|
var _populating := false
|
|
|
|
|
|
func _ready() -> void:
|
|
AudioManager.bind_tree_buttons(self)
|
|
# An idle settings screen has no reason to render past the display's own
|
|
# refresh rate; _on_back_pressed only returns to another capped menu, so
|
|
# no uncap is needed there (contrast main_menu.gd's _leave_to_gameplay).
|
|
var refresh_rate := DisplayServer.screen_get_refresh_rate()
|
|
Engine.max_fps = int(refresh_rate) if refresh_rate > 0 else 0
|
|
|
|
_populating = true
|
|
_populate_preset_dropdown()
|
|
_populate_aa_dropdown()
|
|
_populate_vsync_dropdown()
|
|
_populate_fps_cap_dropdown(refresh_rate)
|
|
_populating = false
|
|
|
|
glow_slider.value = VideoSettings.glow_scale
|
|
brightness_slider.value = VideoSettings.brightness
|
|
resolution_slider.value = VideoSettings.resolution_scale
|
|
_update_glow_label()
|
|
_update_brightness_label()
|
|
_update_resolution_label()
|
|
_update_fps_cap_enabled()
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
fps_readout_label.text = "%d fps" % Performance.get_monitor(Performance.TIME_FPS)
|
|
|
|
|
|
func _populate_preset_dropdown() -> void:
|
|
preset_dropdown.clear()
|
|
var selected := 0
|
|
for i in PRESET_OPTIONS.size():
|
|
preset_dropdown.add_item(PRESET_OPTIONS[i]["name"])
|
|
if PRESET_OPTIONS[i]["preset"] == VideoSettings.preset:
|
|
selected = i
|
|
preset_dropdown.select(selected)
|
|
|
|
|
|
func _populate_aa_dropdown() -> void:
|
|
aa_dropdown.clear()
|
|
var selected := 0
|
|
for i in AA_OPTIONS.size():
|
|
aa_dropdown.add_item(AA_OPTIONS[i]["name"])
|
|
if AA_OPTIONS[i]["mode"] == VideoSettings.aa_mode:
|
|
selected = i
|
|
aa_dropdown.select(selected)
|
|
|
|
|
|
func _populate_vsync_dropdown() -> void:
|
|
vsync_dropdown.clear()
|
|
var selected := 0
|
|
for i in VSYNC_OPTIONS.size():
|
|
vsync_dropdown.add_item(VSYNC_OPTIONS[i]["name"])
|
|
if VSYNC_OPTIONS[i]["mode"] == VideoSettings.vsync_mode:
|
|
selected = i
|
|
vsync_dropdown.select(selected)
|
|
|
|
|
|
# Options derive from the live refresh rate rather than a fixed list, per
|
|
# task 0.17's "fps cap derived from screen_get_refresh_rate() divisors".
|
|
# A -1 (or otherwise non-positive) query falls back to "Uncapped" only,
|
|
# rather than presenting cap choices the engine can't compute a value for.
|
|
func _populate_fps_cap_dropdown(refresh_rate: float) -> void:
|
|
fps_cap_dropdown.clear()
|
|
_fps_cap_divisors.clear()
|
|
fps_cap_dropdown.add_item("Uncapped")
|
|
_fps_cap_divisors.append(0)
|
|
if refresh_rate > 0.0:
|
|
for divisor in [1, 2, 3, 4]:
|
|
var hz := refresh_rate / float(divisor)
|
|
fps_cap_dropdown.add_item("%d fps (refresh / %d)" % [roundi(hz), divisor])
|
|
_fps_cap_divisors.append(divisor)
|
|
var selected := _fps_cap_divisors.find(VideoSettings.fps_cap_divisor)
|
|
fps_cap_dropdown.select(maxi(selected, 0))
|
|
|
|
|
|
# The fps cap dropdown only means anything with vsync Disabled — vsync
|
|
# itself already caps to (a multiple of) the refresh rate otherwise.
|
|
func _update_fps_cap_enabled() -> void:
|
|
fps_cap_dropdown.disabled = VideoSettings.vsync_mode != VideoSettings.VsyncMode.DISABLED
|
|
|
|
|
|
func _update_glow_label() -> void:
|
|
glow_value_label.text = "%d%%" % roundi(glow_slider.value * 100.0)
|
|
|
|
|
|
func _update_brightness_label() -> void:
|
|
brightness_value_label.text = "%d%%" % roundi(brightness_slider.value * 100.0)
|
|
|
|
|
|
func _update_resolution_label() -> void:
|
|
resolution_value_label.text = "%d%%" % roundi(resolution_slider.value * 100.0)
|
|
|
|
|
|
func _on_preset_dropdown_item_selected(index: int) -> void:
|
|
VideoSettings.apply_preset(PRESET_OPTIONS[index]["preset"])
|
|
# The bundle may have changed AA/resolution scale underneath the other
|
|
# controls — resync them without re-triggering their own "user changed
|
|
# this by hand" mark_custom() path.
|
|
_populating = true
|
|
_populate_aa_dropdown()
|
|
resolution_slider.value = VideoSettings.resolution_scale
|
|
_populating = false
|
|
_update_resolution_label()
|
|
|
|
|
|
func _on_aa_dropdown_item_selected(index: int) -> void:
|
|
VideoSettings.aa_mode = AA_OPTIONS[index]["mode"]
|
|
VideoSettings.apply_aa()
|
|
_mark_custom_if_user_driven()
|
|
|
|
|
|
func _on_glow_slider_value_changed(value: float) -> void:
|
|
VideoSettings.glow_scale = value
|
|
_update_glow_label()
|
|
|
|
|
|
func _on_brightness_slider_value_changed(value: float) -> void:
|
|
VideoSettings.brightness = value
|
|
_update_brightness_label()
|
|
|
|
|
|
func _on_resolution_slider_value_changed(value: float) -> void:
|
|
VideoSettings.resolution_scale = value
|
|
VideoSettings.apply_resolution_scale()
|
|
_update_resolution_label()
|
|
_mark_custom_if_user_driven()
|
|
|
|
|
|
func _on_vsync_dropdown_item_selected(index: int) -> void:
|
|
VideoSettings.vsync_mode = VSYNC_OPTIONS[index]["mode"]
|
|
VideoSettings.apply_vsync()
|
|
_update_fps_cap_enabled()
|
|
|
|
|
|
func _on_fps_cap_dropdown_item_selected(index: int) -> void:
|
|
VideoSettings.fps_cap_divisor = _fps_cap_divisors[index]
|
|
VideoSettings.apply_fps_cap()
|
|
|
|
|
|
# Preset-gated fields (AA, resolution scale — glow/shadows/SDFGI/SSIL/SSAO
|
|
# have no direct control in this menu yet) flip the preset dropdown to
|
|
# Custom when the player overrides them by hand, so the dropdown never shows
|
|
# a preset name next to settings that preset doesn't actually produce. Not
|
|
# called while _populating (initial load) or while apply_preset() itself is
|
|
# writing these same fields (VideoSettings.mark_custom() no-ops there too,
|
|
# but skipping the redundant dropdown repopulation here is cheap).
|
|
func _mark_custom_if_user_driven() -> void:
|
|
if _populating:
|
|
return
|
|
VideoSettings.mark_custom()
|
|
_populating = true
|
|
_populate_preset_dropdown()
|
|
_populating = false
|
|
|
|
|
|
func _on_back_pressed() -> void:
|
|
VideoSettings.save()
|
|
# Bindings are applied live as the player rebinds them (InputSettings.apply
|
|
# runs on every change) but are only committed to disk here, matching how
|
|
# the video knobs behave.
|
|
InputSettings.save()
|
|
get_tree().change_scene_to_file(ScenePaths.MAIN_MENU)
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event.is_action_pressed("ui_cancel"):
|
|
_on_back_pressed()
|