From cd54242de05632d397d234cd6b9675e842fb7af1 Mon Sep 17 00:00:00 2001 From: Richard Aasa Date: Thu, 10 Sep 2026 17:35:54 +0300 Subject: [PATCH] Make gamepad vibration work on web export Input.start_joy_vibration() is a documented engine no-op on Godot's HTML5 export (never wired to the browser side). Controls.vibrate() wraps it: native platforms go straight through as before, web drives the browser's Gamepad Haptic Actuator API directly via JavaScriptBridge. Gamepad movement/ability input already worked on desktop browsers via the standard Web Gamepad API, so only vibration needed a fix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MXBNaEBmrYDSMcv4NgUWtV --- actors/player/player.gd | 5 ++--- levels/lobby_underground.gd | 3 +-- systems/input/controls_manager.gd | 29 +++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/actors/player/player.gd b/actors/player/player.gd index d863274..a5f63cd 100644 --- a/actors/player/player.gd +++ b/actors/player/player.gd @@ -835,9 +835,8 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void health_changed.emit(_hp, _max_hp) hit_taken.emit(cause) camera_pivot.call(&"trigger_hit", 0.7) - # Vibrate controller 0 with half weak strength, full strong strength, for 0.5 seconds - if DP.b("gamepad_vibration"): - Input.start_joy_vibration(0, 0.5, 1, 0.2) + # Vibrate controller 0 with half weak strength, full strong strength, for 0.2 seconds + Controls.vibrate(0, 0.5, 1, 0.2) # A spear gores a spot — blood spurts from the entry wound (where it struck the # bull). Melee gores pass no position and skip the spurt; gore can be turned off. diff --git a/levels/lobby_underground.gd b/levels/lobby_underground.gd index e7636f4..c816fdb 100644 --- a/levels/lobby_underground.gd +++ b/levels/lobby_underground.gd @@ -11,5 +11,4 @@ func _process(_delta: float) -> void: if Run.bear_gate_open() or DP.b("gate_unlocked_bear"): bear_gate_open = true $AnimationPlayer.play("Gate_opening") - if DP.b("gamepad_vibration"): - Input.start_joy_vibration(0, 0.5, .5, 7) + Controls.vibrate(0, 0.5, .5, 7) diff --git a/systems/input/controls_manager.gd b/systems/input/controls_manager.gd index cf1d413..66c793a 100644 --- a/systems/input/controls_manager.gd +++ b/systems/input/controls_manager.gd @@ -222,6 +222,35 @@ func rigid_skin_enabled() -> bool: return DP.b("use_rigid_skin") +## Rumble a connected gamepad, gated on the DP "gamepad_vibration" flag. 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 +## 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. +func vibrate(device: int, weak_magnitude: float, strong_magnitude: float, duration: float) -> void: + if not DP.b("gamepad_vibration"): + return + if not OS.has_feature("web"): + Input.start_joy_vibration(device, weak_magnitude, strong_magnitude, duration) + return + JavaScriptBridge.eval(""" +(function(){ + try { + var pads = navigator.getGamepads ? navigator.getGamepads() : []; + var pad = pads[%d]; + var act = pad && pad.vibrationActuator; + if (act && act.playEffect) { + act.playEffect('dual-rumble', { + duration: %d, + weakMagnitude: %f, + strongMagnitude: %f + }); + } + } catch (e) {} +})(); +""" % [device, int(duration * 1000.0), weak_magnitude, strong_magnitude], true) + + ## Returns the first keyboard event bound to an action, or null. func get_key_event(action: StringName) -> InputEventKey: for event: InputEvent in InputMap.action_get_events(action):