Compare commits
9 Commits
1ea4171863
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 02aec4249f | |||
| 431ee295ae | |||
| a34989c49e | |||
| e810d2e75b | |||
| d2a59f695e | |||
| d5c6b749d0 | |||
| 56a4965ff7 | |||
| d8d6b11508 | |||
| a9440c6a43 |
@@ -11,4 +11,4 @@ func _process(_delta: float) -> void:
|
|||||||
if Run.bear_gate_open() or DP.b("gate_unlocked_bear"):
|
if Run.bear_gate_open() or DP.b("gate_unlocked_bear"):
|
||||||
bear_gate_open = true
|
bear_gate_open = true
|
||||||
$AnimationPlayer.play("Gate_opening")
|
$AnimationPlayer.play("Gate_opening")
|
||||||
Controls.vibrate(0, 0.5, .5, 7)
|
Controls.vibrate_for(0, 0.5, .5, 7.0)
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ func _on_matador_killed() -> void:
|
|||||||
if _alive > 0 or _resolved:
|
if _alive > 0 or _resolved:
|
||||||
return
|
return
|
||||||
if _wave >= DP.f("mat_wave_size"):
|
if _wave >= DP.f("mat_wave_size"):
|
||||||
DP.set_value("gate_unlocked_bear", true)
|
|
||||||
_resolved = true
|
_resolved = true
|
||||||
all_defeated.emit()
|
all_defeated.emit()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const SECTION := "options"
|
|||||||
var screen_shake: bool = true
|
var screen_shake: bool = true
|
||||||
## Gore toggle — when off, no blood spawns (impact spurt + ongoing spear-wound bleed).
|
## Gore toggle — when off, no blood spawns (impact spurt + ongoing spear-wound bleed).
|
||||||
var gore: bool = true
|
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.
|
## Audio volumes stored as linear 0–1. Applied to AudioServer buses on load/change.
|
||||||
var volume_master: float = 1.0
|
var volume_master: float = 1.0
|
||||||
@@ -62,6 +65,14 @@ func set_gore(value: bool) -> void:
|
|||||||
save()
|
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:
|
func set_volume_master(value: float) -> void:
|
||||||
volume_master = clampf(value, 0.0, 1.0)
|
volume_master = clampf(value, 0.0, 1.0)
|
||||||
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), _vol_db(volume_master))
|
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), _vol_db(volume_master))
|
||||||
@@ -96,6 +107,7 @@ func save() -> void:
|
|||||||
var cfg := ConfigFile.new()
|
var cfg := ConfigFile.new()
|
||||||
cfg.set_value(SECTION, "screen_shake", screen_shake)
|
cfg.set_value(SECTION, "screen_shake", screen_shake)
|
||||||
cfg.set_value(SECTION, "gore", gore)
|
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_master", volume_master)
|
||||||
cfg.set_value(SECTION, "volume_ambient", volume_ambient)
|
cfg.set_value(SECTION, "volume_ambient", volume_ambient)
|
||||||
cfg.set_value(SECTION, "volume_effects", volume_effects)
|
cfg.set_value(SECTION, "volume_effects", volume_effects)
|
||||||
@@ -110,6 +122,7 @@ func load_saved() -> void:
|
|||||||
return
|
return
|
||||||
screen_shake = cfg.get_value(SECTION, "screen_shake", true) as bool
|
screen_shake = cfg.get_value(SECTION, "screen_shake", true) as bool
|
||||||
gore = cfg.get_value(SECTION, "gore", 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_master = cfg.get_value(SECTION, "volume_master", 1.0) as float
|
||||||
volume_ambient = cfg.get_value(SECTION, "volume_ambient", 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
|
volume_effects = cfg.get_value(SECTION, "volume_effects", 1.0) as float
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ extends Node
|
|||||||
|
|
||||||
const SAVE_PATH := "user://controls.cfg"
|
const SAVE_PATH := "user://controls.cfg"
|
||||||
const SECTION := "keybinds"
|
const SECTION := "keybinds"
|
||||||
|
const JOY_SECTION := "joybinds"
|
||||||
|
|
||||||
## Actions the options menu lets the player rebind (keyboard only).
|
## Actions the options menu lets the player rebind (keyboard only).
|
||||||
const REBINDABLE_ACTIONS: PackedStringArray = [
|
const REBINDABLE_ACTIONS: PackedStringArray = [
|
||||||
@@ -20,12 +21,162 @@ const REBINDABLE_ACTIONS: PackedStringArray = [
|
|||||||
&"ability_roll",
|
&"ability_roll",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
## Actions the options menu also lets the player rebind on a gamepad. Movement stays on the
|
||||||
|
## D-pad/stick (standard, not individually remapped); only the face-button abilities are.
|
||||||
|
const JOY_REBINDABLE_ACTIONS: PackedStringArray = [
|
||||||
|
&"ability_kick",
|
||||||
|
&"ability_dash",
|
||||||
|
&"ability_slam",
|
||||||
|
&"ability_roll",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_ensure_gamepad_confirm_binding()
|
||||||
load_saved()
|
load_saved()
|
||||||
_install_web_fullscreen_hook()
|
_install_web_fullscreen_hook()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Active input device ──────────────────────────────────────────────────────────
|
||||||
|
# Tracks whether the player's last deliberate input was a gamepad or keyboard/mouse, so the
|
||||||
|
# HUD can swap its ability-bar keycaps to match whichever the player is actually holding.
|
||||||
|
# Mouse motion is deliberately ignored — the mouse still drives the camera regardless of
|
||||||
|
# which device is moving the bull, so panning the camera shouldn't flip the hint away from
|
||||||
|
# gamepad glyphs. A small deadzone on joypad axes avoids stick drift/noise doing the same.
|
||||||
|
const _JOY_AXIS_DEADZONE := 0.5
|
||||||
|
var _last_input_is_gamepad: bool = false
|
||||||
|
## Device id of whichever gamepad was pressed most recently — which controller's name we
|
||||||
|
## check to pick Xbox vs PlayStation glyphs when more than one is connected.
|
||||||
|
var _last_joy_device: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventJoypadButton:
|
||||||
|
if event.pressed:
|
||||||
|
_last_input_is_gamepad = true
|
||||||
|
_last_joy_device = event.device
|
||||||
|
elif event is InputEventJoypadMotion:
|
||||||
|
if absf(event.axis_value) > _JOY_AXIS_DEADZONE:
|
||||||
|
_last_input_is_gamepad = true
|
||||||
|
_last_joy_device = event.device
|
||||||
|
elif event is InputEventKey:
|
||||||
|
if event.pressed:
|
||||||
|
_last_input_is_gamepad = false
|
||||||
|
elif event is InputEventMouseButton:
|
||||||
|
if event.pressed:
|
||||||
|
_last_input_is_gamepad = false
|
||||||
|
|
||||||
|
|
||||||
|
## Whether the player's last button/key/stick press was on a gamepad rather than
|
||||||
|
## keyboard/mouse. Drives which glyph set the HUD's ability keycaps show.
|
||||||
|
func using_gamepad() -> bool:
|
||||||
|
return _last_input_is_gamepad
|
||||||
|
|
||||||
|
|
||||||
|
# Godot's joypad button indices are already Xbox-position-normalized — by SDL on native, by
|
||||||
|
# the browser's Standard Gamepad Mapping on web — so a position-based glyph table reads
|
||||||
|
# correctly for any controller brand with no per-device special-casing (a Nintendo pad's
|
||||||
|
# physically-labelled "A" is button 1 here, same as "B" on Xbox; PlayStation's ✕/○/□/△ sit
|
||||||
|
# in the exact same four positions as Xbox's A/B/X/Y, so the same button-index keys apply).
|
||||||
|
const _XBOX_BUTTON_GLYPHS := {
|
||||||
|
JOY_BUTTON_A: "A",
|
||||||
|
JOY_BUTTON_B: "B",
|
||||||
|
JOY_BUTTON_X: "X",
|
||||||
|
JOY_BUTTON_Y: "Y",
|
||||||
|
}
|
||||||
|
const _PLAYSTATION_BUTTON_GLYPHS := {
|
||||||
|
JOY_BUTTON_A: "✕",
|
||||||
|
JOY_BUTTON_B: "○",
|
||||||
|
JOY_BUTTON_X: "□",
|
||||||
|
JOY_BUTTON_Y: "△",
|
||||||
|
}
|
||||||
|
## Substrings (checked lower-case) that identify a controller as a PlayStation pad from
|
||||||
|
## Input.get_joy_name() — which varies a lot by OS/browser/driver, so this is necessarily a
|
||||||
|
## heuristic. "wireless controller" is what macOS/some browsers call a DualShock 4 with no
|
||||||
|
## further detail — but a Bluetooth-connected Xbox pad reports as "Xbox Wireless Controller",
|
||||||
|
## so _is_playstation_pad checks for "xbox" first and bails out before these hints, or that
|
||||||
|
## exact string would misfire as PlayStation. Anything that matches neither reads as
|
||||||
|
## Xbox-style — the safe default, since unrecognised/generic pads and the browser's Standard
|
||||||
|
## Gamepad Mapping are Xbox-shaped.
|
||||||
|
const _PLAYSTATION_NAME_HINTS := [
|
||||||
|
"sony", "playstation", "dualshock", "dualsense", "ps3", "ps4", "ps5", "wireless controller",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
## True if the given gamepad's reported name looks like a PlayStation pad (DualShock/
|
||||||
|
## DualSense), false (Xbox-style) for everything else, including unknown controllers.
|
||||||
|
func _is_playstation_pad(device: int) -> bool:
|
||||||
|
var name := Input.get_joy_name(device).to_lower()
|
||||||
|
if name.contains("xbox"):
|
||||||
|
return false
|
||||||
|
for hint: String in _PLAYSTATION_NAME_HINTS:
|
||||||
|
if name.contains(hint):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the InputEventJoypadButton bound to `action`, or null.
|
||||||
|
func _get_joy_button_event(action: StringName) -> InputEventJoypadButton:
|
||||||
|
for event: InputEvent in InputMap.action_get_events(action):
|
||||||
|
if event is InputEventJoypadButton:
|
||||||
|
return event
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## Face-button glyph for the action's bound joypad button — Xbox letters (A/B/X/Y) or
|
||||||
|
## PlayStation shapes (✕/○/□/△) depending on whichever pad was pressed most recently — or
|
||||||
|
## "—" if the action has no joypad binding.
|
||||||
|
func get_joy_button_label(action: StringName) -> String:
|
||||||
|
var event := _get_joy_button_event(action)
|
||||||
|
if event == null:
|
||||||
|
return "—"
|
||||||
|
var glyphs := _PLAYSTATION_BUTTON_GLYPHS if _is_playstation_pad(_last_joy_device) \
|
||||||
|
else _XBOX_BUTTON_GLYPHS
|
||||||
|
return glyphs.get(event.button_index, "Btn %d" % event.button_index)
|
||||||
|
|
||||||
|
|
||||||
|
## Replaces the joypad button bound to `action`, keeping every other event (keyboard,
|
||||||
|
## alternates) in place.
|
||||||
|
func rebind_joy(action: StringName, event: InputEventJoypadButton) -> void:
|
||||||
|
_rebind_joy_no_save(action, event)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
## Rebuilds the action's event list, swapping the first joypad-button event for `event` in
|
||||||
|
## place so ordering is preserved, same as _rebind_no_save() does for keyboard.
|
||||||
|
func _rebind_joy_no_save(action: StringName, event: InputEventJoypadButton) -> void:
|
||||||
|
var events := InputMap.action_get_events(action)
|
||||||
|
InputMap.action_erase_events(action)
|
||||||
|
var replaced := false
|
||||||
|
for existing: InputEvent in events:
|
||||||
|
if not replaced and existing is InputEventJoypadButton:
|
||||||
|
InputMap.action_add_event(action, event)
|
||||||
|
replaced = true
|
||||||
|
else:
|
||||||
|
InputMap.action_add_event(action, existing)
|
||||||
|
if not replaced:
|
||||||
|
InputMap.action_add_event(action, event)
|
||||||
|
|
||||||
|
|
||||||
|
## Ability-bar keycap label for `action`: the bound Xbox-style face button while a gamepad
|
||||||
|
## is in use, else the bound keyboard key — so the on-screen prompt always matches whichever
|
||||||
|
## the player is actually holding.
|
||||||
|
func ability_key_label(action: StringName) -> String:
|
||||||
|
return get_joy_button_label(action) if using_gamepad() else get_key_label(action)
|
||||||
|
|
||||||
|
|
||||||
|
## Godot's built-in ui_accept (menu/button confirm) ships with zero joypad bindings —
|
||||||
|
## checked directly: only Enter/Kp Enter/Space. ui_left/right/up/down, by contrast, do
|
||||||
|
## default to the D-pad and stick, which is why gamepad focus navigation on a menu works out
|
||||||
|
## of the box while "confirming" the focused button does nothing. Bind joypad button 0 (the
|
||||||
|
## bottom face button in Xbox-style position mapping) as the confirm button.
|
||||||
|
func _ensure_gamepad_confirm_binding() -> void:
|
||||||
|
var alt := InputEventJoypadButton.new()
|
||||||
|
alt.button_index = JOY_BUTTON_A
|
||||||
|
if not InputMap.action_has_event(&"ui_accept", alt):
|
||||||
|
InputMap.action_add_event(&"ui_accept", alt)
|
||||||
|
|
||||||
|
|
||||||
# ── Web fullscreen + landscape lock ─────────────────────────────────────────────
|
# ── Web fullscreen + landscape lock ─────────────────────────────────────────────
|
||||||
# The reliable way to force fullscreen on the web: install a NATIVE DOM listener that calls
|
# The reliable way to force fullscreen on the web: install a NATIVE DOM listener that calls
|
||||||
# requestFullscreen() synchronously inside the real user-gesture event — the same thing
|
# requestFullscreen() synchronously inside the real user-gesture event — the same thing
|
||||||
@@ -222,13 +373,14 @@ func rigid_skin_enabled() -> bool:
|
|||||||
return DP.b("use_rigid_skin")
|
return DP.b("use_rigid_skin")
|
||||||
|
|
||||||
|
|
||||||
## Rumble a connected gamepad, gated on the DP "gamepad_vibration" flag. Off web this is a thin
|
## Rumble a connected gamepad, gated on the player-facing Settings.vibration toggle and the
|
||||||
## wrapper over Input.start_joy_vibration(); on web that call is a documented engine no-op
|
## DP "gamepad_vibration" debug flag (either can silence it). Off web this is a thin wrapper
|
||||||
|
## over Input.start_joy_vibration(); on web that call is a documented engine no-op
|
||||||
## (godotengine/godot#96985 — never wired to the browser side), so there we drive the Gamepad
|
## (godotengine/godot#96985 — never wired to the browser side), so there we drive the Gamepad
|
||||||
## Haptic Actuator API ourselves. Safe no-op wherever it's unsupported (Safari, most Firefox
|
## Haptic Actuator API ourselves. Safe no-op wherever it's unsupported (Safari, most Firefox
|
||||||
## versions, or a controller with no rumble motors) — desktop Chromium/Edge is the target.
|
## versions, or a controller with no rumble motors) — desktop Chromium/Edge is the target.
|
||||||
func vibrate(device: int, weak_magnitude: float, strong_magnitude: float, duration: float) -> void:
|
func vibrate(device: int, weak_magnitude: float, strong_magnitude: float, duration: float) -> void:
|
||||||
if not DP.b("gamepad_vibration"):
|
if not Settings.vibration or not DP.b("gamepad_vibration"):
|
||||||
return
|
return
|
||||||
if not OS.has_feature("web"):
|
if not OS.has_feature("web"):
|
||||||
Input.start_joy_vibration(device, weak_magnitude, strong_magnitude, duration)
|
Input.start_joy_vibration(device, weak_magnitude, strong_magnitude, duration)
|
||||||
@@ -251,8 +403,29 @@ func vibrate(device: int, weak_magnitude: float, strong_magnitude: float, durati
|
|||||||
""" % [device, int(duration * 1000.0), weak_magnitude, strong_magnitude], true)
|
""" % [device, int(duration * 1000.0), weak_magnitude, strong_magnitude], true)
|
||||||
|
|
||||||
|
|
||||||
|
## Godot's own start_joy_vibration() docs warn that a single long effect is unreliable —
|
||||||
|
## "not every hardware is compatible with long effects; you may want to make a shorter
|
||||||
|
## effect and make it repeat instead." That's exactly what broke the lobby gate's 7-second
|
||||||
|
## rumble (the short 0.2 s hit-vibration works fine): this repeats vibrate() as short pulses
|
||||||
|
## to cover the same total duration instead of one long call.
|
||||||
|
## Explicitly stopping between pulses (rather than letting each one time out on its own) and
|
||||||
|
## spacing them out avoids re-triggering the effect faster than some drivers/browsers can
|
||||||
|
## retire the previous one — back-to-back-to-back start calls left the gamepad's rumble
|
||||||
|
## motor stuck reporting "busy" for the rest of the session on at least one setup.
|
||||||
|
func vibrate_for(device: int, weak_magnitude: float, strong_magnitude: float,
|
||||||
|
total_duration: float, pulse: float = 0.25, gap: float = 0.6) -> void:
|
||||||
|
var elapsed := 0.0
|
||||||
|
while elapsed < total_duration:
|
||||||
|
vibrate(device, weak_magnitude, strong_magnitude, pulse)
|
||||||
|
await get_tree().create_timer(pulse).timeout
|
||||||
|
if not OS.has_feature("web"):
|
||||||
|
Input.stop_joy_vibration(device)
|
||||||
|
await get_tree().create_timer(gap).timeout
|
||||||
|
elapsed += pulse + gap
|
||||||
|
|
||||||
|
|
||||||
## Returns the first keyboard event bound to an action, or null.
|
## Returns the first keyboard event bound to an action, or null.
|
||||||
func get_key_event(action: StringName) -> InputEventKey:
|
func _get_key_event(action: StringName) -> InputEventKey:
|
||||||
for event: InputEvent in InputMap.action_get_events(action):
|
for event: InputEvent in InputMap.action_get_events(action):
|
||||||
if event is InputEventKey:
|
if event is InputEventKey:
|
||||||
return event
|
return event
|
||||||
@@ -261,7 +434,7 @@ func get_key_event(action: StringName) -> InputEventKey:
|
|||||||
|
|
||||||
## Human-readable label for the action's current key (e.g. "W", "Space").
|
## Human-readable label for the action's current key (e.g. "W", "Space").
|
||||||
func get_key_label(action: StringName) -> String:
|
func get_key_label(action: StringName) -> String:
|
||||||
var event := get_key_event(action)
|
var event := _get_key_event(action)
|
||||||
if event == null:
|
if event == null:
|
||||||
return "—"
|
return "—"
|
||||||
return event.as_text_physical_keycode()
|
return event.as_text_physical_keycode()
|
||||||
@@ -270,16 +443,20 @@ func get_key_label(action: StringName) -> String:
|
|||||||
## Replaces the primary keyboard event of `action` with `event`, keeping
|
## Replaces the primary keyboard event of `action` with `event`, keeping
|
||||||
## every other event (e.g. arrow-key alternates, joypad) in its place.
|
## every other event (e.g. arrow-key alternates, joypad) in its place.
|
||||||
func rebind(action: StringName, event: InputEventKey) -> void:
|
func rebind(action: StringName, event: InputEventKey) -> void:
|
||||||
rebind_no_save(action, event)
|
_rebind_no_save(action, event)
|
||||||
save()
|
save()
|
||||||
|
|
||||||
|
|
||||||
func save() -> void:
|
func save() -> void:
|
||||||
var cfg := ConfigFile.new()
|
var cfg := ConfigFile.new()
|
||||||
for action: StringName in REBINDABLE_ACTIONS:
|
for action: StringName in REBINDABLE_ACTIONS:
|
||||||
var event := get_key_event(action)
|
var event := _get_key_event(action)
|
||||||
if event != null:
|
if event != null:
|
||||||
cfg.set_value(SECTION, String(action), event.physical_keycode)
|
cfg.set_value(SECTION, String(action), event.physical_keycode)
|
||||||
|
for action: StringName in JOY_REBINDABLE_ACTIONS:
|
||||||
|
var joy_event := _get_joy_button_event(action)
|
||||||
|
if joy_event != null:
|
||||||
|
cfg.set_value(JOY_SECTION, String(action), joy_event.button_index)
|
||||||
cfg.save(SAVE_PATH)
|
cfg.save(SAVE_PATH)
|
||||||
|
|
||||||
|
|
||||||
@@ -294,12 +471,20 @@ func load_saved() -> void:
|
|||||||
var keycode: int = cfg.get_value(SECTION, key)
|
var keycode: int = cfg.get_value(SECTION, key)
|
||||||
var event := InputEventKey.new()
|
var event := InputEventKey.new()
|
||||||
event.physical_keycode = keycode as Key
|
event.physical_keycode = keycode as Key
|
||||||
rebind_no_save(action, event)
|
_rebind_no_save(action, event)
|
||||||
|
for action: StringName in JOY_REBINDABLE_ACTIONS:
|
||||||
|
var joy_key := String(action)
|
||||||
|
if not cfg.has_section_key(JOY_SECTION, joy_key):
|
||||||
|
continue
|
||||||
|
var button_index: int = cfg.get_value(JOY_SECTION, joy_key)
|
||||||
|
var joy_event := InputEventJoypadButton.new()
|
||||||
|
joy_event.button_index = button_index
|
||||||
|
_rebind_joy_no_save(action, joy_event)
|
||||||
|
|
||||||
|
|
||||||
## Rebuilds the action's event list, swapping the first keyboard event for
|
## Rebuilds the action's event list, swapping the first keyboard event for
|
||||||
## `event` in place so ordering (and thus the "primary" key) is preserved.
|
## `event` in place so ordering (and thus the "primary" key) is preserved.
|
||||||
func rebind_no_save(action: StringName, event: InputEventKey) -> void:
|
func _rebind_no_save(action: StringName, event: InputEventKey) -> void:
|
||||||
var events := InputMap.action_get_events(action)
|
var events := InputMap.action_get_events(action)
|
||||||
InputMap.action_erase_events(action)
|
InputMap.action_erase_events(action)
|
||||||
var replaced := false
|
var replaced := false
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ var _hp_seg_fills: Array[ColorRect] = []
|
|||||||
var _bottom_col: VBoxContainer
|
var _bottom_col: VBoxContainer
|
||||||
var _ability_bar: HBoxContainer
|
var _ability_bar: HBoxContainer
|
||||||
var _ability_panels: Array[PanelContainer] = []
|
var _ability_panels: Array[PanelContainer] = []
|
||||||
|
var _ability_key_labels: Array[Label] = []
|
||||||
var _bull_bg: TextureRect
|
var _bull_bg: TextureRect
|
||||||
|
# Cached so the ability keycaps only get rewritten on an actual keyboard<->gamepad switch,
|
||||||
|
# not every frame.
|
||||||
|
var _showing_gamepad_hints: bool = false
|
||||||
|
|
||||||
# Lobby barrel-smash reward — tally the barrels present on load; when the last one bursts,
|
# Lobby barrel-smash reward — tally the barrels present on load; when the last one bursts,
|
||||||
# throw confetti + a "WOooOoW!" and grant the bull two yellow bonus pips. Latched so it
|
# throw confetti + a "WOooOoW!" and grant the bull two yellow bonus pips. Latched so it
|
||||||
@@ -257,6 +261,7 @@ static func _format_time(seconds: float) -> String:
|
|||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
_update_control_hints()
|
_update_control_hints()
|
||||||
|
_update_ability_key_labels()
|
||||||
if _run_timer_label != null:
|
if _run_timer_label != null:
|
||||||
_run_timer_label.visible = Run.run_timer_started() and not _game_over
|
_run_timer_label.visible = Run.run_timer_started() and not _game_over
|
||||||
if _run_timer_label.visible:
|
if _run_timer_label.visible:
|
||||||
@@ -469,6 +474,18 @@ func _update_control_hints() -> void:
|
|||||||
_tab_hint.visible = not _controls_visible
|
_tab_hint.visible = not _controls_visible
|
||||||
|
|
||||||
|
|
||||||
|
# Swaps the ability-bar keycaps between keyboard letters and Xbox-style face-button glyphs
|
||||||
|
# (A/B/X/Y) as the player switches between keyboard and gamepad. Only rewrites the labels on
|
||||||
|
# an actual switch, not every frame — Controls.using_gamepad() itself is cheap either way.
|
||||||
|
func _update_ability_key_labels() -> void:
|
||||||
|
var gamepad := Controls.using_gamepad()
|
||||||
|
if gamepad == _showing_gamepad_hints:
|
||||||
|
return
|
||||||
|
_showing_gamepad_hints = gamepad
|
||||||
|
for i in _ability_key_labels.size():
|
||||||
|
_ability_key_labels[i].text = Controls.ability_key_label(_ABILITY_ACTIONS[i])
|
||||||
|
|
||||||
|
|
||||||
# ── Damage vignette ───────────────────────────────────────────────────────────
|
# ── Damage vignette ───────────────────────────────────────────────────────────
|
||||||
# A full-screen ColorRect driven by damage_vignette.gdshader: transparent centre, red
|
# A full-screen ColorRect driven by damage_vignette.gdshader: transparent centre, red
|
||||||
# toward the edges, alpha ("flash") tweened up on a hit and eased back to nothing. Built
|
# toward the edges, alpha ("flash") tweened up on a hit and eased back to nothing. Built
|
||||||
@@ -590,6 +607,11 @@ func _present_outcome() -> void:
|
|||||||
_over_continue.text = "Play Again (Enter)" if _is_final_boss else "Continue (Enter)"
|
_over_continue.text = "Play Again (Enter)" if _is_final_boss else "Continue (Enter)"
|
||||||
_over_continue.visible = _result_win
|
_over_continue.visible = _result_win
|
||||||
_over_again.visible = not _result_win
|
_over_again.visible = not _result_win
|
||||||
|
# Nothing has focus by default when this screen appears, so a gamepad's ui_accept/ui_left/
|
||||||
|
# ui_right (bound to Godot's built-in ui_* actions) would have nothing to act on — grab it
|
||||||
|
# on the default button, same as main_menu.gd does for its own panels. Geometry-based focus
|
||||||
|
# neighbors (Godot's default) then let ui_left/ui_right reach Main Menu from here.
|
||||||
|
(_over_continue if _result_win else _over_again).grab_focus()
|
||||||
|
|
||||||
var path := _THANKYOU_VIDEO if _is_final_boss else (_WIN_VIDEO if _result_win else _LOSS_VIDEO)
|
var path := _THANKYOU_VIDEO if _is_final_boss else (_WIN_VIDEO if _result_win else _LOSS_VIDEO)
|
||||||
if ResourceLoader.exists(path):
|
if ResourceLoader.exists(path):
|
||||||
@@ -897,7 +919,7 @@ func _build_ability_slot(idx: int) -> Control:
|
|||||||
vbox.add_child(keycap)
|
vbox.add_child(keycap)
|
||||||
|
|
||||||
var key_lbl := Label.new()
|
var key_lbl := Label.new()
|
||||||
key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
|
key_lbl.text = Controls.ability_key_label(_ABILITY_ACTIONS[idx])
|
||||||
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
key_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
key_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
@@ -907,6 +929,7 @@ func _build_ability_slot(idx: int) -> Control:
|
|||||||
key_lbl.add_theme_constant_override("outline_size", 3)
|
key_lbl.add_theme_constant_override("outline_size", 3)
|
||||||
key_lbl.add_theme_font_size_override("font_size", 14)
|
key_lbl.add_theme_font_size_override("font_size", 14)
|
||||||
keycap.add_child(key_lbl)
|
keycap.add_child(key_lbl)
|
||||||
|
_ability_key_labels.append(key_lbl)
|
||||||
|
|
||||||
return vbox
|
return vbox
|
||||||
|
|
||||||
|
|||||||
+83
-9
@@ -80,6 +80,11 @@ const ACTION_LABELS: Dictionary = {
|
|||||||
|
|
||||||
var _listening_action: StringName = &""
|
var _listening_action: StringName = &""
|
||||||
var _listening_button: Button = null
|
var _listening_button: Button = null
|
||||||
|
var _listening_joy_action: StringName = &""
|
||||||
|
var _listening_joy_button: Button = null
|
||||||
|
# Cached so the rebind list only rebuilds on an actual keyboard<->gamepad switch, not
|
||||||
|
# every frame the Options panel is open.
|
||||||
|
var _rebind_rows_show_gamepad: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -184,6 +189,7 @@ func _plaque(hot: bool) -> StyleBoxFlat:
|
|||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
|
_update_rebind_rows_for_device()
|
||||||
if not main_panel.visible or video.stream == null:
|
if not main_panel.visible or video.stream == null:
|
||||||
return
|
return
|
||||||
var frame_count := CAPE_POS.size()
|
var frame_count := CAPE_POS.size()
|
||||||
@@ -207,6 +213,7 @@ func _build_options_toggles() -> void:
|
|||||||
_add_section_label("── Gameplay ──", idx); idx += 1
|
_add_section_label("── Gameplay ──", idx); idx += 1
|
||||||
_add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, idx); idx += 1
|
_add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, idx); idx += 1
|
||||||
_add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, idx); idx += 1
|
_add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, idx); idx += 1
|
||||||
|
_add_toggle_row("Controller Vibration", Settings.vibration, Settings.set_vibration, idx); idx += 1
|
||||||
_add_section_label("── Controls ──", idx)
|
_add_section_label("── Controls ──", idx)
|
||||||
|
|
||||||
|
|
||||||
@@ -291,10 +298,24 @@ func _add_toggle_row(label_text: String, value: bool, setter: Callable, at_index
|
|||||||
options_vbox.move_child(row, at_index)
|
options_vbox.move_child(row, at_index)
|
||||||
|
|
||||||
|
|
||||||
|
## Rebuilds the Controls rebind list for whichever device the player is actually holding
|
||||||
|
## (Controls.using_gamepad(), the same last-input heuristic the HUD's ability keycaps use —
|
||||||
|
## not mere connection presence, so a controller left plugged in but unused doesn't hide
|
||||||
|
## keyboard rebinds from a mouse-and-keyboard player). On a gamepad: movement rows drop
|
||||||
|
## entirely (the D-pad/stick isn't individually remapped, so there's nothing to show), and
|
||||||
|
## each ability shows only its gamepad button. On keyboard: the reverse — WASD-style
|
||||||
|
## keyboard binds only, no gamepad buttons cluttering the list.
|
||||||
func _build_rebind_rows() -> void:
|
func _build_rebind_rows() -> void:
|
||||||
for child in rebind_list.get_children():
|
for child in rebind_list.get_children():
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
var gamepad := Controls.using_gamepad()
|
||||||
|
_rebind_rows_show_gamepad = gamepad
|
||||||
|
_update_rebind_hint(gamepad)
|
||||||
for action: StringName in ACTION_LABELS:
|
for action: StringName in ACTION_LABELS:
|
||||||
|
var is_joy_rebindable := Controls.JOY_REBINDABLE_ACTIONS.has(String(action))
|
||||||
|
if gamepad and not is_joy_rebindable:
|
||||||
|
continue # movement: nothing to show/rebind on a gamepad
|
||||||
|
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
@@ -309,16 +330,45 @@ func _build_rebind_rows() -> void:
|
|||||||
name_label.add_theme_constant_override("outline_size", 5)
|
name_label.add_theme_constant_override("outline_size", 5)
|
||||||
row.add_child(name_label)
|
row.add_child(name_label)
|
||||||
|
|
||||||
var key_button := Button.new()
|
if not gamepad:
|
||||||
key_button.custom_minimum_size = Vector2(160, 0)
|
var key_button := Button.new()
|
||||||
key_button.text = Controls.get_key_label(action)
|
key_button.custom_minimum_size = Vector2(160, 0)
|
||||||
_style_button(key_button, false)
|
key_button.text = Controls.get_key_label(action)
|
||||||
key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button))
|
_style_button(key_button, false)
|
||||||
row.add_child(key_button)
|
key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button))
|
||||||
|
row.add_child(key_button)
|
||||||
|
|
||||||
|
if gamepad and is_joy_rebindable:
|
||||||
|
var joy_button := Button.new()
|
||||||
|
joy_button.custom_minimum_size = Vector2(72, 0)
|
||||||
|
joy_button.text = Controls.get_joy_button_label(action)
|
||||||
|
_style_button(joy_button, false)
|
||||||
|
joy_button.pressed.connect(_on_rebind_joy_pressed.bind(action, joy_button))
|
||||||
|
row.add_child(joy_button)
|
||||||
|
|
||||||
rebind_list.add_child(row)
|
rebind_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_rebind_hint(gamepad: bool) -> void:
|
||||||
|
var hint := get_node_or_null("OptionsPanel/VBox/Hint") as Label
|
||||||
|
if hint != null:
|
||||||
|
hint.text = "Click a button, then press a new one on your controller. Esc cancels." \
|
||||||
|
if gamepad else "Click a key, then press a new key. Esc cancels."
|
||||||
|
|
||||||
|
|
||||||
|
# Live-swaps the rebind list if the player switches device while the Options panel is open
|
||||||
|
# (e.g. they were clicking through with a mouse, then pick up a controller). Skipped while
|
||||||
|
# actively listening for a rebind so an in-progress capture never gets torn out from under
|
||||||
|
# the player.
|
||||||
|
func _update_rebind_rows_for_device() -> void:
|
||||||
|
if not options_panel.visible:
|
||||||
|
return
|
||||||
|
if _listening_action != &"" or _listening_joy_action != &"":
|
||||||
|
return
|
||||||
|
if Controls.using_gamepad() != _rebind_rows_show_gamepad:
|
||||||
|
_build_rebind_rows()
|
||||||
|
|
||||||
|
|
||||||
func _on_play_pressed() -> void:
|
func _on_play_pressed() -> void:
|
||||||
_set_buttons_disabled(true)
|
_set_buttons_disabled(true)
|
||||||
Run.start_new_run() # fresh map for this run; the arena is the intro fight
|
Run.start_new_run() # fresh map for this run; the arena is the intro fight
|
||||||
@@ -369,17 +419,26 @@ func _on_rebind_pressed(action: StringName, button: Button) -> void:
|
|||||||
button.text = "Press a key…"
|
button.text = "Press a key…"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_rebind_joy_pressed(action: StringName, button: Button) -> void:
|
||||||
|
_cancel_listening()
|
||||||
|
_listening_joy_action = action
|
||||||
|
_listening_joy_button = button
|
||||||
|
button.text = "Press…"
|
||||||
|
|
||||||
|
|
||||||
func _cancel_listening() -> void:
|
func _cancel_listening() -> void:
|
||||||
if _listening_button != null:
|
if _listening_button != null:
|
||||||
_listening_button.text = Controls.get_key_label(_listening_action)
|
_listening_button.text = Controls.get_key_label(_listening_action)
|
||||||
|
if _listening_joy_button != null:
|
||||||
|
_listening_joy_button.text = Controls.get_joy_button_label(_listening_joy_action)
|
||||||
_listening_action = &""
|
_listening_action = &""
|
||||||
_listening_button = null
|
_listening_button = null
|
||||||
|
_listening_joy_action = &""
|
||||||
|
_listening_joy_button = null
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if _listening_action == &"":
|
if _listening_action != &"" and event is InputEventKey and event.pressed and not event.echo:
|
||||||
return
|
|
||||||
if event is InputEventKey and event.pressed and not event.echo:
|
|
||||||
if event.physical_keycode == KEY_ESCAPE:
|
if event.physical_keycode == KEY_ESCAPE:
|
||||||
_cancel_listening()
|
_cancel_listening()
|
||||||
else:
|
else:
|
||||||
@@ -390,3 +449,18 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
_listening_action = &""
|
_listening_action = &""
|
||||||
_listening_button = null
|
_listening_button = null
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
|
||||||
|
if _listening_joy_action != &"":
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo \
|
||||||
|
and event.physical_keycode == KEY_ESCAPE:
|
||||||
|
_cancel_listening()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event is InputEventJoypadButton and event.pressed:
|
||||||
|
var new_joy_event := InputEventJoypadButton.new()
|
||||||
|
new_joy_event.button_index = event.button_index
|
||||||
|
Controls.rebind_joy(_listening_joy_action, new_joy_event)
|
||||||
|
_listening_joy_button.text = Controls.get_joy_button_label(_listening_joy_action)
|
||||||
|
_listening_joy_action = &""
|
||||||
|
_listening_joy_button = null
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|||||||
Reference in New Issue
Block a user