14 Commits

Author SHA1 Message Date
richard 45427b7070 Merge remote-tracking branch 'origin/main' 2026-08-23 12:37:35 +03:00
richard d514435dd8 touch controls and hud 2026-08-23 12:37:30 +03:00
siim 3eb9fa50f3 +bear boss 2026-08-23 11:18:45 +03:00
siim aee900c009 bear boss, aga broken 2026-08-21 18:23:30 +03:00
siim 4cf714f516 Update spear.glb 2026-08-17 23:49:29 +03:00
siim b78736a1f4 icon update 2026-08-10 04:43:48 +03:00
siim 212263c858 credits tuun 2026-08-10 03:53:17 +03:00
siim 8c4eeb0861 cooldowns? 2026-08-10 03:30:53 +03:00
siim 95def2d527 loading text + fullscreen default 2026-08-10 01:04:57 +03:00
siim 9a43a9ff8b icon 2026-08-09 23:58:01 +03:00
siim 7238cbf49e death anim speed .8 2026-08-09 23:19:23 +03:00
siim 44c667f097 collision scale 2026-08-09 23:15:43 +03:00
siim 3cd347d420 walk anim, death anim ja loading screen 2026-08-09 23:05:55 +03:00
richard 6414b2e7cc uids 2026-08-09 19:40:39 +03:00
31 changed files with 813 additions and 226 deletions
+3 -2
View File
@@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://dk6xuwwa0mfpe"
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
path.etc2="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@@ -15,7 +16,7 @@ generator_parameters={
[deps]
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]
+3 -2
View File
@@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://b4gkpa7jypcsm"
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
path.etc2="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@@ -15,7 +16,7 @@ generator_parameters={
[deps]
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]
+3 -2
View File
@@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://yercpq3y2o0x"
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
path.etc2="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@@ -15,7 +16,7 @@ generator_parameters={
[deps]
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]
BIN
View File
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cofv5dbavesis"
path="res://.godot/imported/Bear.glb-d982be4b426919de19c028d884069d30.scn"
[deps]
source_file="res://Assets/Bear.glb"
dest_files=["res://.godot/imported/Bear.glb-d982be4b426919de19c028d884069d30.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ch1wqha62vhtb"
path="res://.godot/imported/IG_square.png-c38dd1a0a4601b6ff4ffec2910ed2ce0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/IG_square.png"
dest_files=["res://.godot/imported/IG_square.png-c38dd1a0a4601b6ff4ffec2910ed2ce0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+1 -1
View File
@@ -28,7 +28,7 @@ meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
meshes/force_disable_compression=true
skins/use_named_skins=true
animation/import=true
animation/fps=30
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -28,7 +28,7 @@ meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
meshes/force_disable_compression=true
skins/use_named_skins=true
animation/import=true
animation/fps=30
Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dct1d88g015el"
path="res://.godot/imported/kleeps_v2.png-55aa0dd25620611263a8b3000c5e2a49.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/kleeps_v2.png"
dest_files=["res://.godot/imported/kleeps_v2.png-55aa0dd25620611263a8b3000c5e2a49.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bh5nuyudh7x44"
path="res://.godot/imported/loading_screen.png-253743b71eb7ab3f4f9e2b29cbabf407.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/loading_screen.png"
dest_files=["res://.godot/imported/loading_screen.png-253743b71eb7ab3f4f9e2b29cbabf407.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
BIN
View File
Binary file not shown.
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://bxm7c6ldrdiq5"
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={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
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]
+3 -2
View File
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://3syhacswepyj"
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={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
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]
+3 -3
View File
@@ -51,7 +51,7 @@ dof_blur_near_transition = 5.0
transform = Transform3D(-0.56715536, 0, -0.8236109, 0, 1, 0, 0.8236109, 0, -0.56715536, 5.2114463, -0.08998108, 1.8927763)
script = ExtResource("2_5jr0i")
[node name="Skeleton3D" parent="Matador/Armature" parent_id_path=PackedInt32Array(166852612, 1269629432) index="0" unique_id=978366810]
[node name="Skeleton3D" parent="Matador/Armature" parent_id_path=PackedInt32Array(166852612, 1447304122) index="0" unique_id=357848305]
bones/0/position = Vector3(0.056660548, 0.6424767, -0.0167365)
bones/0/rotation = Quaternion(0.013179442, -0.2660563, 0.073319815, 0.9610747)
bones/1/rotation = Quaternion(0.06234612, 0.049236443, 0.08183311, 0.99347484)
@@ -81,10 +81,10 @@ bones/23/rotation = Quaternion(0.9993525, -0.0021175682, 0.03502641, -0.00795782
bones/24/rotation = Quaternion(0.08874816, -0.018719967, -0.04671622, 0.99478185)
bones/25/rotation = Quaternion(0.09634846, -0.0268489, -0.04050248, 0.9941608)
[node name="matador" parent="Matador/Armature/Skeleton3D" index="0" unique_id=256196413]
[node name="matador" parent="Matador/Armature/Skeleton3D" index="0" unique_id=593562242]
transform = Transform3D(0.8642424, 0, 0.50307554, 0, 1, 0, -0.50307554, 0, 0.8642424, 0, 0, 0)
[node name="Muleta" parent="Matador/Armature/Skeleton3D" index="1" unique_id=988244930]
[node name="Muleta" parent="Matador/Armature/Skeleton3D" index="1" unique_id=978680467]
cast_shadow = 0
[node name="Camera3D" type="Camera3D" parent="." unique_id=1531255459]
+2 -3
View File
@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://46y10alai56q" path="res://main_menu.gd" id="1_menu"]
[ext_resource type="VideoStream" uid="uid://csxxlnq74fej4" path="res://Assets/intro_bg.ogv" id="2_video"]
[ext_resource type="Shader" path="res://menu_blur.gdshader" id="3_blur"]
[ext_resource type="Shader" uid="uid://bmdtg6qlh7wri" path="res://menu_blur.gdshader" id="3_blur"]
[sub_resource type="ShaderMaterial" id="mat_blur"]
shader = ExtResource("3_blur")
@@ -74,7 +74,6 @@ grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
ratio = 1.7778
stretch_mode = 2
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="VideoAspect" unique_id=374854265]
layout_mode = 2
@@ -281,7 +280,7 @@ bbcode_enabled = true
text = "[center][font_size=32][color=#d9a833]Bullosseum[/color][/font_size]
[b][color=#d9a833]Siim Raud[/color][/b] [url=https://instagram.com/siimpressionist]@siimpressionist[/url]
[font_size=20]Artist[/font_size]
[font_size=20]Artist / Animator[/font_size]
[b][color=#d9a833]Richard Aasa[/color][/b] [url=https://instagram.com/aasarichard]@aasarichard[/url]
[font_size=20]Programmer[/font_size]
+27
View File
@@ -35,4 +35,31 @@ script = ExtResource("2_camera_iso")
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
[node name="Skeleton3D" parent="bull/Armature" parent_id_path=PackedInt32Array(1386495166, 547304299) index="0" unique_id=2096877109]
bones/0/position = Vector3(0, 0, 0.009963839)
bones/0/rotation = Quaternion(-0.733504, -0.0036974847, 0.0013620416, -0.67967373)
bones/0/scale = Vector3(1, 0.99999994, 0.99999994)
bones/1/position = Vector3(0.0035266252, -0.0043296535, 0.0040827994)
bones/1/rotation = Quaternion(0.020680686, 0.45675468, 0.8716418, -0.1766016)
bones/2/rotation = Quaternion(0.3289326, -0.03432299, -0.06047997, 0.94178957)
bones/4/position = Vector3(0.0045358227, -0.0021376952, -0.004238624)
bones/4/rotation = Quaternion(-0.2448651, -0.018126775, 0.96885973, -0.0319902)
bones/4/scale = Vector3(1.0000001, 1, 1.0000001)
bones/5/rotation = Quaternion(-0.7492961, -0.32333943, -0.24235487, 0.5246629)
bones/5/scale = Vector3(1, 1, 1)
bones/6/rotation = Quaternion(0.32210493, 0.15572408, 0.14126852, 0.9230611)
bones/6/scale = Vector3(0.9999998, 1, 1)
bones/17/position = Vector3(-2.3283064e-12, 0.0009373817, 0.007552617)
bones/17/rotation = Quaternion(-5.3357535e-08, 0.7071068, 0.7071067, -5.3357535e-08)
bones/19/position = Vector3(-0.0035266252, -0.0043296525, 0.0040827994)
bones/19/rotation = Quaternion(0.07793758, -0.2513099, 0.94914854, 0.1728764)
bones/20/rotation = Quaternion(-0.3791731, -0.08821739, 0.07153107, 0.91832936)
bones/22/position = Vector3(-0.0045358227, -0.0021376943, -0.004238624)
bones/22/rotation = Quaternion(0.20629132, 0.46411392, 0.84344316, 0.17506008)
bones/22/scale = Vector3(1.0000001, 1.0000001, 1.0000001)
bones/23/rotation = Quaternion(-0.7287124, 0.31838557, 0.209407, 0.5689971)
bones/23/scale = Vector3(1.0000001, 1.0000002, 1.0000002)
bones/24/rotation = Quaternion(0.71033937, -0.13944183, -0.049755, 0.6881123)
bones/24/scale = Vector3(0.9999999, 1, 1.0000001)
[editable path="bull"]
+9
View File
@@ -42,6 +42,15 @@ func _unhandled_input(event: InputEvent) -> void:
_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:
fov = lerpf(fov, _user_fov, delta * 8.0)
+53 -6
View File
@@ -26,18 +26,65 @@ func _ready() -> void:
## Whether to drive the game with the on-screen touch UI (joystick + buttons)
## instead of keyboard hints. DisplayServer.is_touchscreen_available() can't be
## used here: `emulate_touch_from_mouse` (on for desktop testing) makes it report
## true on every desktop. Gate on the actual runtime platform instead — real
## mobile/native and mobile web browsers — plus the DP force flag for testing.
## instead of keyboard hints. Platform detection is cached (it can't change mid-run)
## so the per-frame/per-input callers don't repay the cost — but the DP force flag is
## re-read every call so the console can toggle the layout live on a desktop build.
var _touch_ui_cached: int = -1 # -1 = not yet detected, 0 = false, 1 = true
func use_touch_ui() -> bool:
if DP.b("force_touch_controls"):
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")
or OS.has_feature("web_android")
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.
+3
View File
@@ -299,6 +299,9 @@ func _register_all() -> void:
# Force the on-screen touch controls (joystick + ability buttons) on even on a
# desktop build — otherwise they appear only when a touchscreen is detected.
_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,
+88 -2
View File
@@ -1,18 +1,30 @@
[runnable_presets]
Web="Web"
"Windows Desktop"="Windows Desktop"
[preset.0]
name="Web"
platform="Web"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/web/index.html"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.0.options]
@@ -21,7 +33,7 @@ custom_template/release=""
variant/extensions_support=false
variant/thread_support=true
vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=false
vram_texture_compression/for_mobile=true
html/export_icon=true
html/custom_html_shell=""
html/head_include=""
@@ -29,6 +41,7 @@ html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
progressive_web_app/enabled=false
progressive_web_app/ensure_cross_origin_isolation_headers=true
progressive_web_app/offline_page=""
progressive_web_app/display=1
progressive_web_app/orientation=0
@@ -36,3 +49,76 @@ progressive_web_app/icon_144x144=""
progressive_web_app/icon_180x180=""
progressive_web_app/icon_512x512=""
progressive_web_app/background_color=Color(0, 0, 0, 1)
threads/emscripten_pool_size=8
threads/godot_pool_size=4
[preset.1]
name="Windows Desktop"
platform="Windows Desktop"
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../Export/Bullosseum_020.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.1.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
Start-ScheduledTask -TaskName godot_remote_debug
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"
+11 -1
View File
@@ -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
# 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.
# 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:
if Controls.use_touch_ui():
return
_bull_bg = TextureRect.new()
if ResourceLoader.exists(_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
# overlay so exactly one control scheme shows on any given device.
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
_tab_hint.visible = false
else:
+10 -3
View File
@@ -14,7 +14,9 @@ const _ANIM_IDLE: StringName = &"Armature|IDLE"
const _ANIM_KICK: StringName = &"Armature|KICK"
const _ANIM_SLAM: StringName = &"Armature|SLAM"
const _ANIM_DASH: StringName = &"Armature|DASH"
const _ANIM_WALK: StringName = &"Armature|WALK"
const _ANIM_ROLL: StringName = &"Armature|ROLL"
const _ANIM_DEATH: StringName = &"Armature|DEATH"
@onready var camera_pivot: Node3D = $Camera3D
@onready var cube_guy: Node3D = $bull
@@ -306,9 +308,11 @@ func _update_locomotion_anim(moving: bool) -> void:
return
_locomotion_moving = moving
if moving:
if _bull_anim.has_animation(_ANIM_DASH):
_bull_anim.speed_scale = 1.0
_bull_anim.play(_ANIM_DASH, 0.25)
if _bull_anim.has_animation(_ANIM_WALK):
_bull_anim.speed_scale = 2.0
_bull_anim.play(_ANIM_WALK, 1)
var anim = _bull_anim.get_animation(_ANIM_WALK)
anim.loop_mode = Animation.LOOP_LINEAR
else:
_play_idle()
@@ -1085,6 +1089,9 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void
_huff_player.pitch_scale = randf_range(0.7, 0.9)
_huff_player.play()
if _hp <= 0:
if _bull_anim.current_animation != _ANIM_DEATH:
_bull_anim.speed_scale = 0.8
_bull_anim.play(_ANIM_DEATH)
_dead = true
died.emit(cause)
+10 -5
View File
@@ -14,12 +14,11 @@ config/name="Bullosseum"
config/version="0.2.0"
run/main_scene="res://MainMenu.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")
config/icon="res://icon.svg"
config/run/main_scene="res://MainMenu.tscn"
boot_splash/bg_color=Color(0, 0, 0, 1)
boot_splash/image="res://Assets/loading_screen.png"
boot_splash/fullsize=true
boot_splash/use_filter=true
config/icon="uid://dct1d88g015el"
boot_splash/minimum_display_time=4000
config/run/main_scene="res://MainMenu.tscn"
[autoload]
@@ -29,6 +28,10 @@ Controls="*res://controls_manager.gd"
DebugDraw="*res://debug_draw.gd"
Settings="*res://settings.gd"
[display]
window/size/mode=3
[editor_plugins]
enabled=PackedStringArray("res://addons/rider-plugin/plugin.cfg")
@@ -118,4 +121,6 @@ pointing/emulate_touch_from_mouse=true
[rendering]
rendering_device/driver.windows="d3d12"
anti_aliasing/quality/use_taa=true
renderer/rendering_method="gl_compatibility"
textures/vram_compression/import_s3tc_bptc=true
textures/vram_compression/import_etc2_astc=true
+54 -1
View File
@@ -47,6 +47,56 @@ fov = 50.0
near = 2.636
far = 0.01
[node name="Skeleton3D" parent="Player/bull/Armature" parent_id_path=PackedInt32Array(165510044, 1386495166, 547304299) index="0" unique_id=2096877109]
bones/0/position = Vector3(0, 0, 0.0074015963)
bones/0/rotation = Quaternion(-0.36922705, 0.6030517, -0.6030517, -0.36922705)
bones/0/scale = Vector3(1, 1, 1)
bones/1/position = Vector3(0.003526625, -0.0043296535, 0.004082799)
bones/1/rotation = Quaternion(-0.00038647576, 0.06909673, 0.9868542, -0.14609681)
bones/1/scale = Vector3(1.0000021, 1.0000001, 1.0000006)
bones/2/position = Vector3(-5.853537e-11, 0.002669535, -1.489375e-10)
bones/2/rotation = Quaternion(-0.06929122, 3.2415448e-07, -0.11267061, 0.99121344)
bones/2/scale = Vector3(1, 1.0000001, 0.9999999)
bones/4/position = Vector3(0.004535823, -0.0021376952, -0.0042386237)
bones/4/rotation = Quaternion(-0.24948508, 0.19590344, 0.9416978, -0.11217982)
bones/4/scale = Vector3(0.99999994, 0.99999994, 1.0000002)
bones/5/position = Vector3(1.264555e-10, 0.0028599252, -2.4303243e-10)
bones/5/rotation = Quaternion(-0.58943665, -0.36607245, -0.2256448, 0.683842)
bones/6/position = Vector3(-7.092306e-11, 0.003274492, 4.6413304e-10)
bones/6/rotation = Quaternion(0.52910197, 0.15997829, 0.12024502, 0.82462066)
bones/6/scale = Vector3(1, 1.0000001, 1.0000001)
bones/8/position = Vector3(2.259811e-19, 0.0010177285, -0.008495503)
bones/8/rotation = Quaternion(-0.70710677, 0, 0, 0.70710677)
bones/8/scale = Vector3(1, 1, 1)
bones/9/position = Vector3(4.814825e-35, 0.0017711426, 0)
bones/10/position = Vector3(4.814825e-35, 0.0018082206, 0)
bones/11/position = Vector3(0, 0.0019911951, 0)
bones/11/scale = Vector3(1, 1, 1)
bones/12/position = Vector3(0, 0.002023485, 0)
bones/12/scale = Vector3(1, 1, 1)
bones/13/position = Vector3(0, 0.0016682985, 0)
bones/13/scale = Vector3(1, 1, 1)
bones/14/position = Vector3(0, 0.0014960878, 0)
bones/14/scale = Vector3(1, 1, 1)
bones/15/position = Vector3(0, 0.0010996431, 0)
bones/17/position = Vector3(0, 0.000937381, 0.0075526168)
bones/17/rotation = Quaternion(-5.3385083e-08, 0.70710677, 0.70710677, -5.3385083e-08)
bones/19/position = Vector3(-0.003526625, -0.0043296535, 0.004082799)
bones/19/rotation = Quaternion(0.00038647576, 0.06909673, 0.9868542, 0.14609681)
bones/19/scale = Vector3(1.0000021, 1.0000001, 1.0000006)
bones/20/position = Vector3(5.853537e-11, 0.002669535, -1.489375e-10)
bones/20/rotation = Quaternion(-0.06929122, -3.2415448e-07, 0.11267061, 0.99121344)
bones/20/scale = Vector3(1, 1.0000001, 0.9999999)
bones/22/position = Vector3(-0.004535823, -0.0021376952, -0.0042386237)
bones/22/rotation = Quaternion(0.24948508, 0.19590344, 0.9416978, 0.11217982)
bones/22/scale = Vector3(0.99999994, 0.99999994, 1.0000002)
bones/23/position = Vector3(-1.264555e-10, 0.0028599252, -2.4303243e-10)
bones/23/rotation = Quaternion(-0.58943665, 0.36607245, 0.2256448, 0.683842)
bones/23/scale = Vector3(1, 1, 1)
bones/24/position = Vector3(7.092306e-11, 0.003274492, 4.6413304e-10)
bones/24/rotation = Quaternion(0.52910197, -0.15997829, -0.12024502, 0.82462066)
bones/24/scale = Vector3(1, 1.0000001, 1.0000001)
[node name="Node3D" type="Node3D" parent="." unique_id=1081100876]
[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=2072095624]
@@ -63,6 +113,9 @@ visible = false
[node name="Wall" parent="Node3D/arena_placeholder_v01" index="1" unique_id=171701243]
visible = false
[node name="CollisionShape3D" parent="Node3D/arena_placeholder_v01/Wall/StaticBody3D" parent_id_path=PackedInt32Array(1341659233, 1793167121) index="0" unique_id=404958985]
transform = Transform3D(0.98309773, 0, 0, 0, 0.9558868, 0, 0, 0, 4.061706, 0.13596812, -0.091219574, -5.4371103e-09)
[node name="Arena2" parent="Node3D/arena_placeholder_v01" unique_id=524718167 instance=ExtResource("3_fy5k1")]
[node name="arena model2026" parent="Node3D/arena_placeholder_v01" unique_id=179544183 instance=ExtResource("10_5juve")]
@@ -112,7 +165,7 @@ spot_range = 32.90586
[node name="SUN_DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=1706919950]
transform = Transform3D(0.45811793, 0.34843865, -0.81775206, 0.5316611, 0.62986195, 0.56622505, 0.7123655, -0.69416475, 0.10329977, 0, 8.980043, 0)
light_energy = 1.199
light_energy = 0.106
shadow_enabled = true
shadow_blur = 10.0
+360 -190
View File
@@ -2,15 +2,23 @@ extends CanvasLayer
## 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.
##
## 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(), which shares the keyboard's cooldown gate.
## All interaction is driven from raw `_input()` with manual hit-testing, NOT from
## Control.gui_input. On the web/wasm build the overlay lives in a CanvasLayer nested
## under the HUD, and GUI touch routing into a nested CanvasLayer renders fine but
## 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
## "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
## testing.
## "force_touch_controls" is on so the layout can be exercised on a desktop build
## (emulate_touch_from_mouse turns a mouse click into a touch event for that).
# move_* actions, indexed by the four joystick push directions.
const _MOVE_RIGHT: StringName = &"move_right"
@@ -33,80 +41,208 @@ const _ABILITY_ACCENT: Array[Color] = [
Color(1.00, 0.82, 0.32), # Roll — gold
]
var _root: Control
var _stick: Control
var _cd_overlays: Array[ColorRect] = []
# Sentinel touch indices. A real finger uses its own non-negative index; the mouse
# (desktop force-flag testing, when no real touch has been seen) uses _MOUSE.
const _MOUSE: int = -2
var _surface: Control
var _player: Node = null
# Floating-joystick state. _origin is where the thumb first touched (base centre),
# _knob is the current thumb position; both in _stick-local coordinates. _touch_index
# is the finger owning the stick (-1 = none, -2 = mouse), so a second finger tapping
# an ability button never steals the stick.
# Cached textures + styleboxes so the per-frame redraw (cooldowns animate) allocates nothing.
var _ability_tex: Array[Texture2D] = []
var _ability_style: Array[StyleBoxFlat] = []
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 _origin: Vector2 = Vector2.ZERO
var _knob: Vector2 = Vector2.ZERO
var _touch_index: int = -3
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:
layer = 10 # below the HUD (11) so the game-over screen always draws on top
process_mode = Node.PROCESS_MODE_ALWAYS
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_FULL_RECT)
_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.visible = false
add_child(_root)
for slot: int in 4:
var tex: Texture2D = null
if ResourceLoader.exists(_ABILITY_ICONS[slot]):
tex = load(_ABILITY_ICONS[slot])
_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()
get_viewport().size_changed.connect(_relayout)
# ── Joystick ────────────────────────────────────────────────────────────────
# The stick zone is the whole left half of the screen: touching anywhere in it
# plants the base under the thumb, so the player never has to find a fixed pad.
# ── Layout ────────────────────────────────────────────────────────────────────
# Left half is the joystick zone (touch anywhere in it to plant the base under the thumb);
# the ability cluster and menu button sit in the right half so they never overlap it.
func _build_joystick() -> void:
_stick = Control.new()
_stick.mouse_filter = Control.MOUSE_FILTER_STOP
_stick.gui_input.connect(_on_stick_input)
_stick.draw.connect(_draw_stick)
_root.add_child(_stick)
func _relayout() -> void:
var vp := get_viewport().get_visible_rect().size
var short := minf(vp.x, vp.y)
_stick_radius = clampf(short * 0.16, 80.0, 150.0)
_stick_zone = Rect2(Vector2.ZERO, Vector2(vp.x * 0.5, vp.y))
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:
_saw_real_touch = true
var t := event as InputEventScreenTouch
if t.pressed and not _stick_active:
_begin_stick(t.position, t.index)
elif not t.pressed and _touch_index == t.index:
_end_stick()
_dbg_events += 1
_dbg_last_pos = t.position
if t.pressed:
_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:
var d := event as InputEventScreenDrag
if _touch_index == d.index:
_update_stick(d.position)
elif event is InputEventMouseButton:
_dbg_events += 1
_dbg_last_pos = d.position
_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
if mb.button_index == MOUSE_BUTTON_LEFT:
if mb.pressed and not _stick_active:
_begin_stick(mb.position, -2)
elif not mb.pressed and _touch_index == -2:
_end_stick()
elif event is InputEventMouseMotion and _touch_index == -2:
_update_stick((event as InputEventMouseMotion).position)
_dbg_events += 1
_dbg_last_pos = mb.position
if mb.pressed:
_touches[_MOUSE] = mb.position
_on_press(_MOUSE, mb.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
_touch_index = index
_origin = pos
_knob = pos
_stick.queue_redraw()
func _update_stick(pos: Vector2) -> void:
@@ -117,19 +253,16 @@ func _update_stick(pos: Vector2) -> void:
offset = offset.normalized() * _stick_radius
_knob = _origin + offset
_apply_move(offset / _stick_radius)
_stick.queue_redraw()
func _end_stick() -> void:
_stick_active = false
_touch_index = -3
_release_move()
_stick.queue_redraw()
# 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
# only one of each axis is ever pressed at a time.
# strengths on the four movement actions. Opposite pairs are mutually exclusive, so only
# one of each axis is ever pressed at a time.
func _apply_move(v: Vector2) -> void:
Input.action_press(_MOVE_RIGHT, 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)
func _draw_stick() -> void:
if not _stick_active:
# ── Pinch-to-zoom ─────────────────────────────────────────────────────────────
# 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
var ring := Color(0.93, 0.88, 0.72, 0.30)
var knob := Color(1.00, 0.82, 0.32, 0.55)
_stick.draw_circle(_origin, _stick_radius, Color(0.0, 0.0, 0.0, 0.20))
_stick.draw_arc(_origin, _stick_radius, 0.0, TAU, 48, ring, 4.0, true)
_stick.draw_circle(_knob, _stick_radius * 0.42, knob)
var dist := (pts[0] as Vector2).distance_to(pts[1] as Vector2)
if _pinch_dist <= 0.0 or dist <= 0.0:
_pinch_dist = dist
return
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 ─────────────────────────────────────────────────────────
# 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.
# ── Drawing ───────────────────────────────────────────────────────────────────
func _build_ability_buttons() -> void:
var grid := GridContainer.new()
grid.name = "AbilityGrid"
grid.columns = 2
grid.add_theme_constant_override("h_separation", 14)
grid.add_theme_constant_override("v_separation", 14)
grid.mouse_filter = Control.MOUSE_FILTER_IGNORE
grid.anchor_left = 1.0
grid.anchor_right = 1.0
grid.anchor_top = 1.0
grid.anchor_bottom = 1.0
grid.grow_horizontal = Control.GROW_DIRECTION_BEGIN
grid.grow_vertical = Control.GROW_DIRECTION_BEGIN
grid.offset_left = -220.0
grid.offset_top = -220.0
grid.offset_right = -24.0
grid.offset_bottom = -24.0
_root.add_child(grid)
func _draw_surface() -> void:
for slot: int in _ability_rects.size():
var r: Rect2 = _ability_rects[slot]
_surface.draw_style_box(_ability_style[slot], r)
var inner := r.grow(-r.size.x * 0.14)
if _ability_tex[slot] != null:
_draw_icon_fit(_ability_tex[slot], inner)
else:
_draw_centered_text(_ABILITY_LETTERS[slot], r, 40, _ABILITY_ACCENT[slot])
var frac := 0.0
if _player != null:
frac = clampf(_player.call(&"ability_cooldown_fraction", slot), 0.0, 1.0)
if frac > 0.0:
var ch := r.size.y * frac
_surface.draw_rect(
Rect2(r.position.x, r.position.y + r.size.y - ch, r.size.x, ch),
Color(0.0, 0.0, 0.0, 0.55))
for slot: int in 4:
grid.add_child(_build_ability_button(slot))
_surface.draw_style_box(_menu_style, _menu_rect)
_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:
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(96.0, 96.0)
panel.add_theme_stylebox_override("panel", _button_style(slot))
panel.mouse_filter = Control.MOUSE_FILTER_STOP
var stack := Control.new()
stack.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_child(stack)
if ResourceLoader.exists(_ABILITY_ICONS[slot]):
var icon := TextureRect.new()
icon.texture = load(_ABILITY_ICONS[slot])
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.set_anchors_preset(Control.PRESET_FULL_RECT)
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
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
# A faint "ghost" joystick at rest in the lower-left with a caption, so the movement zone
# is discoverable. Breathes gently via _hint_pulse; vanishes the moment the stick is grabbed.
func _draw_stick_hint() -> void:
var a := 0.16 + 0.12 * (0.5 + 0.5 * sin(_hint_pulse))
var c := _stick_hint_center
var r := _stick_radius * 0.72
_surface.draw_circle(c, r, Color(0.0, 0.0, 0.0, a * 0.45))
_surface.draw_arc(c, r, 0.0, TAU, 40, Color(0.93, 0.88, 0.72, a + 0.14), 3.0, true)
_surface.draw_circle(c, r * 0.32, Color(1.00, 0.82, 0.32, a + 0.16))
var font := ThemeDB.fallback_font
if font != null:
var txt := "DRAG TO MOVE"
var fs := 18
var tw := font.get_string_size(txt, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
_surface.draw_string(
font, c + Vector2(-tw.x * 0.5, r + 28.0), txt, HORIZONTAL_ALIGNMENT_LEFT, -1, fs,
Color(0.93, 0.88, 0.72, a + 0.30))
func _on_ability_input(event: InputEvent, slot: int) -> void:
var pressed := false
if event is InputEventScreenTouch:
pressed = (event as InputEventScreenTouch).pressed
elif event is InputEventMouseButton:
var mb := event as InputEventMouseButton
pressed = mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT
if pressed and _player != null:
_player.call(&"try_activate_ability", slot)
func _draw_icon_fit(tex: Texture2D, box: Rect2) -> void:
var ts := tex.get_size()
if ts.x <= 0.0 or ts.y <= 0.0:
return
var scale := minf(box.size.x / ts.x, box.size.y / ts.y)
var ds := ts * scale
_surface.draw_texture_rect(tex, Rect2(box.position + (box.size - ds) * 0.5, ds), false)
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()
s.bg_color = Color(0.10, 0.07, 0.03, 0.80)
s.set_corner_radius_all(18)
s.corner_detail = 6
s.set_border_width_all(3)
s.border_color = _ABILITY_ACCENT[slot]
s.border_color = accent
return s
# ── Menu button ──────────────────────────────────────────────────────────────
# 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)
# ── Menu ──────────────────────────────────────────────────────────────────────
func _return_to_menu() -> void:
get_tree().paused = false
@@ -282,34 +460,26 @@ func _return_to_menu() -> void:
get_tree().change_scene_to_file("res://MainMenu.tscn")
# ── Layout & lifecycle ──────────────────────────────────────────────────────
# ── Lifecycle ─────────────────────────────────────────────────────────────────
func _relayout() -> void:
var vp := get_viewport().get_visible_rect().size
# Left half is the joystick zone; base radius scales with the shorter screen edge.
_stick.position = Vector2.ZERO
_stick.size = Vector2(vp.x * 0.5, vp.y)
_stick_radius = clampf(minf(vp.x, vp.y) * 0.16, 80.0, 150.0)
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
func _process(delta: float) -> void:
_hint_pulse = fmod(_hint_pulse + delta * 2.2, TAU)
var active := Controls.use_touch_ui() and not get_tree().paused
# 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:
if _surface.visible:
_surface.visible = false
if _stick_active:
_end_stick()
_touches.clear()
_touch_owner.clear()
_pinching = false
return
_root.visible = true
_surface.visible = true
if _player == null:
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
_player = players[0]
return
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)
_surface.queue_redraw() # cooldown overlays animate every frame
+1
View File
@@ -0,0 +1 @@
uid://d4ky4pbu70bo3