Compare commits
2 Commits
3eb9fa50f3
...
45427b7070
| Author | SHA1 | Date | |
|---|---|---|---|
| 45427b7070 | |||
| d514435dd8 |
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://dk6xuwwa0mfpe"
|
uid="uid://dk6xuwwa0mfpe"
|
||||||
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_sand.png"
|
source_file="res://Assets/Arena_sand.png"
|
||||||
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex", "res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://b4gkpa7jypcsm"
|
uid="uid://b4gkpa7jypcsm"
|
||||||
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_wall.png"
|
source_file="res://Assets/Arena_wall.png"
|
||||||
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex", "res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://yercpq3y2o0x"
|
uid="uid://yercpq3y2o0x"
|
||||||
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_wood.png"
|
source_file="res://Assets/Arena_wood.png"
|
||||||
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex", "res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ meshes/generate_lods=true
|
|||||||
meshes/create_shadow_meshes=true
|
meshes/create_shadow_meshes=true
|
||||||
meshes/light_baking=1
|
meshes/light_baking=1
|
||||||
meshes/lightmap_texel_size=0.2
|
meshes/lightmap_texel_size=0.2
|
||||||
meshes/force_disable_compression=false
|
meshes/force_disable_compression=true
|
||||||
skins/use_named_skins=true
|
skins/use_named_skins=true
|
||||||
animation/import=true
|
animation/import=true
|
||||||
animation/fps=30
|
animation/fps=30
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ meshes/generate_lods=true
|
|||||||
meshes/create_shadow_meshes=true
|
meshes/create_shadow_meshes=true
|
||||||
meshes/light_baking=1
|
meshes/light_baking=1
|
||||||
meshes/lightmap_texel_size=0.2
|
meshes/lightmap_texel_size=0.2
|
||||||
meshes/force_disable_compression=false
|
meshes/force_disable_compression=true
|
||||||
skins/use_named_skins=true
|
skins/use_named_skins=true
|
||||||
animation/import=true
|
animation/import=true
|
||||||
animation/fps=30
|
animation/fps=30
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://bxm7c6ldrdiq5"
|
uid="uid://bxm7c6ldrdiq5"
|
||||||
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"
|
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
|
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
|
||||||
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"]
|
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex", "res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://3syhacswepyj"
|
uid="uid://3syhacswepyj"
|
||||||
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"
|
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
|
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
|
||||||
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"]
|
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex", "res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
|
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
|
||||||
|
|
||||||
|
|
||||||
|
# Pinch-to-zoom hook for the touch UI. `ratio` is the two-finger spread this frame over
|
||||||
|
# last frame: >1 (fingers parting) narrows the FOV to zoom in, <1 widens it to pull back,
|
||||||
|
# mirroring the scroll-wheel zoom_in/zoom_out feel.
|
||||||
|
func adjust_zoom(ratio: float) -> void:
|
||||||
|
if ratio <= 0.0:
|
||||||
|
return
|
||||||
|
_user_fov = clampf(_user_fov / ratio, fov_min, fov_max)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
fov = lerpf(fov, _user_fov, delta * 8.0)
|
fov = lerpf(fov, _user_fov, delta * 8.0)
|
||||||
|
|
||||||
|
|||||||
+53
-6
@@ -26,18 +26,65 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
## Whether to drive the game with the on-screen touch UI (joystick + buttons)
|
## Whether to drive the game with the on-screen touch UI (joystick + buttons)
|
||||||
## instead of keyboard hints. DisplayServer.is_touchscreen_available() can't be
|
## instead of keyboard hints. Platform detection is cached (it can't change mid-run)
|
||||||
## used here: `emulate_touch_from_mouse` (on for desktop testing) makes it report
|
## so the per-frame/per-input callers don't repay the cost — but the DP force flag is
|
||||||
## true on every desktop. Gate on the actual runtime platform instead — real
|
## re-read every call so the console can toggle the layout live on a desktop build.
|
||||||
## mobile/native and mobile web browsers — plus the DP force flag for testing.
|
var _touch_ui_cached: int = -1 # -1 = not yet detected, 0 = false, 1 = true
|
||||||
|
|
||||||
|
|
||||||
func use_touch_ui() -> bool:
|
func use_touch_ui() -> bool:
|
||||||
if DP.b("force_touch_controls"):
|
if DP.b("force_touch_controls"):
|
||||||
return true
|
return true
|
||||||
return (
|
if _touch_ui_cached == -1:
|
||||||
|
_touch_ui_cached = 1 if _detect_touch_ui() else 0
|
||||||
|
return _touch_ui_cached == 1
|
||||||
|
|
||||||
|
|
||||||
|
## True only on touch-primary devices (phones, tablets). Detection is deliberately
|
||||||
|
## conservative so a desktop never gets the on-screen thumb UI.
|
||||||
|
##
|
||||||
|
## DisplayServer.is_touchscreen_available() is unusable here on two fronts: on native
|
||||||
|
## desktop `emulate_touch_from_mouse` (on for testing) forces it true, and on the web build
|
||||||
|
## it maps to `'ontouchstart' in window`, which desktop Chromium reports true even with no
|
||||||
|
## touchscreen. So on web we ask the browser two questions instead: does it report touch
|
||||||
|
## points, AND is the *primary* pointer coarse? A desktop-with-mouse (even a touch laptop)
|
||||||
|
## answers no to the second and keeps the desktop HUD; a phone/tablet answers yes to both.
|
||||||
|
## maxTouchPoints also catches iPadOS, which masquerades as desktop Safari.
|
||||||
|
func _detect_touch_ui() -> bool:
|
||||||
|
if (
|
||||||
OS.has_feature("mobile")
|
OS.has_feature("mobile")
|
||||||
or OS.has_feature("web_android")
|
or OS.has_feature("web_android")
|
||||||
or OS.has_feature("web_ios")
|
or OS.has_feature("web_ios")
|
||||||
)
|
):
|
||||||
|
return true
|
||||||
|
if not OS.has_feature("web"):
|
||||||
|
return false
|
||||||
|
var js := "(navigator.maxTouchPoints > 0) && window.matchMedia('(pointer: coarse)').matches"
|
||||||
|
var touch_primary: Variant = JavaScriptBridge.eval(js, true)
|
||||||
|
if touch_primary is bool:
|
||||||
|
return touch_primary
|
||||||
|
if touch_primary is int or touch_primary is float:
|
||||||
|
return int(touch_primary) != 0
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## Whether on-screen debug overlays (touch state + render diagnostics) should show. True on
|
||||||
|
## the DP "touch_debug" flag, or — so it can be switched on from a phone where no console is
|
||||||
|
## reachable — when the page URL carries a `debug` query param (append `?debug=1`).
|
||||||
|
var _url_debug: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
func debug_overlay() -> bool:
|
||||||
|
if DP.b("touch_debug"):
|
||||||
|
return true
|
||||||
|
if _url_debug == -1:
|
||||||
|
_url_debug = 0
|
||||||
|
if OS.has_feature("web"):
|
||||||
|
var has: Variant = JavaScriptBridge.eval(
|
||||||
|
"String(window.location.search).indexOf('debug') >= 0", true)
|
||||||
|
if (has is bool and has) or ((has is int or has is float) and int(has) != 0):
|
||||||
|
_url_debug = 1
|
||||||
|
return _url_debug == 1
|
||||||
|
|
||||||
|
|
||||||
## Returns the first keyboard event bound to an action, or null.
|
## Returns the first keyboard event bound to an action, or null.
|
||||||
|
|||||||
@@ -299,6 +299,9 @@ func _register_all() -> void:
|
|||||||
# Force the on-screen touch controls (joystick + ability buttons) on even on a
|
# Force the on-screen touch controls (joystick + ability buttons) on even on a
|
||||||
# desktop build — otherwise they appear only when a touchscreen is detected.
|
# desktop build — otherwise they appear only when a touchscreen is detected.
|
||||||
_reg_b("Touch", "force_touch_controls", false)
|
_reg_b("Touch", "force_touch_controls", false)
|
||||||
|
# On-screen readout of live touch state (event count, last position, active zone) so
|
||||||
|
# touch problems can be diagnosed on the actual device, where no console is reachable.
|
||||||
|
_reg_b("Touch", "touch_debug", false)
|
||||||
|
|
||||||
|
|
||||||
func _reg_f(section: String, key: String, default: float,
|
func _reg_f(section: String, key: String, default: float,
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ custom_template/release=""
|
|||||||
variant/extensions_support=false
|
variant/extensions_support=false
|
||||||
variant/thread_support=true
|
variant/thread_support=true
|
||||||
vram_texture_compression/for_desktop=true
|
vram_texture_compression/for_desktop=true
|
||||||
vram_texture_compression/for_mobile=false
|
vram_texture_compression/for_mobile=true
|
||||||
html/export_icon=true
|
html/export_icon=true
|
||||||
html/custom_html_shell=""
|
html/custom_html_shell=""
|
||||||
html/head_include=""
|
html/head_include=""
|
||||||
|
|||||||
@@ -119,7 +119,11 @@ func _build_bottom_hud() -> void:
|
|||||||
# Bull-head plate that sits behind the ability + health cluster. It's a sibling of the
|
# Bull-head plate that sits behind the ability + health cluster. It's a sibling of the
|
||||||
# cluster (not a parent) so it never constrains the cluster's layout; we size and place
|
# cluster (not a parent) so it never constrains the cluster's layout; we size and place
|
||||||
# it against the cluster's rect in _position_bull_bg.
|
# it against the cluster's rect in _position_bull_bg.
|
||||||
|
# Skipped on touch/phone: the on-screen thumb cluster already crowds the bottom edge, so
|
||||||
|
# the wide horned plate just adds clutter there.
|
||||||
func _build_bull_hud_bg() -> void:
|
func _build_bull_hud_bg() -> void:
|
||||||
|
if Controls.use_touch_ui():
|
||||||
|
return
|
||||||
_bull_bg = TextureRect.new()
|
_bull_bg = TextureRect.new()
|
||||||
if ResourceLoader.exists(_BULL_BG):
|
if ResourceLoader.exists(_BULL_BG):
|
||||||
_bull_bg.texture = load(_BULL_BG)
|
_bull_bg.texture = load(_BULL_BG)
|
||||||
@@ -240,7 +244,13 @@ func _toggle_controls() -> void:
|
|||||||
# joystick and ability buttons instead. Shares Controls.use_touch_ui() with that
|
# joystick and ability buttons instead. Shares Controls.use_touch_ui() with that
|
||||||
# overlay so exactly one control scheme shows on any given device.
|
# overlay so exactly one control scheme shows on any given device.
|
||||||
func _update_control_hints() -> void:
|
func _update_control_hints() -> void:
|
||||||
if _game_over or Controls.use_touch_ui():
|
var touch := Controls.use_touch_ui()
|
||||||
|
# The desktop tiles carry keyboard keycaps that mean nothing on a phone, and their
|
||||||
|
# STOP-filtered panels would sit over the joystick zone eating touches — touch_controls
|
||||||
|
# shows its own ability buttons instead, so drop the desktop bar on touch devices.
|
||||||
|
if _ability_bar != null:
|
||||||
|
_ability_bar.visible = not touch
|
||||||
|
if _game_over or touch:
|
||||||
_controls_panel.visible = false
|
_controls_panel.visible = false
|
||||||
_tab_hint.visible = false
|
_tab_hint.visible = false
|
||||||
else:
|
else:
|
||||||
|
|||||||
+2
-1
@@ -122,4 +122,5 @@ pointing/emulate_touch_from_mouse=true
|
|||||||
|
|
||||||
rendering_device/driver.windows="d3d12"
|
rendering_device/driver.windows="d3d12"
|
||||||
renderer/rendering_method="gl_compatibility"
|
renderer/rendering_method="gl_compatibility"
|
||||||
anti_aliasing/quality/use_taa=true
|
textures/vram_compression/import_s3tc_bptc=true
|
||||||
|
textures/vram_compression/import_etc2_astc=true
|
||||||
|
|||||||
+360
-190
@@ -2,15 +2,23 @@ extends CanvasLayer
|
|||||||
## On-screen controls for touch devices: a floating movement joystick on the left
|
## On-screen controls for touch devices: a floating movement joystick on the left
|
||||||
## and a cluster of ability buttons on the right. Instantiated by hud.gd.
|
## and a cluster of ability buttons on the right. Instantiated by hud.gd.
|
||||||
##
|
##
|
||||||
## The joystick feeds the same `move_*` input actions the keyboard/gamepad drive
|
## All interaction is driven from raw `_input()` with manual hit-testing, NOT from
|
||||||
## (via Input.action_press with an analog strength), so player.gd needs no touch
|
## Control.gui_input. On the web/wasm build the overlay lives in a CanvasLayer nested
|
||||||
## awareness — it just reads Input.get_vector() as always. Ability buttons call the
|
## under the HUD, and GUI touch routing into a nested CanvasLayer renders fine but
|
||||||
## player's public try_activate_ability(), which shares the keyboard's cooldown gate.
|
## silently drops input — a standard Button here never fires. `_input()` sees every
|
||||||
|
## InputEventScreenTouch regardless of nesting, mouse_filter, or emulate_mouse_from_touch,
|
||||||
|
## so the joystick and buttons work reliably across browsers. Everything is drawn in one
|
||||||
|
## Control (_surface) whose rects are the single source of truth for both drawing and
|
||||||
|
## hit-testing.
|
||||||
|
##
|
||||||
|
## The joystick feeds the same `move_*` input actions the keyboard/gamepad drive (via
|
||||||
|
## Input.action_press with an analog strength), so player.gd needs no touch awareness — it
|
||||||
|
## just reads Input.get_vector() as always. Ability buttons call the player's public
|
||||||
|
## try_activate_ability(), sharing the keyboard's cooldown gate.
|
||||||
##
|
##
|
||||||
## Shown only on touch platforms (see Controls.use_touch_ui), or when DP
|
## Shown only on touch platforms (see Controls.use_touch_ui), or when DP
|
||||||
## "force_touch_controls" is on so the layout can be exercised on a desktop build.
|
## "force_touch_controls" is on so the layout can be exercised on a desktop build
|
||||||
## emulate_touch_from_mouse (project.godot) lets a mouse stand in for a finger while
|
## (emulate_touch_from_mouse turns a mouse click into a touch event for that).
|
||||||
## testing.
|
|
||||||
|
|
||||||
# move_* actions, indexed by the four joystick push directions.
|
# move_* actions, indexed by the four joystick push directions.
|
||||||
const _MOVE_RIGHT: StringName = &"move_right"
|
const _MOVE_RIGHT: StringName = &"move_right"
|
||||||
@@ -33,80 +41,208 @@ const _ABILITY_ACCENT: Array[Color] = [
|
|||||||
Color(1.00, 0.82, 0.32), # Roll — gold
|
Color(1.00, 0.82, 0.32), # Roll — gold
|
||||||
]
|
]
|
||||||
|
|
||||||
var _root: Control
|
# Sentinel touch indices. A real finger uses its own non-negative index; the mouse
|
||||||
var _stick: Control
|
# (desktop force-flag testing, when no real touch has been seen) uses _MOUSE.
|
||||||
var _cd_overlays: Array[ColorRect] = []
|
const _MOUSE: int = -2
|
||||||
|
|
||||||
|
var _surface: Control
|
||||||
var _player: Node = null
|
var _player: Node = null
|
||||||
|
|
||||||
# Floating-joystick state. _origin is where the thumb first touched (base centre),
|
# Cached textures + styleboxes so the per-frame redraw (cooldowns animate) allocates nothing.
|
||||||
# _knob is the current thumb position; both in _stick-local coordinates. _touch_index
|
var _ability_tex: Array[Texture2D] = []
|
||||||
# is the finger owning the stick (-1 = none, -2 = mouse), so a second finger tapping
|
var _ability_style: Array[StyleBoxFlat] = []
|
||||||
# an ability button never steals the stick.
|
var _menu_style: StyleBoxFlat
|
||||||
|
|
||||||
|
# Layout rects in screen space — the single source of truth for drawing AND hit-testing.
|
||||||
|
var _stick_zone: Rect2 = Rect2()
|
||||||
|
var _ability_rects: Array[Rect2] = []
|
||||||
|
var _menu_rect: Rect2 = Rect2()
|
||||||
|
|
||||||
|
# Resting "drag to move" affordance drawn in the lower-left when the stick is idle, so a
|
||||||
|
# first-time player sees where to plant their thumb instead of guessing. _hint_pulse gives
|
||||||
|
# it a slow breathing alpha for discoverability.
|
||||||
|
var _stick_hint_center: Vector2 = Vector2()
|
||||||
|
var _hint_pulse: float = 0.0
|
||||||
|
|
||||||
|
# Floating-joystick state. _origin is where the thumb first touched (base centre), _knob is
|
||||||
|
# the current thumb position; both in screen coordinates.
|
||||||
var _stick_active: bool = false
|
var _stick_active: bool = false
|
||||||
var _origin: Vector2 = Vector2.ZERO
|
var _origin: Vector2 = Vector2.ZERO
|
||||||
var _knob: Vector2 = Vector2.ZERO
|
var _knob: Vector2 = Vector2.ZERO
|
||||||
var _touch_index: int = -3
|
|
||||||
var _stick_radius: float = 110.0
|
var _stick_radius: float = 110.0
|
||||||
|
|
||||||
|
# Live fingers by index → screen position, plus what each finger is doing ("stick", a slot
|
||||||
|
# int, "menu", or "camera"). Ownership keeps a second finger on a button from stealing the
|
||||||
|
# stick, and keeps steering/ability fingers from being mistaken for a pinch gesture.
|
||||||
|
var _touches: Dictionary = {}
|
||||||
|
var _touch_owner: Dictionary = {}
|
||||||
|
var _saw_real_touch: bool = false
|
||||||
|
|
||||||
|
# Pinch-to-zoom: only "camera" fingers (right side, not on any button) count, so steering
|
||||||
|
# with one thumb while tapping an ability with the other never reads as a zoom.
|
||||||
|
var _pinching: bool = false
|
||||||
|
var _pinch_dist: float = 0.0
|
||||||
|
|
||||||
|
# Diagnostics surfaced by DP "touch_debug" — lets us see, on the actual device, whether
|
||||||
|
# events arrive and where, since no console is reachable there.
|
||||||
|
var _dbg_events: int = 0
|
||||||
|
var _dbg_last_pos: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
layer = 10 # below the HUD (11) so the game-over screen always draws on top
|
layer = 10 # below the HUD (11) so the game-over screen always draws on top
|
||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
|
||||||
_root = Control.new()
|
for slot: int in 4:
|
||||||
_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
var tex: Texture2D = null
|
||||||
_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
if ResourceLoader.exists(_ABILITY_ICONS[slot]):
|
||||||
_root.visible = false
|
tex = load(_ABILITY_ICONS[slot])
|
||||||
add_child(_root)
|
_ability_tex.append(tex)
|
||||||
|
_ability_style.append(_button_style(_ABILITY_ACCENT[slot]))
|
||||||
|
_menu_style = _button_style(Color(1.00, 0.78, 0.22, 0.8))
|
||||||
|
|
||||||
|
_surface = Control.new()
|
||||||
|
_surface.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_surface.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_surface.visible = false
|
||||||
|
_surface.draw.connect(_draw_surface)
|
||||||
|
add_child(_surface)
|
||||||
|
|
||||||
_build_joystick()
|
|
||||||
_build_ability_buttons()
|
|
||||||
_build_menu_button()
|
|
||||||
_relayout()
|
_relayout()
|
||||||
get_viewport().size_changed.connect(_relayout)
|
get_viewport().size_changed.connect(_relayout)
|
||||||
|
|
||||||
|
|
||||||
# ── Joystick ────────────────────────────────────────────────────────────────
|
# ── Layout ────────────────────────────────────────────────────────────────────
|
||||||
# The stick zone is the whole left half of the screen: touching anywhere in it
|
# Left half is the joystick zone (touch anywhere in it to plant the base under the thumb);
|
||||||
# plants the base under the thumb, so the player never has to find a fixed pad.
|
# the ability cluster and menu button sit in the right half so they never overlap it.
|
||||||
|
|
||||||
func _build_joystick() -> void:
|
func _relayout() -> void:
|
||||||
_stick = Control.new()
|
var vp := get_viewport().get_visible_rect().size
|
||||||
_stick.mouse_filter = Control.MOUSE_FILTER_STOP
|
var short := minf(vp.x, vp.y)
|
||||||
_stick.gui_input.connect(_on_stick_input)
|
_stick_radius = clampf(short * 0.16, 80.0, 150.0)
|
||||||
_stick.draw.connect(_draw_stick)
|
_stick_zone = Rect2(Vector2.ZERO, Vector2(vp.x * 0.5, vp.y))
|
||||||
_root.add_child(_stick)
|
|
||||||
|
var bs := clampf(short * 0.16, 72.0, 120.0) # ability button edge
|
||||||
|
var gap := bs * 0.18
|
||||||
|
var margin := bs * 0.28
|
||||||
|
var right := vp.x - margin
|
||||||
|
var bottom := vp.y - margin
|
||||||
|
var col0 := right - bs * 2.0 - gap
|
||||||
|
var col1 := right - bs
|
||||||
|
var row0 := bottom - bs * 2.0 - gap
|
||||||
|
var row1 := bottom - bs
|
||||||
|
_ability_rects = [
|
||||||
|
Rect2(col0, row0, bs, bs), # kick
|
||||||
|
Rect2(col1, row0, bs, bs), # dash
|
||||||
|
Rect2(col0, row1, bs, bs), # slam
|
||||||
|
Rect2(col1, row1, bs, bs), # roll
|
||||||
|
]
|
||||||
|
|
||||||
|
var ms := clampf(short * 0.09, 48.0, 72.0)
|
||||||
|
_menu_rect = Rect2(vp.x - ms - 16.0, 16.0, ms, ms)
|
||||||
|
|
||||||
|
_stick_hint_center = Vector2(vp.x * 0.20, vp.y * 0.74)
|
||||||
|
_surface.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _on_stick_input(event: InputEvent) -> void:
|
# ── Input ───────────────────────────────────────────────────────────────────
|
||||||
|
# Raw events only. Real touches drive the touch path; once any real touch is seen the
|
||||||
|
# mouse path is disabled (emulate_mouse_from_touch would otherwise double-fire every tap).
|
||||||
|
# The mouse path stays live for desktop force-flag testing where no touchscreen exists.
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if get_tree().paused or not Controls.use_touch_ui():
|
||||||
|
return
|
||||||
|
|
||||||
if event is InputEventScreenTouch:
|
if event is InputEventScreenTouch:
|
||||||
|
_saw_real_touch = true
|
||||||
var t := event as InputEventScreenTouch
|
var t := event as InputEventScreenTouch
|
||||||
if t.pressed and not _stick_active:
|
_dbg_events += 1
|
||||||
_begin_stick(t.position, t.index)
|
_dbg_last_pos = t.position
|
||||||
elif not t.pressed and _touch_index == t.index:
|
if t.pressed:
|
||||||
_end_stick()
|
_touches[t.index] = t.position
|
||||||
|
_on_press(t.index, t.position)
|
||||||
|
else:
|
||||||
|
_on_release(t.index, t.position)
|
||||||
|
_touches.erase(t.index)
|
||||||
|
_update_pinch_state()
|
||||||
|
_surface.queue_redraw()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
elif event is InputEventScreenDrag:
|
elif event is InputEventScreenDrag:
|
||||||
var d := event as InputEventScreenDrag
|
var d := event as InputEventScreenDrag
|
||||||
if _touch_index == d.index:
|
_dbg_events += 1
|
||||||
_update_stick(d.position)
|
_dbg_last_pos = d.position
|
||||||
elif event is InputEventMouseButton:
|
_touches[d.index] = d.position
|
||||||
|
_on_drag(d.index, d.position)
|
||||||
|
_surface.queue_redraw()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
elif not _saw_real_touch and event is InputEventMouseButton:
|
||||||
var mb := event as InputEventMouseButton
|
var mb := event as InputEventMouseButton
|
||||||
if mb.button_index == MOUSE_BUTTON_LEFT:
|
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||||
if mb.pressed and not _stick_active:
|
_dbg_events += 1
|
||||||
_begin_stick(mb.position, -2)
|
_dbg_last_pos = mb.position
|
||||||
elif not mb.pressed and _touch_index == -2:
|
if mb.pressed:
|
||||||
_end_stick()
|
_touches[_MOUSE] = mb.position
|
||||||
elif event is InputEventMouseMotion and _touch_index == -2:
|
_on_press(_MOUSE, mb.position)
|
||||||
_update_stick((event as InputEventMouseMotion).position)
|
else:
|
||||||
|
_on_release(_MOUSE, mb.position)
|
||||||
|
_touches.erase(_MOUSE)
|
||||||
|
_update_pinch_state()
|
||||||
|
_surface.queue_redraw()
|
||||||
|
|
||||||
|
elif not _saw_real_touch and event is InputEventMouseMotion:
|
||||||
|
if _touch_owner.get(_MOUSE, null) == "stick":
|
||||||
|
_touches[_MOUSE] = (event as InputEventMouseMotion).position
|
||||||
|
_update_stick(_touches[_MOUSE])
|
||||||
|
_surface.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _begin_stick(pos: Vector2, index: int) -> void:
|
func _on_press(index: int, pos: Vector2) -> void:
|
||||||
|
if _menu_rect.has_point(pos):
|
||||||
|
_touch_owner[index] = "menu"
|
||||||
|
return
|
||||||
|
for slot: int in _ability_rects.size():
|
||||||
|
if _ability_rects[slot].has_point(pos):
|
||||||
|
_touch_owner[index] = slot
|
||||||
|
if _player != null:
|
||||||
|
_player.call(&"try_activate_ability", slot)
|
||||||
|
return
|
||||||
|
if _stick_zone.has_point(pos) and not _stick_active:
|
||||||
|
_touch_owner[index] = "stick"
|
||||||
|
_begin_stick(pos)
|
||||||
|
return
|
||||||
|
_touch_owner[index] = "camera"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag(index: int, pos: Vector2) -> void:
|
||||||
|
match _touch_owner.get(index, null):
|
||||||
|
"stick":
|
||||||
|
_update_stick(pos)
|
||||||
|
"camera":
|
||||||
|
if _pinching:
|
||||||
|
_apply_pinch()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_release(index: int, pos: Vector2) -> void:
|
||||||
|
match _touch_owner.get(index, null):
|
||||||
|
"stick":
|
||||||
|
_end_stick()
|
||||||
|
"menu":
|
||||||
|
if _menu_rect.has_point(pos):
|
||||||
|
_touch_owner.erase(index)
|
||||||
|
_return_to_menu()
|
||||||
|
return
|
||||||
|
_touch_owner.erase(index)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Joystick ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _begin_stick(pos: Vector2) -> void:
|
||||||
_stick_active = true
|
_stick_active = true
|
||||||
_touch_index = index
|
|
||||||
_origin = pos
|
_origin = pos
|
||||||
_knob = pos
|
_knob = pos
|
||||||
_stick.queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func _update_stick(pos: Vector2) -> void:
|
func _update_stick(pos: Vector2) -> void:
|
||||||
@@ -117,19 +253,16 @@ func _update_stick(pos: Vector2) -> void:
|
|||||||
offset = offset.normalized() * _stick_radius
|
offset = offset.normalized() * _stick_radius
|
||||||
_knob = _origin + offset
|
_knob = _origin + offset
|
||||||
_apply_move(offset / _stick_radius)
|
_apply_move(offset / _stick_radius)
|
||||||
_stick.queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
func _end_stick() -> void:
|
func _end_stick() -> void:
|
||||||
_stick_active = false
|
_stick_active = false
|
||||||
_touch_index = -3
|
|
||||||
_release_move()
|
_release_move()
|
||||||
_stick.queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
# Translate a normalised stick offset (screen space: +x right, +y down) into analog
|
# Translate a normalised stick offset (screen space: +x right, +y down) into analog
|
||||||
# strengths on the four movement actions. Opposite pairs are mutually exclusive, so
|
# strengths on the four movement actions. Opposite pairs are mutually exclusive, so only
|
||||||
# only one of each axis is ever pressed at a time.
|
# one of each axis is ever pressed at a time.
|
||||||
func _apply_move(v: Vector2) -> void:
|
func _apply_move(v: Vector2) -> void:
|
||||||
Input.action_press(_MOVE_RIGHT, maxf(0.0, v.x))
|
Input.action_press(_MOVE_RIGHT, maxf(0.0, v.x))
|
||||||
Input.action_press(_MOVE_LEFT, maxf(0.0, -v.x))
|
Input.action_press(_MOVE_LEFT, maxf(0.0, -v.x))
|
||||||
@@ -142,139 +275,184 @@ func _release_move() -> void:
|
|||||||
Input.action_release(action)
|
Input.action_release(action)
|
||||||
|
|
||||||
|
|
||||||
func _draw_stick() -> void:
|
# ── Pinch-to-zoom ─────────────────────────────────────────────────────────────
|
||||||
if not _stick_active:
|
# Only "camera" fingers (open right-side area, not on a button) participate, so it can't
|
||||||
|
# fire while the player is steering or tapping abilities.
|
||||||
|
|
||||||
|
func _camera_touches() -> Array:
|
||||||
|
var pts: Array = []
|
||||||
|
for idx: int in _touches:
|
||||||
|
if _touch_owner.get(idx, null) == "camera":
|
||||||
|
pts.append(_touches[idx])
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
func _update_pinch_state() -> void:
|
||||||
|
var pts := _camera_touches()
|
||||||
|
var was := _pinching
|
||||||
|
_pinching = pts.size() >= 2
|
||||||
|
if _pinching and not was:
|
||||||
|
if _stick_active:
|
||||||
|
_end_stick()
|
||||||
|
_pinch_dist = (pts[0] as Vector2).distance_to(pts[1] as Vector2)
|
||||||
|
elif was and not _pinching:
|
||||||
|
_pinch_dist = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_pinch() -> void:
|
||||||
|
var pts := _camera_touches()
|
||||||
|
if pts.size() < 2:
|
||||||
return
|
return
|
||||||
var ring := Color(0.93, 0.88, 0.72, 0.30)
|
var dist := (pts[0] as Vector2).distance_to(pts[1] as Vector2)
|
||||||
var knob := Color(1.00, 0.82, 0.32, 0.55)
|
if _pinch_dist <= 0.0 or dist <= 0.0:
|
||||||
_stick.draw_circle(_origin, _stick_radius, Color(0.0, 0.0, 0.0, 0.20))
|
_pinch_dist = dist
|
||||||
_stick.draw_arc(_origin, _stick_radius, 0.0, TAU, 48, ring, 4.0, true)
|
return
|
||||||
_stick.draw_circle(_knob, _stick_radius * 0.42, knob)
|
var cam := get_viewport().get_camera_3d()
|
||||||
|
if cam != null and cam.has_method(&"adjust_zoom"):
|
||||||
|
cam.call(&"adjust_zoom", dist / _pinch_dist)
|
||||||
|
_pinch_dist = dist
|
||||||
|
|
||||||
|
|
||||||
# ── Ability buttons ─────────────────────────────────────────────────────────
|
# ── Drawing ───────────────────────────────────────────────────────────────────
|
||||||
# A 2×2 cluster in the bottom-right thumb reach. Each button taps the matching
|
|
||||||
# ability; a dark overlay wipes down over it while that ability is cooling down.
|
|
||||||
|
|
||||||
func _build_ability_buttons() -> void:
|
func _draw_surface() -> void:
|
||||||
var grid := GridContainer.new()
|
for slot: int in _ability_rects.size():
|
||||||
grid.name = "AbilityGrid"
|
var r: Rect2 = _ability_rects[slot]
|
||||||
grid.columns = 2
|
_surface.draw_style_box(_ability_style[slot], r)
|
||||||
grid.add_theme_constant_override("h_separation", 14)
|
var inner := r.grow(-r.size.x * 0.14)
|
||||||
grid.add_theme_constant_override("v_separation", 14)
|
if _ability_tex[slot] != null:
|
||||||
grid.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
_draw_icon_fit(_ability_tex[slot], inner)
|
||||||
grid.anchor_left = 1.0
|
else:
|
||||||
grid.anchor_right = 1.0
|
_draw_centered_text(_ABILITY_LETTERS[slot], r, 40, _ABILITY_ACCENT[slot])
|
||||||
grid.anchor_top = 1.0
|
var frac := 0.0
|
||||||
grid.anchor_bottom = 1.0
|
if _player != null:
|
||||||
grid.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
frac = clampf(_player.call(&"ability_cooldown_fraction", slot), 0.0, 1.0)
|
||||||
grid.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
if frac > 0.0:
|
||||||
grid.offset_left = -220.0
|
var ch := r.size.y * frac
|
||||||
grid.offset_top = -220.0
|
_surface.draw_rect(
|
||||||
grid.offset_right = -24.0
|
Rect2(r.position.x, r.position.y + r.size.y - ch, r.size.x, ch),
|
||||||
grid.offset_bottom = -24.0
|
Color(0.0, 0.0, 0.0, 0.55))
|
||||||
_root.add_child(grid)
|
|
||||||
|
|
||||||
for slot: int in 4:
|
_surface.draw_style_box(_menu_style, _menu_rect)
|
||||||
grid.add_child(_build_ability_button(slot))
|
_draw_hamburger(_menu_rect)
|
||||||
|
|
||||||
|
if _stick_active:
|
||||||
|
_surface.draw_circle(_origin, _stick_radius, Color(0.0, 0.0, 0.0, 0.20))
|
||||||
|
_surface.draw_arc(
|
||||||
|
_origin, _stick_radius, 0.0, TAU, 48, Color(0.93, 0.88, 0.72, 0.30), 4.0, true)
|
||||||
|
_surface.draw_circle(_knob, _stick_radius * 0.42, Color(1.00, 0.82, 0.32, 0.55))
|
||||||
|
elif not _pinching:
|
||||||
|
_draw_stick_hint()
|
||||||
|
|
||||||
|
if Controls.debug_overlay():
|
||||||
|
_draw_debug()
|
||||||
|
|
||||||
|
|
||||||
func _build_ability_button(slot: int) -> Control:
|
# A faint "ghost" joystick at rest in the lower-left with a caption, so the movement zone
|
||||||
var panel := PanelContainer.new()
|
# is discoverable. Breathes gently via _hint_pulse; vanishes the moment the stick is grabbed.
|
||||||
panel.custom_minimum_size = Vector2(96.0, 96.0)
|
func _draw_stick_hint() -> void:
|
||||||
panel.add_theme_stylebox_override("panel", _button_style(slot))
|
var a := 0.16 + 0.12 * (0.5 + 0.5 * sin(_hint_pulse))
|
||||||
panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
var c := _stick_hint_center
|
||||||
|
var r := _stick_radius * 0.72
|
||||||
var stack := Control.new()
|
_surface.draw_circle(c, r, Color(0.0, 0.0, 0.0, a * 0.45))
|
||||||
stack.set_anchors_preset(Control.PRESET_FULL_RECT)
|
_surface.draw_arc(c, r, 0.0, TAU, 40, Color(0.93, 0.88, 0.72, a + 0.14), 3.0, true)
|
||||||
panel.add_child(stack)
|
_surface.draw_circle(c, r * 0.32, Color(1.00, 0.82, 0.32, a + 0.16))
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
if ResourceLoader.exists(_ABILITY_ICONS[slot]):
|
if font != null:
|
||||||
var icon := TextureRect.new()
|
var txt := "DRAG TO MOVE"
|
||||||
icon.texture = load(_ABILITY_ICONS[slot])
|
var fs := 18
|
||||||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
var tw := font.get_string_size(txt, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
|
||||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
_surface.draw_string(
|
||||||
icon.set_anchors_preset(Control.PRESET_FULL_RECT)
|
font, c + Vector2(-tw.x * 0.5, r + 28.0), txt, HORIZONTAL_ALIGNMENT_LEFT, -1, fs,
|
||||||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
Color(0.93, 0.88, 0.72, a + 0.30))
|
||||||
stack.add_child(icon)
|
|
||||||
else:
|
|
||||||
var lbl := Label.new()
|
|
||||||
lbl.text = _ABILITY_LETTERS[slot]
|
|
||||||
lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
||||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
||||||
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
||||||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
lbl.add_theme_color_override("font_color", _ABILITY_ACCENT[slot])
|
|
||||||
lbl.add_theme_font_size_override("font_size", 40)
|
|
||||||
stack.add_child(lbl)
|
|
||||||
|
|
||||||
# Cooldown wipe — anchored to the bottom, its top edge driven in _process so it
|
|
||||||
# shrinks away as the ability comes off cooldown (mirrors the desktop HUD tiles).
|
|
||||||
var cd := ColorRect.new()
|
|
||||||
cd.color = Color(0.0, 0.0, 0.0, 0.62)
|
|
||||||
cd.anchor_left = 0.0
|
|
||||||
cd.anchor_right = 1.0
|
|
||||||
cd.anchor_top = 1.0
|
|
||||||
cd.anchor_bottom = 1.0
|
|
||||||
cd.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
stack.add_child(cd)
|
|
||||||
_cd_overlays.append(cd)
|
|
||||||
|
|
||||||
# Fire on press (not release) so abilities feel instant under the thumb.
|
|
||||||
panel.gui_input.connect(_on_ability_input.bind(slot))
|
|
||||||
return panel
|
|
||||||
|
|
||||||
|
|
||||||
func _on_ability_input(event: InputEvent, slot: int) -> void:
|
func _draw_icon_fit(tex: Texture2D, box: Rect2) -> void:
|
||||||
var pressed := false
|
var ts := tex.get_size()
|
||||||
if event is InputEventScreenTouch:
|
if ts.x <= 0.0 or ts.y <= 0.0:
|
||||||
pressed = (event as InputEventScreenTouch).pressed
|
return
|
||||||
elif event is InputEventMouseButton:
|
var scale := minf(box.size.x / ts.x, box.size.y / ts.y)
|
||||||
var mb := event as InputEventMouseButton
|
var ds := ts * scale
|
||||||
pressed = mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT
|
_surface.draw_texture_rect(tex, Rect2(box.position + (box.size - ds) * 0.5, ds), false)
|
||||||
if pressed and _player != null:
|
|
||||||
_player.call(&"try_activate_ability", slot)
|
|
||||||
|
|
||||||
|
|
||||||
func _button_style(slot: int) -> StyleBoxFlat:
|
func _draw_centered_text(text: String, box: Rect2, size: int, color: Color) -> void:
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
if font == null:
|
||||||
|
return
|
||||||
|
var tw := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
|
||||||
|
var pos := box.position + (box.size - tw) * 0.5 + Vector2(0.0, tw.y * 0.35)
|
||||||
|
_surface.draw_string(font, pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
|
||||||
|
|
||||||
|
|
||||||
|
# Three stacked bars — a "☰" glyph drawn by hand, since the fallback font that ships with
|
||||||
|
# the web build has no U+2630 and renders it as tofu / a codepoint number.
|
||||||
|
func _draw_hamburger(r: Rect2) -> void:
|
||||||
|
var col := Color(1.00, 0.78, 0.22)
|
||||||
|
var bw := r.size.x * 0.5
|
||||||
|
var bh := maxf(3.0, r.size.y * 0.09)
|
||||||
|
var x := r.position.x + (r.size.x - bw) * 0.5
|
||||||
|
var cy := r.position.y + r.size.y * 0.5
|
||||||
|
var gap := r.size.y * 0.22
|
||||||
|
for i: int in [-1, 0, 1]:
|
||||||
|
_surface.draw_rect(Rect2(x, cy + i * gap - bh * 0.5, bw, bh), col)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_debug() -> void:
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
if font == null:
|
||||||
|
return
|
||||||
|
var players := get_tree().get_nodes_in_group(&"player")
|
||||||
|
var mats := get_tree().get_nodes_in_group(&"matador")
|
||||||
|
var lines := [
|
||||||
|
"GPU: %s" % RenderingServer.get_video_adapter_name(),
|
||||||
|
"API: %s" % RenderingServer.get_video_adapter_api_version(),
|
||||||
|
"method=%s web=%s touch_ui=%s" % [
|
||||||
|
ProjectSettings.get_setting("rendering/renderer/rendering_method", "?"),
|
||||||
|
OS.has_feature("web"), Controls.use_touch_ui()],
|
||||||
|
"bull: %s" % _node_report(players),
|
||||||
|
"matador(%d): %s" % [mats.size(), _node_report(mats)],
|
||||||
|
"events=%d last=%s touches=%d stick=%s" % [
|
||||||
|
_dbg_events, str(_dbg_last_pos.round()), _touches.size(), _stick_active],
|
||||||
|
]
|
||||||
|
var fs := 18
|
||||||
|
var line_h := 24.0
|
||||||
|
var w := 0.0
|
||||||
|
for line: String in lines:
|
||||||
|
w = maxf(w, font.get_string_size(line, HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x)
|
||||||
|
_surface.draw_rect(
|
||||||
|
Rect2(10.0, 34.0, w + 16.0, line_h * lines.size() + 12.0), Color(0.0, 0.0, 0.0, 0.62))
|
||||||
|
var y := 34.0 + 22.0
|
||||||
|
for line: String in lines:
|
||||||
|
_surface.draw_string(
|
||||||
|
font, Vector2(18.0, y), line, HORIZONTAL_ALIGNMENT_LEFT, -1, fs,
|
||||||
|
Color(1.0, 1.0, 0.3))
|
||||||
|
y += line_h
|
||||||
|
|
||||||
|
|
||||||
|
# One-line "does this character exist / is it drawable / where is it" report for the debug
|
||||||
|
# overlay, so an invisible bull/matador on-device can be pinned to spawn vs. position vs.
|
||||||
|
# GPU-draw failure.
|
||||||
|
func _node_report(nodes: Array) -> String:
|
||||||
|
if nodes.is_empty():
|
||||||
|
return "NONE"
|
||||||
|
var n := nodes[0] as Node3D
|
||||||
|
if n == null:
|
||||||
|
return "not Node3D"
|
||||||
|
return "vis=%s pos=%s" % [n.is_visible_in_tree(), str(n.global_position.round())]
|
||||||
|
|
||||||
|
|
||||||
|
func _button_style(accent: Color) -> StyleBoxFlat:
|
||||||
var s := StyleBoxFlat.new()
|
var s := StyleBoxFlat.new()
|
||||||
s.bg_color = Color(0.10, 0.07, 0.03, 0.80)
|
s.bg_color = Color(0.10, 0.07, 0.03, 0.80)
|
||||||
s.set_corner_radius_all(18)
|
s.set_corner_radius_all(18)
|
||||||
s.corner_detail = 6
|
s.corner_detail = 6
|
||||||
s.set_border_width_all(3)
|
s.set_border_width_all(3)
|
||||||
s.border_color = _ABILITY_ACCENT[slot]
|
s.border_color = accent
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
# ── Menu button ──────────────────────────────────────────────────────────────
|
# ── Menu ──────────────────────────────────────────────────────────────────────
|
||||||
# Top-right "☰" — the touch stand-in for the keyboard's Esc-to-menu, so a phone
|
|
||||||
# player has a way out of a run.
|
|
||||||
|
|
||||||
func _build_menu_button() -> void:
|
|
||||||
var btn := Button.new()
|
|
||||||
btn.text = "☰"
|
|
||||||
btn.custom_minimum_size = Vector2(56.0, 56.0)
|
|
||||||
btn.focus_mode = Control.FOCUS_NONE
|
|
||||||
btn.anchor_left = 1.0
|
|
||||||
btn.anchor_right = 1.0
|
|
||||||
btn.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
|
||||||
btn.offset_left = -72.0
|
|
||||||
btn.offset_top = 16.0
|
|
||||||
btn.offset_right = -16.0
|
|
||||||
btn.offset_bottom = 72.0
|
|
||||||
var style := StyleBoxFlat.new()
|
|
||||||
style.bg_color = Color(0.10, 0.07, 0.03, 0.80)
|
|
||||||
style.set_corner_radius_all(12)
|
|
||||||
style.set_border_width_all(2)
|
|
||||||
style.border_color = Color(1.00, 0.78, 0.22, 0.8)
|
|
||||||
btn.add_theme_stylebox_override("normal", style)
|
|
||||||
btn.add_theme_stylebox_override("hover", style)
|
|
||||||
btn.add_theme_stylebox_override("pressed", style)
|
|
||||||
btn.add_theme_color_override("font_color", Color(1.00, 0.78, 0.22))
|
|
||||||
btn.add_theme_font_size_override("font_size", 28)
|
|
||||||
btn.pressed.connect(_return_to_menu)
|
|
||||||
_root.add_child(btn)
|
|
||||||
|
|
||||||
|
|
||||||
func _return_to_menu() -> void:
|
func _return_to_menu() -> void:
|
||||||
get_tree().paused = false
|
get_tree().paused = false
|
||||||
@@ -282,34 +460,26 @@ func _return_to_menu() -> void:
|
|||||||
get_tree().change_scene_to_file("res://MainMenu.tscn")
|
get_tree().change_scene_to_file("res://MainMenu.tscn")
|
||||||
|
|
||||||
|
|
||||||
# ── Layout & lifecycle ──────────────────────────────────────────────────────
|
# ── Lifecycle ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _relayout() -> void:
|
func _process(delta: float) -> void:
|
||||||
var vp := get_viewport().get_visible_rect().size
|
_hint_pulse = fmod(_hint_pulse + delta * 2.2, TAU)
|
||||||
# Left half is the joystick zone; base radius scales with the shorter screen edge.
|
var active := Controls.use_touch_ui() and not get_tree().paused
|
||||||
_stick.position = Vector2.ZERO
|
# The game-over / pause screen owns the display when paused — stand down and drop any
|
||||||
_stick.size = Vector2(vp.x * 0.5, vp.y)
|
# held movement so the bull doesn't coast on a stuck joystick.
|
||||||
_stick_radius = clampf(minf(vp.x, vp.y) * 0.16, 80.0, 150.0)
|
if not active:
|
||||||
|
if _surface.visible:
|
||||||
|
_surface.visible = false
|
||||||
func _process(_delta: float) -> void:
|
|
||||||
var active := Controls.use_touch_ui()
|
|
||||||
# The game-over / pause screen owns the display when paused — stand down and drop
|
|
||||||
# any held movement so the bull doesn't coast on a stuck joystick.
|
|
||||||
if not active or get_tree().paused:
|
|
||||||
if _root.visible:
|
|
||||||
_root.visible = false
|
|
||||||
if _stick_active:
|
if _stick_active:
|
||||||
_end_stick()
|
_end_stick()
|
||||||
|
_touches.clear()
|
||||||
|
_touch_owner.clear()
|
||||||
|
_pinching = false
|
||||||
return
|
return
|
||||||
_root.visible = true
|
|
||||||
|
|
||||||
|
_surface.visible = true
|
||||||
if _player == null:
|
if _player == null:
|
||||||
var players := get_tree().get_nodes_in_group(&"player")
|
var players := get_tree().get_nodes_in_group(&"player")
|
||||||
if not players.is_empty():
|
if not players.is_empty():
|
||||||
_player = players[0]
|
_player = players[0]
|
||||||
return
|
_surface.queue_redraw() # cooldown overlays animate every frame
|
||||||
|
|
||||||
for slot: int in _cd_overlays.size():
|
|
||||||
var fraction: float = _player.call(&"ability_cooldown_fraction", slot)
|
|
||||||
_cd_overlays[slot].anchor_top = 1.0 - clampf(fraction, 0.0, 1.0)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user