Files
richard 431ee295ae Full Xbox/PlayStation controller support: glyphs, remapping, vibration toggle
Extends the earlier Xbox-only ability glyphs into full brand-aware
controller support, following standard practice (position-based
button mapping, safe defaults for unrecognised pads, no per-device
special-casing):

- Controls detects PlayStation pads (DualShock/DualSense) from
  Input.get_joy_name() via a name-substring heuristic, checking for
  "xbox" first so a Bluetooth-connected Xbox pad (reports as "Xbox
  Wireless Controller") can't be misread as PlayStation through the
  generic "wireless controller" hint. Unrecognised/generic pads read
  as Xbox-style, matching the browser's Standard Gamepad Mapping.
- Ability-bar keycaps and the options menu now show ✕/○/□/△ for a
  detected PlayStation pad instead of A/B/X/Y — no index remapping
  needed, since PlayStation's face buttons sit in the exact same
  four positions as Xbox's.
- The options menu's Controls section gains a gamepad rebind button
  next to each ability's keyboard one (movement stays on the D-pad/
  stick, not individually remapped, matching convention). Clicking it
  captures the next joypad button press, mirroring the existing
  keyboard-rebind flow exactly (Esc cancels either). Gamepad rebinds
  persist to controls.cfg alongside keyboard ones and survive a
  reload.
- Added a player-facing "Controller Vibration" toggle (Settings...
  Gameplay section) — Controls.vibrate() now checks it alongside the
  existing DP debug flag, so either can silence rumble.

Verified headlessly end-to-end: default bindings show correct
brand-agnostic glyphs (kick=X, dash=A, ...), rebinding kick to a
different button through the real options-menu flow changes only
that action's glyph, leaves its keyboard binding and other abilities'
gamepad bindings untouched, and survives a reload from disk. Sanity-
checked the PlayStation name heuristic against realistic controller
name strings from Xbox, PlayStation (Windows/macOS/Bluetooth
variants), and other brands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV
2026-09-10 19:33:02 +03:00

131 lines
4.2 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
## Player-facing game options (autoload `Settings`), persisted to user://settings.cfg.
## Distinct from DP (debug tunables) and Controls (keybinds): this is the small set of
## comfort/accessibility toggles the options menu exposes to the player.
signal changed(key: String, value: Variant)
const SAVE_PATH := "user://settings.cfg"
const SECTION := "options"
## Master screen-shake toggle — gates every camera_iso.trigger_hit (hits and ability FX).
var screen_shake: bool = true
## Gore toggle — when off, no blood spawns (impact spurt + ongoing spear-wound bleed).
var gore: bool = true
## Controller rumble toggle — player-facing; Controls.vibrate() also respects the DP
## "gamepad_vibration" debug flag, so either can silence it.
var vibration: bool = true
## Audio volumes stored as linear 0–1. Applied to AudioServer buses on load/change.
var volume_master: float = 1.0
var volume_ambient: float = 1.0
var volume_effects: float = 1.0
var mute_all: bool = false
func _ready() -> void:
_ensure_buses()
load_saved()
## Ensure Ambient and Effects buses exist as children of Master.
func _ensure_buses() -> void:
for bus_name: String in ["Ambient", "Effects"]:
if AudioServer.get_bus_index(bus_name) == -1:
AudioServer.add_bus()
var idx := AudioServer.bus_count - 1
AudioServer.set_bus_name(idx, bus_name)
AudioServer.set_bus_send(idx, &"Master")
func _apply_audio() -> void:
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), _vol_db(volume_master))
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Ambient"), _vol_db(volume_ambient))
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Effects"), _vol_db(volume_effects))
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), mute_all)
func _vol_db(linear: float) -> float:
return linear_to_db(maxf(linear, 0.0001))
func set_screen_shake(value: bool) -> void:
if screen_shake == value:
return
screen_shake = value
changed.emit("screen_shake", value)
save()
func set_gore(value: bool) -> void:
if gore == value:
return
gore = value
changed.emit("gore", value)
save()
func set_vibration(value: bool) -> void:
if vibration == value:
return
vibration = value
changed.emit("vibration", value)
save()
func set_volume_master(value: float) -> void:
volume_master = clampf(value, 0.0, 1.0)
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), _vol_db(volume_master))
changed.emit("volume_master", volume_master)
save()
func set_volume_ambient(value: float) -> void:
volume_ambient = clampf(value, 0.0, 1.0)
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Ambient"), _vol_db(volume_ambient))
changed.emit("volume_ambient", volume_ambient)
save()
func set_volume_effects(value: float) -> void:
volume_effects = clampf(value, 0.0, 1.0)
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Effects"), _vol_db(volume_effects))
changed.emit("volume_effects", volume_effects)
save()
func set_mute_all(value: bool) -> void:
if mute_all == value:
return
mute_all = value
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), mute_all)
changed.emit("mute_all", value)
save()
func save() -> void:
var cfg := ConfigFile.new()
cfg.set_value(SECTION, "screen_shake", screen_shake)
cfg.set_value(SECTION, "gore", gore)
cfg.set_value(SECTION, "vibration", vibration)
cfg.set_value(SECTION, "volume_master", volume_master)
cfg.set_value(SECTION, "volume_ambient", volume_ambient)
cfg.set_value(SECTION, "volume_effects", volume_effects)
cfg.set_value(SECTION, "mute_all", mute_all)
cfg.save(SAVE_PATH)
func load_saved() -> void:
var cfg := ConfigFile.new()
if cfg.load(SAVE_PATH) != OK:
_apply_audio()
return
screen_shake = cfg.get_value(SECTION, "screen_shake", true) as bool
gore = cfg.get_value(SECTION, "gore", true) as bool
vibration = cfg.get_value(SECTION, "vibration", true) as bool
volume_master = cfg.get_value(SECTION, "volume_master", 1.0) as float
volume_ambient = cfg.get_value(SECTION, "volume_ambient", 1.0) as float
volume_effects = cfg.get_value(SECTION, "volume_effects", 1.0) as float
mute_all = cfg.get_value(SECTION, "mute_all", false) as bool
_apply_audio()