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:
2026-09-04 14:07:57 +03:00
parent 01d6ecf475
commit 981ebf1910
32 changed files with 1536 additions and 113 deletions
+105
View File
@@ -0,0 +1,105 @@
extends CanvasLayer
## "TAP TO PLAY" gate for the web/touch build (autoload `WebStartGate`). Browsers only allow
## requestFullscreen / screen.orientation.lock from inside a user gesture, so instead of
## piggybacking the first *gameplay* tap (which used to race the resize/orientation flip and
## strand the joystick — a dropped touchend left the stick stuck), we capture a dedicated tap
## on a full-screen panel BEFORE play. The tap drives Controls.request_fullscreen_landscape()
## and the gate dismisses, so all the fullscreen/orientation churn happens with no in-flight
## touch to lose.
##
## Re-armable, so the player is never stuck windowed: whenever we're NOT fullscreen (boot, or
## after they left via Esc / Android back / edge swipe) the gate re-appears and its tap goes
## back in — fullscreen otherwise persists across menu ↔ game, so tapping Play never lands you
## windowed. Where fullscreen is impossible (iPhone Safari has no requestFullscreen) it shows
## once and then stays out of the way instead of nagging. Only active on web touch; no-op else.
const _POLL: float = 0.4 # seconds between browser fullscreen-state polls
var _panel: ColorRect
var _active: bool = false # web + touch: the only case this gate does anything
var _fs_supported: bool = true
var _dismissed_unsupported: bool = false # iPhone: tapped once, don't show again
var _poll_accum: float = 0.0
func _ready() -> void:
layer = 127 # above HUD/touch UI, just below OrientationGuard (128) so "rotate" wins in portrait
process_mode = Node.PROCESS_MODE_ALWAYS
_active = OS.has_feature("web") and Controls.use_touch_ui()
if not _active:
return
_fs_supported = Controls.fullscreen_supported()
_panel = ColorRect.new()
_panel.color = Color(0.05, 0.03, 0.02, 0.94)
_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
_panel.mouse_filter = Control.MOUSE_FILTER_STOP # swallow the tap from the game behind it
add_child(_panel)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
_panel.add_child(center)
var box := VBoxContainer.new()
box.alignment = BoxContainer.ALIGNMENT_CENTER
box.add_theme_constant_override("separation", 16)
center.add_child(box)
var title := Label.new()
title.text = "TAP TO PLAY"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 52)
title.add_theme_color_override("font_color", Color(1.0, 0.78, 0.22))
box.add_child(title)
var sub := Label.new()
sub.text = "Fullscreen, landscape"
sub.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
sub.add_theme_font_size_override("font_size", 22)
sub.add_theme_color_override("font_color", Color(0.85, 0.85, 0.85))
box.add_child(sub)
func _input(event: InputEvent) -> void:
if not _active or not _panel.visible:
return
var gesture := (event is InputEventScreenTouch and (event as InputEventScreenTouch).pressed) \
or (event is InputEventMouseButton and (event as InputEventMouseButton).pressed)
if gesture:
# The native DOM listener (controls_manager.gd) enters fullscreen synchronously on this
# same tap; this is the belt-and-suspenders explicit call.
Controls.request_fullscreen_landscape()
_panel.visible = false
if not _fs_supported:
_dismissed_unsupported = true # iPhone: don't reappear, there's nothing to enter
get_viewport().set_input_as_handled()
func _process(delta: float) -> void:
if not _active:
return
_poll_accum += delta
if _poll_accum < _POLL:
return
_poll_accum = 0.0
# Portrait: OrientationGuard owns the screen — stand down until they turn the device.
if _is_portrait():
_panel.visible = false
return
if not _fs_supported:
# Can't ever enter fullscreen (iPhone) — offer the tap once, then get out of the way.
_panel.visible = not _dismissed_unsupported
return
# Show the gate whenever we're not fullscreen, and arm so the next tap (re-)enters.
var fs := Controls.is_browser_fullscreen()
_panel.visible = not fs
if not fs:
Controls.arm_fullscreen()
func _is_portrait() -> bool:
var s := get_viewport().get_visible_rect().size
return s.y > s.x