Add blood decals, gore, mobile HUD, web start gate + touch/perf tests
Remove tools/fstest.html scratch page used to probe browser fullscreen/orientation APIs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EznnY8rH2dXhtono1kwsXg
This commit is contained in:
+126
-21
@@ -23,36 +23,115 @@ const REBINDABLE_ACTIONS: PackedStringArray = [
|
||||
|
||||
func _ready() -> void:
|
||||
load_saved()
|
||||
_install_web_fullscreen_hook()
|
||||
|
||||
|
||||
# ── Web fullscreen + landscape lock ─────────────────────────────────────────────
|
||||
# Browsers only allow requestFullscreen / screen.orientation.lock from inside a user
|
||||
# gesture, so we piggyback on the first tap/click of the web build to go fullscreen and
|
||||
# lock to landscape. One-shot; input still flows to the menu/game normally (never consumed).
|
||||
var _fs_triggered: bool = false
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _fs_triggered or not OS.has_feature("web"):
|
||||
# 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
|
||||
# three.js games do. Godot dispatches input from its rAF render loop, one hop removed from the
|
||||
# DOM event, so both Godot's window_set_mode and a JS call made from _input() run outside the
|
||||
# gesture's activation and some mobile browsers reject them. The listener below sidesteps that
|
||||
# entirely, and — left attached — re-enters fullscreen on the next tap if the player leaves it.
|
||||
# All of it is guarded, so iPhone Safari (no requestFullscreen) and desktop just no-op cleanly.
|
||||
func _install_web_fullscreen_hook() -> void:
|
||||
if not OS.has_feature("web"):
|
||||
return
|
||||
var gesture := (event is InputEventScreenTouch and (event as InputEventScreenTouch).pressed) \
|
||||
or (event is InputEventMouseButton and (event as InputEventMouseButton).pressed)
|
||||
if gesture:
|
||||
_fs_triggered = true
|
||||
request_fullscreen_landscape()
|
||||
JavaScriptBridge.eval("""
|
||||
(function(){
|
||||
if (window.__bullFSInit) return;
|
||||
window.__bullFSInit = true;
|
||||
window.__bull_fs = 'idle';
|
||||
// Armed = we should grab fullscreen on the next tap. Starts true so the first tap enters,
|
||||
// then disarms — so once the player is in (or deliberately leaves via Esc / back / swipe)
|
||||
// ordinary gameplay taps never yank them back in. WebStartGate re-arms via arm_fullscreen().
|
||||
window.__bullFSArmed = true;
|
||||
window.__bullFS = function(){
|
||||
try {
|
||||
if (document.fullscreenElement || document.webkitFullscreenElement) {
|
||||
window.__bull_fs = 'ok'; window.__bullFSArmed = false; return;
|
||||
}
|
||||
if (!window.__bullFSArmed) return;
|
||||
var c = document.getElementById('canvas') || document.querySelector('canvas')
|
||||
|| document.documentElement;
|
||||
var rf = c.requestFullscreen || c.webkitRequestFullscreen
|
||||
|| c.msRequestFullscreen || c.mozRequestFullScreen;
|
||||
if (!rf) { window.__bull_fs = 'no-api'; lock(); return; }
|
||||
window.__bull_fs = 'req';
|
||||
var p = rf.call(c);
|
||||
if (p && p.then) {
|
||||
p.then(function(){ window.__bull_fs = 'ok'; window.__bullFSArmed = false; lock(); },
|
||||
function(e){ window.__bull_fs = 'err:' + (e && e.name || e); });
|
||||
} else { window.__bull_fs = 'ok'; window.__bullFSArmed = false; lock(); }
|
||||
} catch(e){ window.__bull_fs = 'ex:' + (e && e.name || e); }
|
||||
function lock(){ try { if (screen.orientation && screen.orientation.lock)
|
||||
screen.orientation.lock('landscape').catch(function(){}); } catch(e){} }
|
||||
};
|
||||
// pointerup / touchend / click grant transient activation (pointerdown/touchstart do not).
|
||||
['pointerup','touchend','click'].forEach(function(ev){
|
||||
window.addEventListener(ev, function(){ window.__bullFS(); }, true);
|
||||
});
|
||||
})();
|
||||
""", true)
|
||||
|
||||
|
||||
## Enter browser fullscreen and lock to landscape. Must be called from a user-gesture
|
||||
## context (first tap, or a button press). No-op off web. Android Chrome/Brave honour the
|
||||
## orientation lock; iOS Safari ignores it (unsupported), so the .catch() swallows the reject.
|
||||
## Ask to enter fullscreen + landscape now (e.g. from WebStartGate's tap). The DOM listener
|
||||
## installed above normally beats this to it on the same gesture; this is the explicit path and
|
||||
## is a safe no-op if we're already fullscreen. No-op off web.
|
||||
func request_fullscreen_landscape() -> void:
|
||||
if not OS.has_feature("web"):
|
||||
return
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||
JavaScriptBridge.eval(
|
||||
"(function(){function l(){try{if(screen.orientation&&screen.orientation.lock)"
|
||||
+ "screen.orientation.lock('landscape').catch(function(){});}catch(e){}}"
|
||||
+ "l();document.addEventListener('fullscreenchange',l,{once:true});})()", true)
|
||||
_install_web_fullscreen_hook()
|
||||
JavaScriptBridge.eval("if (window.__bullFS) window.__bullFS();", true)
|
||||
|
||||
|
||||
## Re-arm the "grab fullscreen on next tap" hook, so the next tap re-enters. WebStartGate calls
|
||||
## this whenever it shows itself (boot, or after the player left fullscreen). No-op off web.
|
||||
func arm_fullscreen() -> void:
|
||||
if OS.has_feature("web"):
|
||||
JavaScriptBridge.eval("window.__bullFSArmed = true;", true)
|
||||
|
||||
|
||||
## Whether the browser can go fullscreen at all. False on iPhone Safari (no requestFullscreen),
|
||||
## which lets WebStartGate stop prompting there instead of nagging forever. True off web (n/a).
|
||||
func fullscreen_supported() -> bool:
|
||||
if not OS.has_feature("web"):
|
||||
return true
|
||||
var r: Variant = JavaScriptBridge.eval(
|
||||
"(function(){var c=document.getElementById('canvas')||document.documentElement;"
|
||||
+ "return !!(c.requestFullscreen||c.webkitRequestFullscreen"
|
||||
+ "||c.msRequestFullscreen||c.mozRequestFullScreen)?1:0;})()", true)
|
||||
if r is bool:
|
||||
return r
|
||||
if r is int or r is float:
|
||||
return int(r) != 0
|
||||
return false
|
||||
|
||||
|
||||
## Is the browser currently fullscreen? No-op-ish off web (returns false).
|
||||
func is_browser_fullscreen() -> bool:
|
||||
if not OS.has_feature("web"):
|
||||
return false
|
||||
var r: Variant = JavaScriptBridge.eval(
|
||||
"(document.fullscreenElement||document.webkitFullscreenElement)?1:0", true)
|
||||
if r is bool:
|
||||
return r
|
||||
if r is int or r is float:
|
||||
return int(r) != 0
|
||||
return false
|
||||
|
||||
|
||||
## One-line web diagnostic for the on-device debug overlay: secure context, cross-origin
|
||||
## isolation (threads), last fullscreen attempt result, and whether we're fullscreen now.
|
||||
func fullscreen_status() -> String:
|
||||
if not OS.has_feature("web"):
|
||||
return "native"
|
||||
var r: Variant = JavaScriptBridge.eval(
|
||||
"'sec=' + (window.isSecureContext ? 1 : 0) + ' iso=' + (window.crossOriginIsolated ? 1 : 0)"
|
||||
+ " + ' fs=' + (window.__bull_fs || '?')"
|
||||
+ " + ' cur=' + ((document.fullscreenElement || document.webkitFullscreenElement) ? 1 : 0)",
|
||||
true)
|
||||
return str(r) if r != null else "?"
|
||||
|
||||
|
||||
## Whether to drive the game with the on-screen touch UI (joystick + buttons)
|
||||
@@ -117,6 +196,32 @@ func debug_overlay() -> bool:
|
||||
return _url_debug == 1
|
||||
|
||||
|
||||
## Whether to apply the rigid-skin workaround (rigid_skin.gd) instead of native GPU skinning.
|
||||
## Defaults to the DP "use_rigid_skin" flag (on for web), but a page URL param overrides it so
|
||||
## native skinning can be A/B'd on the actual phone with no console reachable: append `?noskin=1`
|
||||
## to force it OFF (does the device render skinned meshes natively now — e.g. after a Godot web
|
||||
## template update? — and do the bear's rigid-skin seam holes go away), or `?skin=1` to force ON.
|
||||
## -1 = not read yet, 0 = url forces off, 1 = url forces on, 2 = no url override (use DP flag).
|
||||
var _url_skin: int = -1
|
||||
|
||||
|
||||
func rigid_skin_enabled() -> bool:
|
||||
if _url_skin == -1:
|
||||
_url_skin = 2
|
||||
if OS.has_feature("web"):
|
||||
var q: Variant = JavaScriptBridge.eval("String(window.location.search)", true)
|
||||
var s: String = str(q) if q != null else ""
|
||||
if s.find("noskin") >= 0:
|
||||
_url_skin = 0
|
||||
elif s.find("skin") >= 0:
|
||||
_url_skin = 1
|
||||
if _url_skin == 0:
|
||||
return false
|
||||
if _url_skin == 1:
|
||||
return true
|
||||
return DP.b("use_rigid_skin")
|
||||
|
||||
|
||||
## 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):
|
||||
|
||||
Reference in New Issue
Block a user