51 Commits

Author SHA1 Message Date
siim bd10d1789b bear max hp 30 2026-08-27 21:30:48 +03:00
siim 170d8ff3f1 siim timmimised väiksed 2026-08-27 21:15:36 +03:00
siim 550bf2c5ad adjustable wave size with debug parameter "mat_wave_size" 2026-08-27 20:36:00 +03:00
siim 02f3e283de landing. don't bounce! 2026-08-27 08:30:03 +03:00
siim 9123574947 crowd billbord placement + mat sword run fix 2026-08-26 22:32:30 +03:00
siim aadf59d2d0 lobby collisions 2026-08-26 21:09:41 +03:00
richard d57c3dc2a2 Merge remote-tracking branch 'origin/main' 2026-08-26 20:38:14 +03:00
richard 09ae6bfbf4 lobby level 2026-08-26 20:38:07 +03:00
richard 40c1b8e680 debug param save works, bull/bear hitbox 2026-08-26 20:37:50 +03:00
siim a1c77fed5e Update debug_params.gd 2026-08-26 20:02:23 +03:00
siim 1a8af92d88 matador attack fix 2026-08-26 19:53:43 +03:00
siim 8166a6c18e Merge branch 'main' of https://git.richard.work/siim/Bullosseum 2026-08-26 19:41:35 +03:00
siim 46d6dda325 capsule 2026-08-26 19:41:23 +03:00
richard fc9348551d fix sword holster 2026-08-26 19:18:52 +03:00
richard c4a19c7443 crowd markers, unsheathed sword correct 2026-08-26 19:13:39 +03:00
siim 95c4c0ddf9 BEAR ARENA UPDATE 2026-08-26 19:12:27 +03:00
siim 6a4f104655 bear arena model updeit 2026-08-26 18:30:09 +03:00
siim d4b67888d7 controller zoom with right stick 2026-08-26 18:12:12 +03:00
siim b177e0aee3 matadors dont come inside the player anymore with sword attack. also got rid of matador.gd dublicate.. that i made for testing animation 2026-08-26 08:39:40 +03:00
siim 2a6f4c0f7b bear_FX 2026-08-25 21:58:43 +03:00
siim 32d0c58f5c bear_ Call method track testing with spears 2026-08-25 08:49:44 +03:00
siim 251002ea7c bull and bear flashing red when take damage 2026-08-25 08:04:39 +03:00
siim 986496097f arena cleanup 2026-08-24 18:45:03 +03:00
siim d976deeb6a arena cleanup+tex modification 2026-08-24 18:41:34 +03:00
siim d632670092 +death animation bear 2026-08-24 08:36:48 +03:00
siim e1899e49f4 level lobby 2026-08-23 23:40:28 +03:00
siim 6f5f9e6bba ps1 filter tuun 2026-08-23 22:26:58 +03:00
siim 91cd027a98 adj 2026-08-23 22:05:57 +03:00
siim ef2fb24969 cooldowns increase 2026-08-23 21:39:19 +03:00
siim 4c75776c70 debug_param_cooldown adjusts 2026-08-23 21:28:19 +03:00
siim 7688084b7e spear update 2026-08-23 21:15:25 +03:00
richard ccc92f201f mobile support tweaks 2026-08-23 20:32:52 +03:00
richard 008650a87e Merge remote-tracking branch 'origin/main' 2026-08-23 17:34:05 +03:00
richard b9e3147519 Bear, level selector (to be removed), new matador spawn, new bear level, saving config updates source files 2026-08-23 17:33:47 +03:00
richard cdba320cbc bear 2026-08-23 17:33:32 +03:00
richard ee7b1617c1 Bear, level selector (to be removed), new matador spawn, new bear level, saving config updates source files 2026-08-23 17:33:15 +03:00
richard d749485126 Map screen version 2026-08-23 13:16:04 +03:00
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
117 changed files with 16957 additions and 1438 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+3 -2
View File
@@ -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]
+3 -2
View File
@@ -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]
+3 -2
View File
@@ -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]
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
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
Binary file not shown.
@@ -3,13 +3,13 @@
importer="scene" importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://dxe72ig8gfbcg" uid="uid://cwmo5vh738x6w"
path="res://.godot/imported/arena model2026.glb-299db01cae891e5005f8ac1e6c83d0a6.scn" path="res://.godot/imported/Lobby_level.glb-a7e896ecf7377d9530a80d5e63140cc3.scn"
[deps] [deps]
source_file="res://Assets/arena model2026.glb" source_file="res://Assets/Lobby_level.glb"
dest_files=["res://.godot/imported/arena model2026.glb-299db01cae891e5005f8ac1e6c83d0a6.scn"] dest_files=["res://.godot/imported/Lobby_level.glb-a7e896ecf7377d9530a80d5e63140cc3.scn"]
[params] [params]
Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://xtv6u00w4nmy"
path.s3tc="res://.godot/imported/Lobby_level_sand.png-bb70b0169c6b5159e020ce143afe9786.s3tc.ctex"
path.etc2="res://.godot/imported/Lobby_level_sand.png-bb70b0169c6b5159e020ce143afe9786.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "c8b9b7d6205ce80dfcfaa38152b996fe"
}
[deps]
source_file="res://Assets/Lobby_level_sand.png"
dest_files=["res://.godot/imported/Lobby_level_sand.png-bb70b0169c6b5159e020ce143afe9786.s3tc.ctex", "res://.godot/imported/Lobby_level_sand.png-bb70b0169c6b5159e020ce143afe9786.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0
Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://csy5f3l5lufp5"
path.s3tc="res://.godot/imported/Lobby_level_wall.png-39243638bfc6ea5742b6e2fdc927ae2c.s3tc.ctex"
path.etc2="res://.godot/imported/Lobby_level_wall.png-39243638bfc6ea5742b6e2fdc927ae2c.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "0e7ed34c3c812d96732bd32f9dca0c93"
}
[deps]
source_file="res://Assets/Lobby_level_wall.png"
dest_files=["res://.godot/imported/Lobby_level_wall.png-39243638bfc6ea5742b6e2fdc927ae2c.s3tc.ctex", "res://.godot/imported/Lobby_level_wall.png-39243638bfc6ea5742b6e2fdc927ae2c.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0
Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ym5273astq46"
path.s3tc="res://.godot/imported/Lobby_level_wood.png-4817792c09af3fd5e04ad29e0f9bd0ce.s3tc.ctex"
path.etc2="res://.godot/imported/Lobby_level_wood.png-4817792c09af3fd5e04ad29e0f9bd0ce.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "23a7a951553ffe271c47693fd71314ca"
}
[deps]
source_file="res://Assets/Lobby_level_wood.png"
dest_files=["res://.godot/imported/Lobby_level_wood.png-4817792c09af3fd5e04ad29e0f9bd0ce.s3tc.ctex", "res://.godot/imported/Lobby_level_wood.png-4817792c09af3fd5e04ad29e0f9bd0ce.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0
Binary file not shown.
+1 -1
View File
@@ -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
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
[gd_scene format=3 uid="uid://by4ifsgxlv0gj"]
[ext_resource type="PackedScene" uid="uid://bm6rmt3xprhjs" path="res://Assets/bear_level_arena.glb" id="1_beararena"]
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
[node name="bear_level" type="Node3D" unique_id=2020009824]
[node name="Node3D" type="Node3D" parent="." unique_id=1366355859]
[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=721553462]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=474515358]
transform = Transform3D(30, 0, 0, 0, 1, 0, 0, 0, 30, 0, -0.5, 0)
shape = SubResource("BoxShape3D_floor")
[node name="bear_level_arena" parent="." unique_id=948414670 instance=ExtResource("1_beararena")]
transform = Transform3D(2.875, 0, 0, 0, 2.875, 0, 0, 0, 2.875, 0, 0, 0)
[node name="MatadorSpawner" type="Node3D" parent="." unique_id=1869467620]
[node name="Lights" type="Node3D" parent="." unique_id=1435665429]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=1814200565]
transform = Transform3D(0.01535992, 0.56825536, -0.82270896, 0.16280846, 0.8104039, 0.56279576, 0.9865382, -0.14258848, -0.08006904, -4.08134, 9.197985, 0.97087634)
light_energy = 0.4
light_indirect_energy = 0.767
shadow_blur = 0.47
[node name="SpotLight3D2" type="SpotLight3D" parent="Lights" unique_id=2071037822]
transform = Transform3D(0.904133, 0.30962402, -0.2944087, 0.3084627, 0.0037425472, 0.9512291, 0.2956252, -0.95085174, -0.0921237, -3.629381, 10.86347, -2.0124264)
light_energy = 16.0
light_indirect_energy = 26.095
distance_fade_enabled = true
spot_range = 32.90586
spot_angle = 73.93
[node name="SUN_DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=1003843933]
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 = 0.106
shadow_enabled = true
shadow_blur = 10.0
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bm6rmt3xprhjs"
path="res://.godot/imported/bear_level_arena.glb-b6a9df3b338c8b0345fe2642d8b715eb.scn"
[deps]
source_file="res://Assets/bear_level_arena.glb"
dest_files=["res://.godot/imported/bear_level_arena.glb-b6a9df3b338c8b0345fe2642d8b715eb.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
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/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
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bdbx0wwa73rhp"
path="res://.godot/imported/Bear_FX.glb-5bac655e61b5e140f6cd145298d00ca8.scn"
[deps]
source_file="res://Assets/fx/Bear_FX.glb"
dest_files=["res://.godot/imported/Bear_FX.glb-5bac655e61b5e140f6cd145298d00ca8.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.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://2e6fbder27w1"
path="res://.godot/imported/cubes_fx_test.glb-29490ce7011d666c1c4917a1a92679e4.scn"
[deps]
source_file="res://Assets/fx/cubes_fx_test.glb"
dest_files=["res://.godot/imported/cubes_fx_test.glb-29490ce7011d666c1c4917a1a92679e4.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: 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
-5
View File
@@ -1,5 +0,0 @@
extends Node3D
func _ready() -> void:
$AnimationPlayer.play("Taunt")
-1
View File
@@ -1 +0,0 @@
uid://bn3xwnlktwdlf
BIN
View File
Binary file not shown.
+287
View File
@@ -0,0 +1,287 @@
[gd_scene format=3 uid="uid://o8ocjfprxa7u"]
[ext_resource type="Script" uid="uid://cn7fhjkro7637" path="res://bear.gd" id="1_beargd"]
[ext_resource type="PackedScene" uid="uid://cofv5dbavesis" path="res://Assets/Bear.glb" id="2_bearglb"]
[ext_resource type="Script" uid="uid://bsbed05ifr18s" path="res://bear_test_cube.gd" id="3_rog1v"]
[ext_resource type="PackedScene" uid="uid://bdbx0wwa73rhp" path="res://Assets/fx/Bear_FX.glb" id="4_rog1v"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
radius = 1.0
height = 2.4
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hit"]
radius = 1.3
height = 2.7
[node name="Bear" type="CharacterBody3D" unique_id=1573237287]
script = ExtResource("1_beargd")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=830443382]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0)
shape = SubResource("CapsuleShape3D_body")
[node name="HitArea" type="Area3D" parent="." unique_id=365327397]
monitorable = false
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=2111867348]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.35, 0)
shape = SubResource("CapsuleShape3D_hit")
[node name="bear_model" parent="." unique_id=359217823 instance=ExtResource("2_bearglb")]
[node name="Skeleton3D" parent="bear_model/bear-rig" parent_id_path=PackedInt32Array(359217823, 1619621659) index="0" unique_id=1324818366]
bones/1/position = Vector3(0.04601016, 1.9209582, 0.9051311)
bones/1/rotation = Quaternion(0.5270001, -0.011489415, -0.05297156, 0.848135)
bones/2/rotation = Quaternion(0.29015693, 0.002787927, 0.00639222, 0.9569537)
bones/2/scale = Vector3(0.9999999, 1.0000008, 0.99999934)
bones/3/rotation = Quaternion(0.06707396, 0.012082217, 0.011868715, 0.9976043)
bones/4/rotation = Quaternion(0.3487565, 0.010761848, 0.013076123, 0.93706036)
bones/5/rotation = Quaternion(0.15472108, -0.21109715, -0.09227382, 0.96072114)
bones/6/rotation = Quaternion(0.4204113, 0.23492317, -0.11518617, 0.86879086)
bones/7/rotation = Quaternion(0.76830006, 0.36476925, -0.4668917, -0.24222025)
bones/8/rotation = Quaternion(0.5460264, -0.71257937, 0.40870878, -0.16444752)
bones/9/rotation = Quaternion(-0.02719195, -0.14156067, -0.26938534, 0.9521832)
bones/10/rotation = Quaternion(-0.12630412, -0.39785445, -0.057911687, 0.90686566)
bones/11/rotation = Quaternion(0.70209646, -0.35357565, 0.59454584, -0.16899753)
bones/12/rotation = Quaternion(0.5294657, 0.7712517, -0.33475664, -0.1130258)
bones/13/rotation = Quaternion(-0.04075809, 0.17025389, 0.28913936, 0.94114333)
bones/14/rotation = Quaternion(-0.03496992, 0.2568669, -0.0008229511, 0.9658136)
bones/15/rotation = Quaternion(0.9957146, -0.032195006, 0.031220285, 0.08087847)
bones/16/rotation = Quaternion(0.9367782, -0.012506716, 0.033659726, 0.3480767)
bones/17/rotation = Quaternion(-0.860235, -0.08192984, 0.009118035, 0.50319016)
bones/17/scale = Vector3(0.9999998, 1.0000002, 1)
bones/19/rotation = Quaternion(-4.514978e-15, 1, 7.4505806e-08, -5.523351e-07)
bones/20/rotation = Quaternion(0.9729887, -0.014750167, 0.014303936, 0.22993676)
bones/21/rotation = Quaternion(0.9366144, -0.00601367, 0.016095275, 0.34994036)
bones/22/rotation = Quaternion(-0.7732739, -0.06955802, 0.0075612683, 0.6302)
bones/22/scale = Vector3(0.9999997, 0.9999998, 0.9999999)
bones/24/rotation = Quaternion(6.628114e-12, 0.99999994, 5.2154085e-08, -0.00035543842)
bones/25/position = Vector3(1.0117401, 1.3159828, 3.9725027)
bones/25/rotation = Quaternion(-0.27285534, 0.9274522, 0.08017836, 0.2428039)
bones/25/scale = Vector3(1.0000001, 12.140265, 1.0000001)
bones/26/position = Vector3(-0.9025072, 1.4953425, 4.006362)
bones/26/rotation = Quaternion(0.5100001, 0.6936306, 0.15438241, -0.48470873)
bones/26/scale = Vector3(0.99999994, 11.348344, 1)
bones/27/position = Vector3(0.70899034, 1.0356265, 0.611243)
bones/27/rotation = Quaternion(0.7965308, 0.01196337, -0.0119632045, 0.6043613)
bones/27/scale = Vector3(0.99999994, 29.328602, 1)
bones/28/position = Vector3(-0.9047001, 1.0118608, 0.8510791)
bones/28/rotation = Quaternion(0.776662, 0.010376095, -0.010376263, 0.6297467)
bones/28/scale = Vector3(1, 26.535925, 1)
bones/30/position = Vector3(0.07262612, 0.67294323, -1.1644177)
bones/30/rotation = Quaternion(-0.7183768, 0.04937226, -0.02237074, 0.69353926)
bones/31/position = Vector3(5.9604645e-08, -1.4631644, 0.23291233)
bones/31/rotation = Quaternion(1.1641533e-09, 1.8335415e-09, 2.328307e-10, 1)
bones/31/scale = Vector3(0.99999994, 0.9999999, 0.99999994)
bones/32/position = Vector3(-0.0331195, -2.362161, -0.036310136)
bones/32/rotation = Quaternion(0.20497073, -0.020519394, -0.033738203, 0.9779712)
bones/32/scale = Vector3(0.9999999, 0.9999999, 0.99999994)
bones/33/position = Vector3(8.195639e-08, -1.4631642, 0.23291235)
bones/33/rotation = Quaternion(-8.288772e-08, 3.4342527e-09, -4.1909525e-09, 1)
bones/33/scale = Vector3(0.9999998, 0.9999998, 0.99999994)
bones/35/position = Vector3(-7.893884e-09, -1.0594703, 5.2079617e-08)
bones/35/rotation = Quaternion(0.99971783, 1.2737803e-07, 5.365157e-06, -0.023754692)
bones/35/scale = Vector3(0.99999976, 0.9999998, 1.0000012)
bones/39/position = Vector3(0.8199172, 0.25251278, 0.47214246)
bones/39/rotation = Quaternion(0.9961935, -0.035078514, -0.0045541152, 0.07967066)
bones/39/scale = Vector3(1.0000004, 0.9999996, 0.9999991)
bones/40/rotation = Quaternion(0.93738276, 1.2575202e-07, 3.4991626e-08, 0.3483009)
bones/40/scale = Vector3(1.0000004, 1.0000001, 1.0000001)
bones/41/rotation = Quaternion(-0.860235, -0.081929855, 0.009117997, 0.5031899)
bones/41/scale = Vector3(0.99999946, 0.99999976, 0.9999996)
bones/42/position = Vector3(-1.9729964e-07, 0.56145483, 7.283501e-08)
bones/46/scale = Vector3(0.99999946, 0.99999964, 0.9999999)
bones/48/rotation = Quaternion(9.8842406e-08, -0.054423165, -1.786034e-08, 0.998518)
bones/48/scale = Vector3(0.9999993, 0.9999996, 0.9999999)
bones/50/scale = Vector3(0.99999994, 0.99999994, 0.9999998)
bones/51/rotation = Quaternion(-6.30798e-08, 0.026635498, 6.253992e-09, 0.99964523)
bones/51/scale = Vector3(0.9999999, 0.99999964, 0.99999964)
bones/53/position = Vector3(-0.8199191, 0.25251284, 0.47213903)
bones/53/rotation = Quaternion(0.9730908, -0.0186988, -0.0024160824, 0.22964936)
bones/53/scale = Vector3(1, 0.99999875, 0.99999946)
bones/54/rotation = Quaternion(0.9367526, -1.1281111e-07, -3.6940495e-08, 0.34999225)
bones/55/rotation = Quaternion(-0.77327377, -0.06955799, 0.007561261, 0.6302002)
bones/55/scale = Vector3(1.0000002, 1, 1.0000002)
bones/56/position = Vector3(-5.9126133e-08, 0.5614537, 7.70865e-07)
bones/57/rotation = Quaternion(0.70073944, -0.70073956, -0.094679825, -0.09467953)
bones/60/scale = Vector3(1.0000004, 1.0000004, 1.0000002)
bones/62/rotation = Quaternion(5.8764627e-08, -0.042629674, 2.4442278e-08, 0.99909097)
bones/62/scale = Vector3(1.0000002, 1.0000002, 1.0000001)
bones/64/scale = Vector3(1.0000004, 1.0000004, 1)
bones/65/rotation = Quaternion(8.029546e-08, 0.01272784, 4.3857337e-08, 0.999919)
bones/65/scale = Vector3(1.0000002, 1.0000004, 1.0000004)
bones/67/rotation = Quaternion(0.7179022, -7.671307e-07, 7.885642e-07, 0.696144)
bones/67/scale = Vector3(1.0000004, 0.9999986, 0.99999994)
bones/68/rotation = Quaternion(0.9776482, 2.0029378e-07, 1.0460874e-06, -0.2102479)
bones/68/scale = Vector3(1.0000004, 1.0000004, 0.9999982)
bones/69/position = Vector3(-2.4645832e-08, -6.861796e-09, 7.455772e-08)
bones/69/rotation = Quaternion(-0.8315212, 2.5276703e-08, -9.3330055e-09, 0.5554931)
bones/69/scale = Vector3(1, 1.0000001, 1.0000002)
bones/70/rotation = Quaternion(0.67750055, 8.820757e-08, 7.575834e-08, 0.7355223)
bones/70/scale = Vector3(1, 1, 1)
bones/71/scale = Vector3(0.9999999, 1, 0.9999999)
bones/72/rotation = Quaternion(-2.1641608e-07, -4.6565607e-10, -4.6576065e-10, 1)
bones/75/rotation = Quaternion(0.25299338, 0.020847347, 0.028576687, 0.9668212)
bones/76/position = Vector3(1.01587794e-07, -1.5306958e-07, -4.3554866e-07)
bones/76/rotation = Quaternion(-0.5206146, -3.634943e-08, 7.6474e-08, 0.8537919)
bones/77/rotation = Quaternion(0.9373828, 1.2785249e-07, 3.3241214e-08, 0.3483008)
bones/78/position = Vector3(0.8199172, 0.25251257, 0.47214243)
bones/78/rotation = Quaternion(0.99571455, -0.032195, 0.031220287, 0.08087897)
bones/78/scale = Vector3(1.0000004, 0.9999997, 0.99999905)
bones/80/rotation = Quaternion(0.7179022, -7.6713064e-07, 7.885641e-07, 0.696144)
bones/80/scale = Vector3(1.0000004, 0.9999986, 0.99999994)
bones/81/rotation = Quaternion(0.97764814, 1.9846473e-07, 1.0426893e-06, -0.21024783)
bones/81/scale = Vector3(1.0000004, 1.0000002, 0.9999982)
bones/82/position = Vector3(6.103571e-08, 8.8816435e-08, -3.6955274e-09)
bones/82/rotation = Quaternion(-0.8315211, 2.3407136e-08, -1.2821512e-08, 0.5554932)
bones/82/scale = Vector3(0.99999994, 0.99999994, 1)
bones/83/rotation = Quaternion(0.6775009, -8.600083e-08, -8.977273e-08, 0.735522)
bones/83/scale = Vector3(1, 1.0000001, 1)
bones/84/scale = Vector3(0.99999994, 1.0000001, 0.9999999)
bones/85/rotation = Quaternion(2.6833732e-07, -9.313788e-10, -2.5612494e-09, 1)
bones/85/scale = Vector3(1.0000001, 1, 1.0000001)
bones/87/rotation = Quaternion(-0.5230753, 0.20132942, 0.026226493, 0.8277505)
bones/88/rotation = Quaternion(0.10320552, 0.011111915, 0.015231778, 0.99448144)
bones/89/position = Vector3(2.953037e-07, 4.9207458e-08, -4.3015167e-08)
bones/89/rotation = Quaternion(-0.5201237, 5.177172e-08, -1.16139375e-07, 0.8540909)
bones/90/rotation = Quaternion(0.9367527, -1.0975643e-07, -3.3382268e-08, 0.34999216)
bones/91/position = Vector3(-0.8199192, 0.2525128, 0.47213912)
bones/91/rotation = Quaternion(0.9729887, -0.0147501845, 0.014303951, 0.22993667)
bones/91/scale = Vector3(1.0000002, 0.999999, 0.99999964)
bones/96/position = Vector3(-0.016559705, -1.9126627, 0.09830098)
bones/96/rotation = Quaternion(0.10305436, -0.010316663, -0.016962789, 0.99447757)
bones/96/scale = Vector3(0.9999998, 1, 1)
bones/97/rotation = Quaternion(0.997074, -1.2813135e-07, 1.666339e-06, -0.07644201)
bones/98/position = Vector3(0.0013585633, -0.23419222, 0.016214281)
bones/98/rotation = Quaternion(-0.15130657, -0.005602528, -0.008209973, 0.98843694)
bones/98/scale = Vector3(1.0000001, 1.0000006, 1.0000029)
bones/101/position = Vector3(-0.016559973, 1.4684486, 0.06451648)
bones/101/rotation = Quaternion(0.99944556, 0.016124567, -0.011581497, -0.02673253)
bones/101/scale = Vector3(1.0000001, 0.9999993, 0.99999183)
bones/102/rotation = Quaternion(0.95545256, -6.924111e-08, 2.2887033e-07, -0.29514462)
bones/103/position = Vector3(-0.00705543, 0.6694702, -0.06780842)
bones/103/rotation = Quaternion(0.15471841, -0.21109664, -0.09227309, 0.9607217)
bones/103/scale = Vector3(1.0000005, 1.0000286, 1.0000087)
bones/104/rotation = Quaternion(0.420411, 0.23492327, -0.115186565, 0.86879104)
bones/104/scale = Vector3(0.9999993, 0.9999884, 0.9999856)
bones/105/rotation = Quaternion(0.91976035, -0.013162326, 0.0148637965, 0.39197809)
bones/105/scale = Vector3(1, 1.0000005, 1.0000114)
bones/106/position = Vector3(-0.010389687, -0.091948025, 0.0016658108)
bones/106/rotation = Quaternion(0.82734275, 0.15726796, -0.14167279, -0.52028793)
bones/107/rotation = Quaternion(0.77328545, 0.16233337, -0.107321426, 0.6034564)
bones/107/scale = Vector3(0.9999966, 0.99997723, 0.9999995)
bones/108/position = Vector3(-1.1340098e-08, 7.923448e-07, 9.0939814e-08)
bones/108/rotation = Quaternion(0.42463258, 0.043219328, 0.08968367, -0.89987564)
bones/110/rotation = Quaternion(5.443812e-06, 3.6259098e-09, -1.2069942e-06, 1)
bones/110/scale = Vector3(0.99999994, 0.99999976, 1.0000001)
bones/113/position = Vector3(0.08179821, 0.64012206, 0.7106584)
bones/113/rotation = Quaternion(0.7683008, 0.36476624, -0.46689337, -0.24221936)
bones/114/position = Vector3(0.08595797, 0.6463289, -0.32087424)
bones/114/rotation = Quaternion(0.5615046, -0.7056476, 0.3871702, -0.19202453)
bones/114/scale = Vector3(1.0000049, 1.0000027, 1.0000052)
bones/115/rotation = Quaternion(-0.037640397, -0.10443457, -0.2681242, 0.9569672)
bones/115/scale = Vector3(0.9999974, 1.0000036, 0.99999976)
bones/116/rotation = Quaternion(-0.12630361, -0.39785278, -0.057911348, 0.90686655)
bones/116/scale = Vector3(1.0000005, 0.9999972, 1.0000013)
bones/119/scale = Vector3(1.0000043, 0.9999975, 0.99999744)
bones/121/rotation = Quaternion(-2.7027176e-07, -0.20424414, 2.9341666e-07, 0.97892)
bones/121/scale = Vector3(1.0000037, 0.99999774, 0.9999977)
bones/123/scale = Vector3(1.000003, 0.9999977, 0.99999917)
bones/124/rotation = Quaternion(1.2542905e-06, -0.033494003, 5.941339e-07, 0.999439)
bones/124/scale = Vector3(1.0000037, 0.9999978, 0.99999875)
bones/126/rotation = Quaternion(-0.51635414, -0.1681693, 0.6340872, 0.5504825)
bones/126/scale = Vector3(1.0000056, 1.0000031, 1.0000038)
bones/127/rotation = Quaternion(-0.24332374, 0.12663902, 0.6612198, 0.6982439)
bones/127/scale = Vector3(1.0000057, 1.0000007, 1.0000064)
bones/128/position = Vector3(3.0230987e-07, -5.5011895e-07, -6.94335e-08)
bones/128/rotation = Quaternion(0.17027588, 0.93388194, -0.2967173, 0.104064964)
bones/129/rotation = Quaternion(-0.037640374, -0.10443452, -0.26812422, 0.9569671)
bones/131/rotation = Quaternion(-0.54023695, -0.3639527, 0.10910668, 0.7508517)
bones/132/rotation = Quaternion(-0.13163939, 0.9186296, -0.34996635, -0.12772812)
bones/133/rotation = Quaternion(-0.012778445, 0.011854936, 0.00015161023, 0.99984807)
bones/133/scale = Vector3(1.0422997, 1.0422999, 1.0422992)
bones/134/rotation = Quaternion(0.02152282, -2.7759576e-08, -1.5735115e-07, 0.99976844)
bones/134/scale = Vector3(0.98826313, 0.9882641, 0.9882627)
bones/135/position = Vector3(0.08595812, 0.646329, -0.3208742)
bones/135/rotation = Quaternion(0.5460276, -0.7125794, 0.40870702, -0.16444758)
bones/135/scale = Vector3(1.000007, 1.0000004, 1.0000051)
bones/137/position = Vector3(-0.09762837, 0.6401221, 0.7106583)
bones/137/rotation = Quaternion(0.7020965, -0.35357338, 0.5945473, -0.16899663)
bones/138/position = Vector3(-0.08595776, 0.646329, -0.3208746)
bones/138/rotation = Quaternion(0.5492876, 0.7627154, -0.30113, -0.16083841)
bones/138/scale = Vector3(1.0000051, 1.0000018, 1.0000046)
bones/139/rotation = Quaternion(-0.058682628, 0.11130926, 0.28603986, 0.94991994)
bones/139/scale = Vector3(0.9999983, 1.0000024, 0.9999992)
bones/140/rotation = Quaternion(-0.034969915, 0.25686622, -0.0008231461, 0.96581376)
bones/140/scale = Vector3(1.0000031, 0.9999977, 0.9999988)
bones/143/scale = Vector3(1.000005, 0.99999815, 0.9999967)
bones/145/rotation = Quaternion(-4.0431777e-07, 0.1295653, -5.1404834e-07, 0.99157095)
bones/145/scale = Vector3(1.0000045, 0.99999803, 0.9999968)
bones/147/scale = Vector3(1.0000035, 0.99999845, 0.9999981)
bones/148/rotation = Quaternion(1.9343124e-06, 0.025161995, -3.222267e-07, 0.9996834)
bones/148/scale = Vector3(1.000004, 0.9999991, 0.999998)
bones/150/rotation = Quaternion(-0.5163542, 0.16816999, -0.6340877, 0.5504816)
bones/150/scale = Vector3(1.0000052, 1.0000039, 1.0000025)
bones/151/rotation = Quaternion(-0.24332343, -0.1266389, -0.6612192, 0.6982446)
bones/151/scale = Vector3(1.0000049, 1.0000005, 1.0000063)
bones/152/position = Vector3(-2.0525843e-07, -4.4378166e-07, 2.2242949e-07)
bones/152/rotation = Quaternion(-0.1980554, 0.9486651, -0.20058647, -0.14343536)
bones/153/rotation = Quaternion(-0.058682676, 0.11130926, 0.2860399, 0.94991994)
bones/155/rotation = Quaternion(-0.48170745, 0.21343125, -0.09362694, 0.84477156)
bones/155/scale = Vector3(0.99999994, 1, 0.9999998)
bones/156/rotation = Quaternion(0.2123156, 0.9450362, -0.15822437, 0.19181721)
bones/157/rotation = Quaternion(-0.01277751, -0.011854968, -0.0001513001, 0.9998481)
bones/157/scale = Vector3(1.0423003, 1.0422994, 1.0423005)
bones/158/rotation = Quaternion(0.021521209, -2.5687226e-07, -2.570953e-07, 0.9997684)
bones/158/scale = Vector3(0.9882631, 0.98826426, 0.9882618)
bones/159/position = Vector3(-0.085957706, 0.6463291, -0.32087478)
bones/159/rotation = Quaternion(0.5294669, 0.7712517, -0.33475488, -0.1130262)
bones/159/scale = Vector3(1.0000075, 1.0000001, 1.0000042)
bones/161/position = Vector3(0.08179821, 0.64012206, 0.7106584)
bones/161/rotation = Quaternion(0.7683008, 0.36476624, -0.46689337, -0.24221936)
bones/162/position = Vector3(-0.09762837, 0.6401221, 0.7106583)
bones/162/rotation = Quaternion(0.7020965, -0.35357338, 0.5945473, -0.16899663)
bones/165/position = Vector3(-1.5017577e-08, 0.26895812, -1.6243575e-08)
bones/165/rotation = Quaternion(-0.019247467, -0.20323175, 0.97894067, -0.0012537595)
bones/166/position = Vector3(0.016703974, -2.2814677, 2.0323193)
bones/167/position = Vector3(0.7698192, 2.760036, -1.7026894)
bones/167/rotation = Quaternion(-0.93479383, -0.013274327, 0.31412983, 0.16524744)
bones/167/scale = Vector3(0.9999947, 0.9999997, 1.0000055)
bones/168/position = Vector3(0.18381567, -0.9622623, 0.9894084)
bones/168/rotation = Quaternion(0.93622446, 1.3019375e-07, -0.34127164, -0.08377019)
bones/168/scale = Vector3(1.0000024, 1.0000011, 1.0000095)
bones/170/position = Vector3(0.748137, 0.69556385, 3.7252903e-09)
bones/170/rotation = Quaternion(0.019250846, -0.20323181, 0.9789406, 0.001253055)
bones/171/position = Vector3(0.381392, -2.057497, 2.0762825)
bones/172/position = Vector3(-0.9278099, 2.7545004, -1.5181485)
bones/172/rotation = Quaternion(-0.91823035, 0.06278634, -0.38804358, 0.04830265)
bones/172/scale = Vector3(0.99999493, 0.9999995, 1.0000055)
bones/173/position = Vector3(0.18381546, -0.96226203, 0.9894083)
bones/173/rotation = Quaternion(0.9362244, 3.755922e-07, 0.3412715, -0.0837717)
bones/173/scale = Vector3(1.0000001, 1.0000001, 0.9999949)
bones/183/position = Vector3(-1.982537e-07, 5.9604563e-08, -1.517833e-08)
bones/185/rotation = Quaternion(5.4077844e-07, -0.20349081, 0.97907686, -1.12395135e-07)
bones/186/position = Vector3(0.78680634, 2.8457582, -0.41063747)
bones/186/rotation = Quaternion(-0.9983755, 0.019092962, -0.050729975, -0.017562928)
bones/186/scale = Vector3(0.9999997, 0.99999994, 1)
bones/187/rotation = Quaternion(0.15451065, -2.0305349e-09, -2.0850066e-09, 0.9879911)
bones/187/scale = Vector3(1.0000002, 0.99999994, 0.9999999)
bones/197/position = Vector3(7.903883e-08, 1.4901151e-07, 8.8932055e-08)
bones/199/rotation = Quaternion(0.00034800146, -0.20349097, 0.9790768, -7.2328476e-05)
bones/200/position = Vector3(-0.84339374, 2.905353, -0.57784474)
bones/200/rotation = Quaternion(-0.9983755, 0.019092962, -0.050729975, -0.017562928)
bones/200/scale = Vector3(0.9999997, 0.99999994, 1)
bones/201/rotation = Quaternion(0.15451066, -1.9585848e-09, -2.5450761e-09, 0.9879911)
bones/201/scale = Vector3(1.0000002, 0.99999994, 0.99999994)
[node name="test_cube" type="Node3D" parent="." unique_id=646988893]
script = ExtResource("3_rog1v")
[node name="Bear_FX" parent="test_cube" unique_id=1183391245 instance=ExtResource("4_rog1v")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.1459732, 0, -1.528423)
[node name="Bear_fx_claw_attack_R" parent="test_cube/Bear_FX" index="1" unique_id=941189652]
transform = Transform3D(0.26165774, 0.06970292, -0.23443498, -0.046572562, 0.18028688, 0.0016228362, 0.22878301, 0.056650188, 0.2721929, -0.40204993, 0.29855162, 0.917461)
[editable path="bear_model"]
[editable path="test_cube/Bear_FX"]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17 -3
View File
@@ -12,13 +12,25 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen
| Path | Purpose | | Path | Purpose |
|---|---| |---|---|
| `scene.tscn` | Main scene (arena + player) | | `scene.tscn` | The reusable **shell** loaded for every level: player, camera, HUD, PS1 filter, and an empty `LevelRoot`. Its `game_shell.gd` instances the active level's module into `LevelRoot` at runtime |
| `levels/` | The level system: per-level module scenes plus the code that defines, loads and sequences them (see rows below) |
| `levels/game_shell.gd` | Shell controller — on load, frees any current level module and instances `Run.active_level().level_scene` into `LevelRoot`. Swapping the whole subtree (never hiding it) is what keeps one level's meshes/colliders/spawners from leaking into the next |
| `levels/arena_level.tscn` | The arena **level module**: floor + wall colliders, arena meshes, `MatadorSpawner` (with child `Node3D` spawn points placed by the designer), lighting, audience (`PublikNode`). Instanced into the shell's `LevelRoot` |
| `levels/bear_level.tscn` | The Bear's Den **level module**: bear-arena mesh, its own floor collider, lighting, and a `BearSpawner` (`bear_spawn.gd`) that places the bear at center. Instanced into the shell's `LevelRoot` |
| `Player.tscn` | Player scene root (`CharacterBody3D`) | | `Player.tscn` | Player scene root (`CharacterBody3D`) |
| `player.gd` | Movement, charge, turning logic | | `player.gd` | Movement, charge, turning logic |
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) | | `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) | | `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
| `matador.gd` | Matador AI (wander / ragdoll state machine) | | `matador.gd` | Matador AI (wander / ragdoll state machine) |
| `matador_spawn.gd` | Spawns N matadors at random arena positions | | `levels/matador_spawn.gd` | Arena spawner — one matador per designer-placed child `Node3D` spawn point (each enters with a jump-roll toward center); falls back to a programmatic spread if no spawn points are present. Also handles the split-on-death hydra logic |
| `levels/bear_spawn.gd` | Bear-level spawner — instantiates `enemy_scene` at the arena center; emits the same `all_defeated` / `matador_killed` signals as `matador_spawn.gd` |
| `Bear.tscn` / `bear.gd` | Dark-Souls-style bear boss (`CharacterBody3D`). Routine state machine (wind-up → active → recovery) picking telegraphed moves by range band: Double Swipe / Overhead Smash (close), Leap Slam (mid-far), plus Stalk (2-leg walk-in) / Prowl (4-leg lope) positioning. Melee steps in during the swing (`bear_lunge_speed`) + tracks the bull; the leap is frame-synced to JUMP_SMASH so the paws hit ground on the impact frame (`bear_leap_impact_frame`); slams kick up dust + shake. Combos chain, hyper-armour mid-swing + poise (`bear_stagger_cd`), phase 2 enrage under `bear_enrage_frac` HP. Never damages the bull by colliding — only swings/slams. Joins `&"matador"` so bull abilities already hit it. Tuning under the `Bear` DP section |
| `levels/run_state.gd` | Run progression autoload (`Run`) — Slay-the-Spire map + player position across fights |
| `levels/level_def.gd` | `LevelDef` resource — a level's name / map colour / `scene_path` (always the shell) / `level_scene` (the level module instanced into the shell) / optional `enemy_scene` (arena wave vs. a boss like the bear) |
| `levels/map_node.gd` | `MapNode` — one runtime node on the run map (position, level, links) |
| `levels/MapScreen.tscn` / `levels/map_screen.gd` | Between-fights map screen; win → pick a node → next level |
| `crowd_marker.gd` | `CrowdMarker` (`@tool Node3D`, `class_name`) — designer drops one per billboard spectator via the editor's Add-Node dialog; picks one of six 60°-spaced facings from the `orientation` dropdown. Draws an editor-only preview (slab + forward arrow, never serialised) and joins the `&"crowd_marker"` group |
| `crowd_billboards.gd` / `crowd_billboard.gdshader` | `MultiMeshInstance3D` that harvests every `CrowdMarker` (group `&"crowd_marker"`) into one draw call, using each marker's world position + chosen facing. Front/back sprite sheet baked by `tools/bake_crowd_sheet.gd`; falls back to a generated inward-facing ring when no markers are placed |
| `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) | | `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) |
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) | | `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
| `Blender/` | Blender source files, animation scripts, FBX exports | | `Blender/` | Blender source files, animation scripts, FBX exports |
@@ -88,10 +100,12 @@ After FBX import, animations appear as `Armature|walk` and `Armature|idle` in An
bash run_tests.sh bash run_tests.sh
# Individual tests: # Individual tests:
gdlint *.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv) gdlint *.gd levels/*.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
godot --headless --script tests/logic_test.gd # pure logic, ~2 s godot --headless --script tests/logic_test.gd # pure logic, ~2 s
godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
godot --headless --script tests/level_switch_test.gd # no arena geometry leaks into the bear level, ~2 s
godot --headless --script tests/bear_boss_test.gd # bear boss moveset: routines, leap landing, hyper-armour, ~10 s
# Gameplay test with real rendering (inspect frame strip visually): # Gameplay test with real rendering (inspect frame strip visually):
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
@@ -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]
+3 -2
View File
@@ -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]
+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) 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") 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/position = Vector3(0.056660548, 0.6424767, -0.0167365)
bones/0/rotation = Quaternion(0.013179442, -0.2660563, 0.073319815, 0.9610747) bones/0/rotation = Quaternion(0.013179442, -0.2660563, 0.073319815, 0.9610747)
bones/1/rotation = Quaternion(0.06234612, 0.049236443, 0.08183311, 0.99347484) 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/24/rotation = Quaternion(0.08874816, -0.018719967, -0.04671622, 0.99478185)
bones/25/rotation = Quaternion(0.09634846, -0.0268489, -0.04050248, 0.9941608) 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) 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 cast_shadow = 0
[node name="Camera3D" type="Camera3D" parent="." unique_id=1531255459] [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="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="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"] [sub_resource type="ShaderMaterial" id="mat_blur"]
shader = ExtResource("3_blur") shader = ExtResource("3_blur")
@@ -74,7 +74,6 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
mouse_filter = 2 mouse_filter = 2
ratio = 1.7778 ratio = 1.7778
stretch_mode = 2
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="VideoAspect" unique_id=374854265] [node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="VideoAspect" unique_id=374854265]
layout_mode = 2 layout_mode = 2
@@ -281,7 +280,7 @@ bbcode_enabled = true
text = "[center][font_size=32][color=#d9a833]Bullosseum[/color][/font_size] 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] [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] [b][color=#d9a833]Richard Aasa[/color][/b] [url=https://instagram.com/aasarichard]@aasarichard[/url]
[font_size=20]Programmer[/font_size] [font_size=20]Programmer[/font_size]
+32 -19
View File
@@ -4,30 +4,17 @@
[ext_resource type="Script" uid="uid://cpyq2r5cq653g" path="res://camera_iso.gd" id="2_camera_iso"] [ext_resource type="Script" uid="uid://cpyq2r5cq653g" path="res://camera_iso.gd" id="2_camera_iso"]
[ext_resource type="PackedScene" uid="uid://bun14xmk20pcj" path="res://Assets/bull.fbx" id="4_e80uo"] [ext_resource type="PackedScene" uid="uid://bun14xmk20pcj" path="res://Assets/bull.fbx" id="4_e80uo"]
[sub_resource type="SphereShape3D" id="SphereShape3D_chest"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_kne1u"]
radius = 0.35 radius = 0.7495117
height = 1.845398
[sub_resource type="SphereShape3D" id="SphereShape3D_belly"]
radius = 0.4
[sub_resource type="SphereShape3D" id="SphereShape3D_hips"]
radius = 0.3
[node name="Player" type="CharacterBody3D" unique_id=165510044] [node name="Player" type="CharacterBody3D" unique_id=165510044]
collision_layer = 3 collision_layer = 3
script = ExtResource("1_xhfnw") script = ExtResource("1_xhfnw")
[node name="CollisionChest" type="CollisionShape3D" parent="." unique_id=447032296] [node name="CollisionBull" type="CollisionShape3D" parent="." unique_id=571780801]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, -0.2) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.29183745, 0.3197594)
shape = SubResource("SphereShape3D_chest") shape = SubResource("CapsuleShape3D_kne1u")
[node name="CollisionBelly" type="CollisionShape3D" parent="." unique_id=571780801]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.1)
shape = SubResource("SphereShape3D_belly")
[node name="CollisionHips" type="CollisionShape3D" parent="." unique_id=717874116]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.4)
shape = SubResource("SphereShape3D_hips")
[node name="Camera3D" type="Camera3D" parent="." unique_id=1120161087] [node name="Camera3D" type="Camera3D" parent="." unique_id=1120161087]
script = ExtResource("2_camera_iso") script = ExtResource("2_camera_iso")
@@ -35,4 +22,30 @@ script = ExtResource("2_camera_iso")
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")] [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) 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, 1942410469) index="0" unique_id=550138996]
bones/0/position = Vector3(0, 0, 0.009963839)
bones/0/rotation = Quaternion(0.7071068, 0, 0, 0.7071067)
bones/0/scale = Vector3(1, 1, 1)
bones/1/position = Vector3(0.003526625, -0.0043296535, 0.0040827994)
bones/1/rotation = Quaternion(-0.00038647873, 0.06909673, 0.9868542, -0.14609687)
bones/2/rotation = Quaternion(-0.06929122, 3.2175163e-07, -0.112670615, 0.99121344)
bones/4/position = Vector3(0.0045358227, -0.002137695, -0.004238624)
bones/4/rotation = Quaternion(-0.24948503, 0.19590352, 0.9416978, -0.11217985)
bones/4/scale = Vector3(1.0000001, 0.9999999, 1.0000001)
bones/5/rotation = Quaternion(-0.58943665, -0.3660725, -0.22564478, 0.683842)
bones/5/scale = Vector3(1, 1.0000001, 0.99999994)
bones/6/rotation = Quaternion(0.52910197, 0.15997827, 0.120245, 0.8246207)
bones/6/scale = Vector3(1, 1, 1.0000001)
bones/17/position = Vector3(0, 0.0009373814, 0.007552617)
bones/17/rotation = Quaternion(-9.4156974e-08, 0.7071068, 0.7071067, -9.4156974e-08)
bones/19/position = Vector3(-0.003526625, -0.0043296535, 0.0040827994)
bones/19/rotation = Quaternion(0.00038647873, 0.06909673, 0.9868542, 0.14609687)
bones/20/rotation = Quaternion(-0.06929122, -3.2227587e-07, 0.11267061, 0.99121344)
bones/22/position = Vector3(-0.0045358227, -0.002137695, -0.004238624)
bones/22/rotation = Quaternion(0.24948503, 0.19590352, 0.9416978, 0.11217985)
bones/23/rotation = Quaternion(-0.58943665, 0.3660725, 0.22564478, 0.683842)
bones/23/scale = Vector3(1, 1.0000001, 1)
bones/24/rotation = Quaternion(0.529102, -0.15997836, -0.120245025, 0.82462066)
bones/24/scale = Vector3(1, 1.0000001, 1)
[editable path="bull"] [editable path="bull"]
+1 -1
View File
@@ -8,5 +8,5 @@ script="rider-plugin.gd"
[presets] [presets]
active="off" active="on"
presets="presets.json" presets="presets.json"
+935
View File
@@ -0,0 +1,935 @@
extends CharacterBody3D
## Dark-Souls-style boss bear with a learnable moveset. From NEUTRAL it weighs its
## options by the range band to the bull and commits to one telegraphed routine —
## wind-up (tell) → active (the hit) → recovery (the player's punish window). A routine
## always plays to the end (hyper-armour: it takes damage mid-swing but isn't knocked out
## of it), so every attack is readable and punishable. Variety comes from which routine
## NEUTRAL picks (weighted per range band); predictability from each routine being fixed
## once entered. Below bear_enrage_frac HP it enters phase 2: snappier tells/recovery and
## longer combo chains.
##
## The bear only wounds the bull with its swings and slams — walking or leaping into the
## bull does nothing. It plugs into the bull's combat interface: it joins the &"matador"
## group so every bull ability already targets it (apply_ability_hit), and mauls the bull
## via take_sword_hit.
# Positioning / neutral states, then the three attack routines, then reactions.
enum State { NEUTRAL, STALK, PROWL, SWIPE, SMASH, LEAP, STAGGER, DEAD }
# Every attack routine runs through these three beats.
enum Phase { WINDUP, ACTIVE, RECOVER }
signal killed
signal health_changed(current: int, max_hp: int)
var _state: State = State.NEUTRAL
var _phase: Phase = Phase.WINDUP
var _bull: CharacterBody3D = null
var _skeleton: Skeleton3D = null
var _anim_player: AnimationPlayer = null
var _blood_burst: CPUParticles3D = null
var _hp: int = 10
var _enraged: bool = false
var _phase_timer: float = 0.0
var _active_len: float = 0.8
var _think_timer: float = 0.0
var _state_timer: float = 0.0
var _stagger_timer: float = 0.0
var _stagger_cd: float = 0.0 # poise window after a stagger during which hits don't stun
var _hits_done: int = 0
var _leap_airborne: bool = false
# Solved per-leap so the arc lands on the clip's impact frame: the launch vertical speed
# and the custom gravity used only while airborne (so peak height and airtime are tuned
# independently of world gravity).
var _leap_up: float = 0.0
var _leap_grav: float = 0.0
# Attack routines queued to fire back-to-back as a combo, popped when the current one
# finishes its recovery. Element type is State.
var _combo_queue: Array = []
# Resolved at runtime from the rig's clip list — the Bear.glb ships Rigify-named clips
# (RUN_ON4LEGS, IDLE_standing, SMASH_HIT, …) that may import with any prefix, so we match
# by substring rather than hard-coding the full track names.
var _anim_idle: StringName = &"" # IDLE_ON4LEGS — neutral / recovery (the punish pose)
var _anim_run: StringName = &"" # RUN_ON4LEGS — prowl approach / neutral coast
var _anim_walk2: StringName = &"" # WALK_ON2LEGS — the 2-leg stalk-in
var _anim_stand: StringName = &"" # IDLE_standing — reared-up wind-up / roar
var _anim_swipe: StringName = &"" # ONE_TWO_HIT — double swipe
var _anim_smash: StringName = &"" # SMASH_HIT — overhead smash
var _anim_leap: StringName = &"" # JUMP_SMASH — leap slam
var _anim_death: StringName = &"" # DEATH — death
#take hit flashing
var _hit_flash_active := false
var _hit_flash_tween: Tween
const _ACCEL_FACTOR: float = 9.0
# Hard deceleration for planted states (wind-up / recovery / neutral settle) so the bear
# never coasts across the ground with a stationary pose playing (no foot-sliding).
const _PLANT_DECEL: float = 22.0
# The Bear.glb clips are authored at 30 fps (see Bear.glb.import); leap timing is keyed by
# frame, so convert frames→seconds with this.
const _ANIM_FPS: float = 30.0
# How close the bear closes on the bull before it stops advancing (roughly the bear+bull
# body radii). Its swings still reach well past this; it just never tries to occupy the
# bull's spot, so it doesn't shove up onto it.
const _BULL_STANDOFF: float = 1.9
@onready var _mesh: Node3D = $bear_model
@onready var _hit_area: Area3D = $HitArea
var _dust: CPUParticles3D = null
# Claw-slash bursts bound to the paw bones, fired one per hit of the 1-2 punch.
var _claw_l: CPUParticles3D = null
var _claw_r: CPUParticles3D = null
const RigidSkin = preload("res://rigid_skin.gd")
func _ready() -> void:
add_to_group(&"matador") # so every existing bull ability already targets the bear
add_to_group(&"bear")
_hp = maxi(1, int(DP.f("bear_max_hp")))
health_changed.emit(_hp, _hp)
_skeleton = _find_skeleton(_mesh)
_anim_player = _find_anim_player(_mesh)
# Web (Mali/ANGLE) can't run Compatibility vertex-skinning; rebuild the bear as
# non-skinned bone-attached pieces there. Desktop keeps smooth GPU skinning.
if OS.has_feature("web"):
RigidSkin.convert_tree(self)
if _anim_player:
_anim_idle = _resolve_anim(["IDLE_ON4LEGS", "IDLE"])
_anim_run = _resolve_anim(["RUN_ON4LEGS", "RUN"])
_anim_walk2 = _resolve_anim(["WALK_ON2LEGS", "WALK", "IDLE_standing"])
_anim_stand = _resolve_anim(["IDLE_standing", "WALK_ON2LEGS"])
_anim_swipe = _resolve_anim(["ONE_TWO_HIT", "SMASH_HIT"])
_anim_smash = _resolve_anim(["SMASH_HIT", "ONE_TWO_HIT"])
_anim_leap = _resolve_anim(["JUMP_SMASH", "SMASH_HIT"])
_anim_death = _resolve_anim(["DEATH"])
# Loop the locomotion / idle clips; the attack clips (swipe/smash/leap) are one-shot,
# so their on-ground settle frames play once and then hold instead of looping.
for a in [_anim_idle, _anim_run, _anim_walk2, _anim_stand]:
_ensure_loop(a)
for a in [_anim_swipe, _anim_smash, _anim_leap, _anim_death]:
_set_no_loop(a)
_anim_player.playback_default_blend_time = 0.12
_hit_area.body_entered.connect(_on_body_entered)
_blood_burst = preload("res://blood_burst.gd").new()
add_child(_blood_burst)
_dust = _make_slam_dust()
add_child(_dust)
_setup_claws()
_enter_neutral()
#take hit flashing
func _hit_flash() -> void:
if _mesh == null:
return
if _hit_flash_tween:
_hit_flash_tween.kill()
_hit_flash_active = true
# Make the bear red
for child in _mesh.find_children("*", "MeshInstance3D", true, false):
var mesh := child as MeshInstance3D
if mesh:
mesh.material_overlay = _make_hit_overlay()
# Flash for 0.1 sec, then remove
_hit_flash_tween = create_tween()
_hit_flash_tween.tween_interval(0.10)
_hit_flash_tween.tween_callback(_clear_hit_flash)
func _make_hit_overlay() -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.05, 0.05, 1.0)
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
return mat
func _clear_hit_flash() -> void:
if _mesh == null:
return
for child in _mesh.find_children("*", "MeshInstance3D", true, false):
var mesh := child as MeshInstance3D
if mesh:
mesh.material_overlay = null
_hit_flash_active = false
# Bind a claw-slash emitter to each paw bone (hand_l / hand_r) via a BoneAttachment3D, so
# a burst fired on a swipe hit erupts from that paw wherever the animation has it.
func _setup_claws() -> void:
if _skeleton == null:
return
_claw_r = _attach_claw("hand_r")
_claw_l = _attach_claw("hand_l")
func _attach_claw(bone: String) -> CPUParticles3D:
if _skeleton.find_bone(bone) == -1:
return null
var attach := BoneAttachment3D.new()
attach.bone_name = bone
_skeleton.add_child(attach)
var claw := _make_claw_slash()
attach.add_child(claw)
return claw
func get_hp() -> int:
return _hp
func _acquire_bull() -> void:
if _bull != null:
return
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
_bull = players[0] as CharacterBody3D
# The bear only ever hits via distance / area checks, never body collision, so let
# it pass through the bull. Otherwise the big capsule rides up and gets stuck on top
# of the bull when it lunges or lands a leap on it.
if _bull != null:
add_collision_exception_with(_bull)
func _physics_process(delta: float) -> void:
if not is_on_floor():
# The leap uses its own gravity mid-flight so its arc height and airtime can be
# tuned to land on the clip's impact frame; everything else falls under world gravity.
if _state == State.LEAP and _phase == Phase.ACTIVE and _leap_grav > 0.0:
velocity.y -= _leap_grav * delta
else:
velocity += get_gravity() * delta
if _state == State.DEAD:
velocity.x = 0.0
velocity.z = 0.0
move_and_slide()
return
_acquire_bull()
_update_enrage()
_stagger_cd = maxf(0.0, _stagger_cd - delta)
match _state:
State.NEUTRAL: _tick_neutral(delta)
State.STALK: _tick_stalk(delta)
State.PROWL: _tick_prowl(delta)
State.SWIPE: _tick_swipe(delta)
State.SMASH: _tick_smash(delta)
State.LEAP: _tick_leap(delta)
State.STAGGER: _tick_stagger(delta)
move_and_slide()
# ── Phase 2 ──────────────────────────────────────────────────────────────────────
func _update_enrage() -> void:
if _enraged:
return
if _hp <= int(ceil(DP.f("bear_max_hp") * DP.f("bear_enrage_frac"))):
_enraged = true
# Roar: a beat of stagger-free wind-up with a screen shake, so the phase flip reads.
_blood_burst.burst(global_position + Vector3(0.0, 1.6, 0.0), Vector3.UP)
_shake(0.9)
# Timers that shape readability (tells + recovery) shrink in phase 2; active windows and
# the clip-locked leap timings are left alone.
func _scaled(t: float) -> float:
return t * (DP.f("bear_enrage_scale") if _enraged else 1.0)
# ── NEUTRAL: think, then commit ────────────────────────────────────────────────────
func _enter_neutral() -> void:
_state = State.NEUTRAL
# A short, lightly-varied beat so the bear flows from one action into the next; phase 2
# shrinks it further. Keep the jitter small so it reads as eager, not hesitant.
var think := DP.f("bear_think_time")
_think_timer = _scaled(think * randf_range(0.7, 1.3))
func _tick_neutral(delta: float) -> void:
_decelerate(_PLANT_DECEL, delta)
if _bull != null:
_face_point(_bull.global_position, delta)
# Coast on the run clip while residual speed bleeds off, then settle to idle — never
# hold the idle pose while still sliding.
_play(_anim_run if _horizontal_speed() > 1.0 else _anim_idle)
_think_timer -= delta
if _think_timer <= 0.0:
_decide()
# Pop the next queued combo step, or pick a fresh routine weighted by the current range
# band. Weights lean into pressure (and chaining) in phase 2.
func _decide() -> void:
if not _combo_queue.is_empty():
var next: State = _combo_queue.pop_front()
if _range_allows(next):
_start_routine(next)
return
_combo_queue.clear()
if _bull == null:
_start_routine(State.PROWL)
return
var dist := _flat_dist_to_bull()
var options: Array
if dist <= DP.f("bear_close_range"):
# In strike range: punch or overhead (both step in during the swing).
options = [[State.SWIPE, 3.0], [State.SMASH, 3.0 if _enraged else 2.5]]
elif dist <= DP.f("bear_mid_range"):
# Mid: mostly walk the bull down on two legs; occasionally leap the gap.
options = [[State.STALK, 3.0], [State.LEAP, 2.0 if _enraged else 1.5]]
else:
# Far: lope in on all fours, or spring across with a leap.
options = [[State.PROWL, 3.0], [State.LEAP, 1.5]]
_start_routine(_weighted_pick(options))
# A queued combo step only fires if the bull is still roughly in its band, so a chain
# doesn't whiff into empty air after the player rolls away.
func _range_allows(routine: State) -> bool:
if _bull == null:
return routine == State.PROWL
var dist := _flat_dist_to_bull()
match routine:
State.SWIPE, State.SMASH:
return dist <= DP.f("bear_close_range") * 1.4
State.LEAP:
return dist <= DP.f("bear_mid_range") * 1.5
return true
func _start_routine(routine: State) -> void:
match routine:
State.SWIPE: _start_swipe()
State.SMASH: _start_smash()
State.LEAP: _start_leap()
State.STALK: _start_stalk()
State.PROWL: _start_prowl()
_: _enter_neutral()
# When a routine's recovery ends: fire the next combo step if one is queued, else think.
func _finish_routine() -> void:
if not _combo_queue.is_empty():
_decide()
else:
_enter_neutral()
# ── STALK: a menacing 2-leg walk-in that closes the gap into strike range ────────────
func _start_stalk() -> void:
_state = State.STALK
_state_timer = DP.f("bear_stalk_time")
func _tick_stalk(delta: float) -> void:
_state_timer -= delta
if _bull == null or _state_timer <= 0.0 or _flat_dist_to_bull() <= DP.f("bear_close_range"):
_enter_neutral()
return
var to_bull := _flat_to_bull()
_accelerate(to_bull.normalized(), DP.f("bear_stalk_speed"), delta)
_face_point(_bull.global_position, delta)
_play(_anim_walk2)
# ── PROWL: lope in on all fours to close a big gap ─────────────────────────────────
func _start_prowl() -> void:
_state = State.PROWL
_state_timer = 1.6
func _tick_prowl(delta: float) -> void:
_state_timer -= delta
if _bull == null or _state_timer <= 0.0 or _flat_dist_to_bull() <= DP.f("bear_mid_range") * 0.9:
_enter_neutral()
return
var to_bull := _flat_to_bull()
_accelerate(to_bull.normalized(), DP.f("bear_run_speed"), delta)
_face_point(_bull.global_position, delta)
_play(_anim_run)
# ── SWIPE: Double Swipe (ONE_TWO_HIT), two quick hits ──────────────────────────────
func _start_swipe() -> void:
_state = State.SWIPE
_phase = Phase.WINDUP
_phase_timer = _scaled(DP.f("bear_swipe_windup"))
_hits_done = 0
_play(_anim_stand)
# Sometimes chain the big smash after the swipe (always in phase 2) — the signature
# close string the player learns to bait and punish.
if _combo_queue.is_empty() and (_enraged or randf() < DP.f("bear_combo_chance")):
_combo_queue.append(State.SMASH)
func _tick_swipe(delta: float) -> void:
match _phase:
Phase.WINDUP:
_decelerate(_PLANT_DECEL, delta) # rear up in place — no slide
_face_point_if_bull(delta) # track hard before committing
_play(_anim_stand)
_phase_timer -= delta
if _phase_timer <= 0.0:
_begin_attack_active(_anim_swipe, 0.8)
Phase.ACTIVE:
_lunge_and_track(DP.f("bear_lunge_speed"), delta)
_phase_timer -= delta
var frac := 1.0 - _phase_timer / maxf(_active_len, 0.001)
if _hits_done == 0 and frac >= 0.25:
_hits_done = 1
_try_hit(DP.f("bear_swipe_reach"))
_claw_slash(true) # first paw (right) rakes across
elif _hits_done == 1 and frac >= 0.65:
_hits_done = 2
_try_hit(DP.f("bear_swipe_reach"))
_claw_slash(false) # back-hand with the left paw
if _phase_timer <= 0.0:
_begin_recover(DP.f("bear_swipe_recover"))
Phase.RECOVER:
_tick_recover(delta)
# ── SMASH: Overhead Smash (SMASH_HIT), slower tell, big punish window ────────────────
func _start_smash() -> void:
_state = State.SMASH
_phase = Phase.WINDUP
_phase_timer = _scaled(DP.f("bear_smash_windup"))
_hits_done = 0
_play(_anim_stand)
func _tick_smash(delta: float) -> void:
match _phase:
Phase.WINDUP:
_decelerate(_PLANT_DECEL, delta)
_face_point_if_bull(delta)
_play(_anim_stand)
_phase_timer -= delta
if _phase_timer <= 0.0:
_begin_attack_active(_anim_smash, 0.9)
Phase.ACTIVE:
# The overhead steps in less than the swipe — it's a commitment, not a rush.
_lunge_and_track(DP.f("bear_lunge_speed") * 0.5, delta)
_phase_timer -= delta
var frac := 1.0 - _phase_timer / maxf(_active_len, 0.001)
if _hits_done == 0 and frac >= 0.4:
_hits_done = 1
_slam(DP.f("bear_smash_radius"), 0.8)
if _phase_timer <= 0.0:
_begin_recover(DP.f("bear_smash_recover"))
Phase.RECOVER:
_tick_recover(delta)
# Shared wind-up → active handoff for the melee swings: start the clip (sped up for snappy
# punches) and size the active window to the sped-up clip length.
func _begin_attack_active(clip: StringName, fallback_len: float) -> void:
_phase = Phase.ACTIVE
var spd := maxf(DP.f("bear_attack_anim_speed"), 0.1)
_active_len = _anim_length(clip, fallback_len) / spd
_phase_timer = _active_len
_play_attack(clip, spd)
# ── LEAP: Leap Slam (JUMP_SMASH) — clip-synced ballistic gap-closer + AoE ────────────
# The one JUMP_SMASH clip runs start-to-finish across the whole routine: it plays the
# crouch on the ground (wind-up), springs at the takeoff frame, the arc is solved so the
# paws hit ground exactly on the impact frame, and the last frames settle on the ground.
func _start_leap() -> void:
_state = State.LEAP
_phase = Phase.WINDUP
_hits_done = 0
_leap_airborne = false
_leap_grav = 0.0
_active_len = _anim_length(_anim_leap, 1.0)
_play_attack(_anim_leap, 1.0) # play from frame 0 at real speed, grounded crouch
_phase_timer = DP.f("bear_leap_takeoff_frame") / _ANIM_FPS # ground time before the spring
# In phase 2 a leap sometimes follows straight into a swipe once it lands close.
if _enraged and _combo_queue.is_empty() and randf() < 0.5:
_combo_queue.append(State.SWIPE)
func _tick_leap(delta: float) -> void:
match _phase:
Phase.WINDUP:
_decelerate(_PLANT_DECEL, delta) # crouch in place
_face_point_if_bull(delta) # aim the pounce, tracking the bull
_phase_timer -= delta
if _phase_timer <= 0.0:
_launch_leap()
Phase.ACTIVE:
_phase_timer -= delta
if not is_on_floor():
_leap_airborne = true
elif _leap_airborne or _phase_timer <= 0.0:
velocity.x = 0.0
velocity.z = 0.0
_slam(DP.f("bear_slam_radius"), 0.7)
_phase = Phase.RECOVER
_phase_timer = _scaled(DP.f("bear_leap_recover"))
Phase.RECOVER:
_decelerate(_PLANT_DECEL, delta)
# Let the clip's on-ground settle frames finish first, then idle out the recovery.
if _anim_player != null and _anim_player.is_playing() \
and _anim_player.current_animation == _anim_leap:
return
_play(_anim_idle)
_phase_timer -= delta
if _phase_timer <= 0.0:
_finish_routine()
func _launch_leap() -> void:
_phase = Phase.ACTIVE
_leap_airborne = false
var target := _bull.global_position if _bull != null else global_position
var flat := target - global_position
flat.y = 0.0
var dist := flat.length()
var dir := flat.normalized() if dist > 0.1 else _facing()
_face_dir(dir, 1.0)
# Airtime = the clip span between the takeoff and impact frames, so the paws hit ground
# on the impact frame (25). A custom leap gravity lets the arc reach bear_leap_height in
# exactly that airtime regardless of world gravity: for a symmetric hop of airtime T to
# peak height H, v_up = 4H/T and g = 8H/T².
var air_t := maxf(
(DP.f("bear_leap_impact_frame") - DP.f("bear_leap_takeoff_frame")) / _ANIM_FPS, 0.05)
var h := maxf(DP.f("bear_leap_height"), 0.1)
_leap_up = 4.0 * h / air_t
_leap_grav = 8.0 * h / (air_t * air_t)
# Land just in front of the bull, not on top of it — the slam radius still catches it.
var land_dist := maxf(dist - _BULL_STANDOFF, 0.0)
var h_speed := clampf(land_dist / air_t, 0.0, DP.f("bear_leap_speed"))
velocity = dir * h_speed + Vector3.UP * _leap_up
_phase_timer = air_t + 1.0 # safety fallback if a landing is missed
func _begin_recover(secs: float) -> void:
_phase = Phase.RECOVER
_phase_timer = _scaled(secs)
func _tick_recover(delta: float) -> void:
_decelerate(_PLANT_DECEL, delta)
_play(_anim_idle)
_phase_timer -= delta
if _phase_timer <= 0.0:
_finish_routine()
func _tick_stagger(delta: float) -> void:
_decelerate(12.0, delta)
_stagger_timer -= delta
_play(_anim_idle)
if _stagger_timer <= 0.0:
_stagger_cd = DP.f("bear_stagger_cd") # poise window so it can act before the next stun
_enter_neutral()
# Step toward the bull during a swing (so a strike lands even when the bull is a touch out
# of reach) while tracking its facing — but plant once close so the bear doesn't shove
# through it. Motion here reads as the strike's lunge, under the attack clip.
func _lunge_and_track(speed: float, delta: float) -> void:
if _bull == null:
_decelerate(_PLANT_DECEL, delta)
return
_face_point(_bull.global_position, delta, DP.f("bear_track_active"))
var to_bull := _flat_to_bull()
var dist := to_bull.length()
if speed > 0.0 and dist > _BULL_STANDOFF:
_accelerate(to_bull / dist, speed, delta)
else:
_decelerate(_PLANT_DECEL, delta)
# ── Hitting the bull ───────────────────────────────────────────────────────────────
# Directional strike: the bull is mauled inside `reach` of the bear that's facing it
# (a fast bull tunnels a thin collider between frames, so distance + facing is the
# reliable hit). Returns whether it connected.
func _try_hit(reach: float) -> bool:
if _bull == null:
return false
var to_bull := _flat_to_bull()
var dist := to_bull.length()
if dist > reach or dist < 0.01:
return false
if _facing().dot(to_bull / dist) < cos(deg_to_rad(DP.f("bear_hit_arc"))):
return false
_bull.call(&"take_sword_hit", "mauled")
_shake(0.55)
return true
# Radial slam (smash / leap landing): hits any way the bull is, within `radius`, with a
# ground-pound shake and a burst of dust kicked up at the paws. No facing arc — you're
# caught if you're near where it comes down.
func _slam(radius: float, shake: float) -> void:
_slam_dust()
if _bull == null:
_shake(shake)
return
if _flat_dist_to_bull() <= radius:
_bull.call(&"take_sword_hit", "mauled")
_shake(shake)
# Kick up a one-shot ring of dust at the bear's feet where the slam lands.
func _slam_dust() -> void:
if _dust == null:
return
_dust.global_position = global_position + Vector3(0.0, 0.15, 0.0)
_dust.restart()
_dust.emitting = true
# Tan ground-impact dust: a fast, wide, low burst that arcs up and settles — built in code
# so the Bear scene needs no extra particle node.
func _make_slam_dust() -> CPUParticles3D:
var p := CPUParticles3D.new()
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var sphere := SphereMesh.new()
sphere.radius = 0.16
sphere.height = 0.32
sphere.radial_segments = 4
sphere.rings = 2
sphere.material = mat
var ramp := Gradient.new()
ramp.set_color(0, Color(0.72, 0.60, 0.40, 0.9))
ramp.set_color(1, Color(0.55, 0.45, 0.30, 0.0))
var scale_curve := Curve.new()
scale_curve.add_point(Vector2(0.0, 0.4))
scale_curve.add_point(Vector2(0.35, 1.0))
scale_curve.add_point(Vector2(1.0, 0.0))
p.mesh = sphere
p.color_ramp = ramp
p.scale_amount_curve = scale_curve
p.emitting = false
p.one_shot = true
p.explosiveness = 1.0
p.amount = 36
p.lifetime = 0.9
p.randomness = 0.5
p.local_coords = false
# A flat-ish disc kicked outward and up from the ground.
p.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE
p.emission_sphere_radius = 0.5
p.direction = Vector3(0.0, 1.0, 0.0)
p.spread = 75.0
p.gravity = Vector3(0.0, -6.0, 0.0)
p.initial_velocity_min = 2.5
p.initial_velocity_max = 6.5
p.scale_amount_min = 0.6
p.scale_amount_max = 1.4
return p
# Fire the claw-slash burst on one paw for a swipe hit (right paw first, then left).
func _claw_slash(right: bool) -> void:
var claw := _claw_r if right else _claw_l
if claw == null:
return
claw.restart()
claw.emitting = true
# A short fan of pale streaks raked from a paw — thin boxes aligned to their velocity so
# they read as claw marks slashing outward. Built in code and parented to a hand bone.
func _make_claw_slash() -> CPUParticles3D:
var p := CPUParticles3D.new()
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
# A thin, long box becomes a streak once aligned to its velocity.
var streak := BoxMesh.new()
streak.size = Vector3(0.03, 0.55, 0.03)
streak.material = mat
var ramp := Gradient.new()
ramp.set_color(0, Color(0.85, 0.95, 1.0, 0.95))
ramp.set_color(1, Color(0.5, 0.75, 1.0, 0.0))
var scale_curve := Curve.new()
scale_curve.add_point(Vector2(0.0, 1.0))
scale_curve.add_point(Vector2(0.7, 1.0))
scale_curve.add_point(Vector2(1.0, 0.0))
p.mesh = streak
p.color_ramp = ramp
p.scale_amount_curve = scale_curve
p.emitting = false
p.one_shot = true
p.explosiveness = 1.0 # all at once = a single rake, not a stream
p.amount = 5 # a handful of parallel claw marks
p.lifetime = 0.22
p.randomness = 0.2
p.local_coords = false # streaks stay in the air where they were raked, then fade
p.set_particle_flag(CPUParticles3D.PARTICLE_FLAG_ALIGN_Y_TO_VELOCITY, true)
# Rake across the paw's swing plane: a tight fan of fast streaks.
p.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE
p.emission_sphere_radius = 0.12
p.direction = Vector3(0.0, 0.0, 1.0)
p.spread = 22.0
p.gravity = Vector3.ZERO
p.initial_velocity_min = 6.0
p.initial_velocity_max = 9.0
return p
func _shake(amount: float) -> void:
if _bull == null:
return
var cam := _bull.get_node_or_null("Camera3D")
if cam:
cam.call(&"trigger_hit", clampf(amount, 0.2, 1.0))
# ── Taking damage ──────────────────────────────────────────────────────────────────
# Called by every bull ability (kick / slam / roll) that iterates the matador group.
func apply_ability_hit(hit_dir: Vector3, strength: float, _up_boost: float = 0.0) -> void:
_acquire_bull()
_take_hit(hit_dir, strength)
# Ramming: only a real charge from the bull wounds the bear; a gentle nudge is shrugged
# off. The bear never damages the bull by colliding — only its swings/slams do.
func _on_body_entered(body: Node3D) -> void:
if _state == State.DEAD or not body.is_in_group(&"player"):
return
var player := body as CharacterBody3D
if player.velocity.length() < DP.f("bear_hit_threshold"):
return
var flat := Vector3(player.velocity.x, 0.0, player.velocity.z)
var dir := flat.normalized() if flat.length() > 0.5 else \
(global_position - player.global_position).normalized()
_take_hit(dir, player.velocity.length())
func _take_hit(hit_dir: Vector3, strength: float) -> void:
if _state == State.DEAD:
return
_hp -= 1
_hit_flash()
health_changed.emit(_hp, int(DP.f("bear_max_hp")))
hit_dir.y = 0.0
hit_dir = hit_dir.normalized()
_blood_burst.burst(global_position + Vector3(0.0, 1.4, 0.0), hit_dir)
_shake(clampf(strength / 15.0, 0.4, 1.0))
if _hp <= 0:
_die(hit_dir)
return
# Hyper-armour: a committed swing takes the wound but isn't knocked out of it — only
# the vulnerable states (neutral / positioning / a routine's recovery) stagger. This
# is what makes the recovery the real punish window.
if not _can_stagger():
return
velocity.x = hit_dir.x * 4.0
velocity.z = hit_dir.z * 4.0
_combo_queue.clear()
_state = State.STAGGER
_stagger_timer = DP.f("bear_stagger_time")
func _can_stagger() -> bool:
if _stagger_cd > 0.0:
return false # poise: recently staggered, so it rides out hits instead of stun-locking
match _state:
State.NEUTRAL, State.STALK, State.PROWL, State.STAGGER:
return true
State.SWIPE, State.SMASH, State.LEAP:
return _phase == Phase.RECOVER # airborne / mid-swing can't be staggered
return false
func _die(hit_dir: Vector3) -> void:
_state = State.DEAD
killed.emit()
collision_layer = 0
if _hit_area:
_hit_area.monitoring = false
velocity = Vector3.ZERO
_blood_burst.burst(global_position + Vector3(0.0, 1.4, 0.0), hit_dir)
_play(_anim_death)
# No death clip on the rig — topple the beast onto its side (about the hit direction,
# so it falls the way it was struck) and sink it away, then free.
#var fall_axis := Vector3(-hit_dir.z, 0.0, hit_dir.x)
#if fall_axis.length() < 0.01:
# fall_axis = Vector3.RIGHT
#var tween := create_tween()
#tween.tween_property(_mesh, "rotation",
# _mesh.rotation + fall_axis.normalized() * deg_to_rad(88.0), 0.7)\
# .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
#tween.tween_interval(2.5)
#tween.tween_property(_mesh, "position:y", _mesh.position.y - 3.0, 1.0)
#tween.tween_callback(queue_free)
# ── Interface shared with the matador (guarded elsewhere via has_method) ─────────────
func is_active() -> bool:
return _state != State.DEAD
func ai_state_name() -> String:
var n := State.keys()[_state] as String
if _state in [State.SWIPE, State.SMASH, State.LEAP]:
n += ":" + (Phase.keys()[_phase] as String)
if _enraged:
n += "*"
return n
# ── Movement / geometry helpers ─────────────────────────────────────────────────────
func _flat_to_bull() -> Vector3:
var to := _bull.global_position - global_position
to.y = 0.0
return to
func _flat_dist_to_bull() -> float:
return _flat_to_bull().length()
func _horizontal_speed() -> float:
return Vector2(velocity.x, velocity.z).length()
func _facing() -> Vector3:
return Vector3(sin(_mesh.rotation.y), 0.0, cos(_mesh.rotation.y))
func _accelerate(dir: Vector3, spd: float, delta: float) -> void:
velocity.x = move_toward(velocity.x, dir.x * spd, spd * _ACCEL_FACTOR * delta)
velocity.z = move_toward(velocity.z, dir.z * spd, spd * _ACCEL_FACTOR * delta)
func _decelerate(rate: float, delta: float) -> void:
velocity.x = move_toward(velocity.x, 0.0, rate * delta)
velocity.z = move_toward(velocity.z, 0.0, rate * delta)
func _face_point_if_bull(delta: float, rate: float = -1.0) -> void:
if _bull != null:
_face_point(_bull.global_position, delta, rate)
func _face_point(point: Vector3, delta: float, rate: float = -1.0) -> void:
_face_dir(point - global_position, delta, rate)
func _face_dir(dir: Vector3, delta: float, rate: float = -1.0) -> void:
if rate < 0.0:
rate = DP.f("bear_turn_rate")
dir.y = 0.0
if dir.length_squared() > 0.01:
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(dir.x, dir.z), delta * rate)
func _weighted_pick(options: Array) -> State:
var total := 0.0
for o: Array in options:
total += o[1]
var r := randf() * total
for o: Array in options:
r -= o[1]
if r <= 0.0:
return o[0]
return options[0][0]
# ── Animation helpers ───────────────────────────────────────────────────────────────
func _resolve_anim(candidates: Array) -> StringName:
if _anim_player == null:
return &""
var list := _anim_player.get_animation_list()
for want: String in candidates:
for have: String in list:
if have.to_upper().contains(want.to_upper()):
return StringName(have)
return &""
func _ensure_loop(anim_name: StringName) -> void:
if anim_name != &"" and _anim_player.has_animation(anim_name):
_anim_player.get_animation(anim_name).loop_mode = Animation.LOOP_LINEAR
func _set_no_loop(anim_name: StringName) -> void:
if anim_name != &"" and _anim_player.has_animation(anim_name):
_anim_player.get_animation(anim_name).loop_mode = Animation.LOOP_NONE
func _anim_length(anim_name: StringName, fallback: float) -> float:
if _anim_player and anim_name != &"" and _anim_player.has_animation(anim_name):
return _anim_player.get_animation(anim_name).length
return fallback
func _play(anim_name: StringName) -> void:
if _anim_player == null or anim_name == &"":
return
if _anim_player.current_animation == anim_name:
return
_anim_player.play(anim_name)
# Restart a one-shot attack clip from its first frame at `speed` (even if already current).
func _play_attack(anim_name: StringName, speed: float) -> void:
if _anim_player == null or anim_name == &"":
return
_anim_player.play(anim_name, -1.0, speed)
_anim_player.seek(0.0, true)
func _find_skeleton(node: Node) -> Skeleton3D:
if node is Skeleton3D:
return node as Skeleton3D
for child: Node in node.get_children():
var r := _find_skeleton(child)
if r:
return r
return null
func _find_anim_player(node: Node) -> AnimationPlayer:
if node is AnimationPlayer:
return node as AnimationPlayer
for child: Node in node.get_children():
var r := _find_anim_player(child)
if r:
return r
return null
+1
View File
@@ -0,0 +1 @@
uid://cn7fhjkro7637
+8
View File
@@ -0,0 +1,8 @@
extends Node3D
func mesh_on():
visible = true
func mesh_off():
visible = false
+1
View File
@@ -0,0 +1 @@
uid://bsbed05ifr18s
-9
View File
@@ -1,9 +0,0 @@
extends Camera3D
@export var spring_arm: Node3D
func _process(delta: float) -> void:
var lerp_power: float = DP.f("cam_lerp_power")
position = position.lerp(spring_arm.position, delta * lerp_power)
-1
View File
@@ -1 +0,0 @@
uid://b720l6m0ehrj3
+9
View File
@@ -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)
-39
View File
@@ -1,39 +0,0 @@
extends Node3D
@onready var spring_arm: SpringArm3D = $SpringArm3D
var _desired_length: float = 7.0
func _ready() -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
_desired_length = DP.f("cam_spring_length")
spring_arm.spring_length = _desired_length
func _unhandled_input(event: InputEvent) -> void:
var sensitivity: float = DP.f("cam_sensitivity")
var min_vert: float = deg_to_rad(DP.f("cam_min_vert_angle"))
var max_vert: float = deg_to_rad(DP.f("cam_max_vert_angle"))
var min_zoom: float = DP.f("cam_min_zoom")
var max_zoom: float = DP.f("cam_max_zoom")
if event is InputEventMouseMotion:
rotation.y -= event.relative.x * sensitivity
rotation.y = wrapf(rotation.y, 0.0, TAU)
rotation.x -= event.relative.y * sensitivity
rotation.x = clamp(rotation.x, min_vert, max_vert)
if event.is_action_pressed("zoom_in"):
_desired_length = clamp(_desired_length / 1.2, min_zoom, max_zoom)
if event.is_action_pressed("zoom_out"):
_desired_length = clamp(_desired_length * 1.2, min_zoom, max_zoom)
if event.is_action_pressed("toggle_mouse_capture"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
else:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _process(_delta: float) -> void:
spring_arm.spring_length = _desired_length
-1
View File
@@ -1 +0,0 @@
uid://b4td4k8yo5y78
+29 -2
View File
@@ -18,6 +18,8 @@ const _COMMANDS := {
"diff": "list params changed from default", "diff": "list params changed from default",
"toggle": "flip a bool param", "toggle": "flip a bool param",
"list": "print params (optional section)", "list": "print params (optional section)",
"level": "select level and reload (level <id> — arena / arena_boss / bear)",
"restart": "reload the current level scene",
"clear": "clear the log", "clear": "clear the log",
"help": "show this help", "help": "show this help",
} }
@@ -472,6 +474,12 @@ func _run_command(parts: PackedStringArray) -> bool:
_cmd_diff() _cmd_diff()
"list": "list":
_cmd_list(parts) _cmd_list(parts)
"level":
_cmd_level(parts)
"restart":
_log_ok("Restarting…")
get_tree().paused = false
get_tree().reload_current_scene()
"clear": "clear":
_log.clear() _log.clear()
"help": "help":
@@ -581,9 +589,9 @@ func _set_param(key: String, meta: Dictionary, raw: String) -> void:
var lo := meta["min"] as float var lo := meta["min"] as float
var hi := meta["max"] as float var hi := meta["max"] as float
if not raw.is_valid_float() or not is_finite(raw.to_float()): if not raw.is_valid_float() or not is_finite(raw.to_float()):
_log_warn("%s wants a number %s%s" % [key, _fmt_num(lo), _fmt_num(hi)]) _log_warn("%s wants a number" % key)
return return
var v := snappedf(clampf(raw.to_float(), lo, hi), meta["step"] as float) var v := raw.to_float()
DP.set_value(key, v) DP.set_value(key, v)
_log_ok("[b]%s[/b] ← [color=#ffe080]%s[/color]" % [key, _fmt_num(v)]) _log_ok("[b]%s[/b] ← [color=#ffe080]%s[/color]" % [key, _fmt_num(v)])
elif meta["type"] == TYPE_BOOL: elif meta["type"] == TYPE_BOOL:
@@ -600,6 +608,25 @@ func _set_param(key: String, meta: Dictionary, raw: String) -> void:
_log_warn("Cannot set param of type %d" % meta["type"]) _log_warn("Cannot set param of type %d" % meta["type"])
func _cmd_level(parts: PackedStringArray) -> void:
if parts.size() == 1:
var cur := Run.active_level()
var cur_name := (cur.id as String) if cur != null else "none"
_log_ok("Current level: [b]%s[/b]" % cur_name)
var ids: Array[StringName] = Run.level_ids()
_log_raw("Available: " + " ".join(ids.map(func(id: StringName) -> String: return str(id))))
return
var id := StringName(parts[1].to_lower())
var level := Run.get_level(id)
if level == null:
_log_warn("Unknown level '%s'. Try: %s" % [parts[1], " ".join(Run.level_ids().map(func(i: StringName) -> String: return str(i)))])
return
Run.debug_level_override = level
_log_ok("Level set to [b]%s[/b] — reloading…" % str(id))
get_tree().paused = false
get_tree().change_scene_to_file(level.scene_path)
func _cmd_list(parts: PackedStringArray) -> void: func _cmd_list(parts: PackedStringArray) -> void:
var filter := parts[1].to_lower() if parts.size() > 1 else "" var filter := parts[1].to_lower() if parts.size() > 1 else ""
var all := DP.get_all() var all := DP.get_all()
+83 -6
View File
@@ -25,19 +25,96 @@ func _ready() -> void:
load_saved() load_saved()
# ── Web fullscreen + landscape lock ─────────────────────────────────────────────
# Browsers only allow requestFullscreen / screen.orientation.lock from inside a user
# gesture, so we piggyback on the first tap/click of the web build to go fullscreen and
# lock to landscape. One-shot; input still flows to the menu/game normally (never consumed).
var _fs_triggered: bool = false
func _input(event: InputEvent) -> void:
if _fs_triggered or not OS.has_feature("web"):
return
var gesture := (event is InputEventScreenTouch and (event as InputEventScreenTouch).pressed) \
or (event is InputEventMouseButton and (event as InputEventMouseButton).pressed)
if gesture:
_fs_triggered = true
request_fullscreen_landscape()
## Enter browser fullscreen and lock to landscape. Must be called from a user-gesture
## context (first tap, or a button press). No-op off web. Android Chrome/Brave honour the
## orientation lock; iOS Safari ignores it (unsupported), so the .catch() swallows the reject.
func request_fullscreen_landscape() -> void:
if not OS.has_feature("web"):
return
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
JavaScriptBridge.eval(
"(function(){function l(){try{if(screen.orientation&&screen.orientation.lock)"
+ "screen.orientation.lock('landscape').catch(function(){});}catch(e){}}"
+ "l();document.addEventListener('fullscreenchange',l,{once:true});})()", true)
## 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.
+37 -55
View File
@@ -1,19 +1,16 @@
extends MultiMeshInstance3D extends MultiMeshInstance3D
## A whole stand of cheering spectators rendered as camera-facing billboards in a ## A whole stand of cheering spectators rendered as flat billboards in a single draw
## single draw call — the performant replacement for dozens of skinned crowd skeletons. ## call — the performant replacement for dozens of skinned crowd skeletons. The sprite
## The sprite sheet is baked from the existing crowd figure by tools/bake_crowd_sheet.gd. ## sheet is baked from the existing crowd figure by tools/bake_crowd_sheet.gd.
## ##
## Placement mirrors an existing crowd node (default: the sibling PublikNode) so the ## Placement comes from CrowdMarker nodes the designer drops into the level (collected
## billboards land exactly where the crowd was authored; if that node is missing it ## via the &"crowd_marker" group): each marker gives one spectator its world position and
## falls back to a generated tribune so the scene still populates. ## a chosen facing (one of six 60°-spaced headings). If no markers are present the scene
## falls back to a generated tribune so it still populates.
const SHEET_PATH := "res://Assets/crowd_clap_sheet.png" const SHEET_PATH := "res://Assets/crowd_clap_sheet.png"
const SHADER_PATH := "res://crowd_billboard.gdshader" const SHADER_PATH := "res://crowd_billboard.gdshader"
## Node whose descendant figures (names starting with `figure_prefix`) donate their
## world positions. Empty = use the generated fallback stand.
@export var source_path: NodePath = ^"../PublikNode"
@export var figure_prefix: String = "mees"
## World height of a spectator billboard, in metres. Sheet aspect sets the width. ## World height of a spectator billboard, in metres. Sheet aspect sets the width.
@export var figure_height: float = 2.0 @export var figure_height: float = 2.0
@export var frames: int = 8 @export var frames: int = 8
@@ -36,7 +33,8 @@ var _rng := RandomNumberGenerator.new()
func _ready() -> void: func _ready() -> void:
_rng.seed = hash("bullosseum-crowd") # deterministic layout/phases across runs _rng.seed = hash("bullosseum-crowd") # deterministic layout/phases across runs
_build() # Deferred so every CrowdMarker has run _ready and joined the group before we harvest.
_build.call_deferred()
func _build() -> void: func _build() -> void:
@@ -58,71 +56,53 @@ func _build() -> void:
mat.set_shader_parameter("fps", clap_fps) mat.set_shader_parameter("fps", clap_fps)
material_override = mat material_override = mat
var seats := _seat_positions() # Each seat is a (global feet-position, unit forward) pair.
var center := _arena_center(seats) var positions := PackedVector3Array()
var facings: Array[Vector3] = []
_collect_seats(positions, facings)
var mm := MultiMesh.new() var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D mm.transform_format = MultiMesh.TRANSFORM_3D
mm.use_custom_data = true mm.use_custom_data = true
mm.mesh = quad mm.mesh = quad
mm.instance_count = seats.size() mm.instance_count = positions.size()
for i in seats.size(): for i in positions.size():
var s := 1.0 + _rng.randf_range(-0.06, 0.06) # slight height variety var s := 1.0 + _rng.randf_range(-0.06, 0.06) # slight height variety
mm.set_instance_transform(i, Transform3D(_facing_basis(seats[i], center, s), seats[i])) mm.set_instance_transform(i, Transform3D(_upright_basis(facings[i], s), positions[i]))
var b := 1.0 + _rng.randf_range(-tint_variation, tint_variation) var b := 1.0 + _rng.randf_range(-tint_variation, tint_variation)
mm.set_instance_custom_data(i, Color(_rng.randf(), b, b * 0.99, b * 0.98)) mm.set_instance_custom_data(i, Color(_rng.randf(), b, b * 0.99, b * 0.98))
multimesh = mm multimesh = mm
# Orientation for a seat: the sprite's forward (+Z, the quad's face) points horizontally # Upright basis whose forward (+Z, the quad's face) points along `look`, uniformly
# inward at the arena centre, so each ring segment faces the action instead of the # scaled by `s`. World +Y stays up so spectators never tilt.
# camera. Upright (world +Y), uniformly scaled by `s`. func _upright_basis(look: Vector3, s: float) -> Basis:
func _facing_basis(seat: Vector3, center: Vector3, s: float) -> Basis:
var look := center - seat
look.y = 0.0 look.y = 0.0
look = look.normalized() if look.length() > 0.001 else Vector3(0.0, 0.0, -1.0) look = look.normalized() if look.length() > 0.001 else Vector3(0.0, 0.0, 1.0)
var up := Vector3.UP var up := Vector3.UP
var right := up.cross(look).normalized()
var basis := Basis() var basis := Basis()
basis.x = right basis.x = up.cross(look).normalized()
basis.y = up basis.y = up
basis.z = look basis.z = look
return basis.scaled(Vector3.ONE * s) return basis.scaled(Vector3.ONE * s)
# Horizontal centre the crowd faces — the mean of the seat positions (the ring/stand # Seats from designer-placed CrowdMarker nodes: each contributes its world position and
# centroid), which for an arena crowd is the middle of the pitch. # its chosen 60°-spaced facing. With no markers present, fall back to a generated ring
func _arena_center(seats: PackedVector3Array) -> Vector3: # that faces inward so the scene still shows a crowd.
if seats.is_empty(): func _collect_seats(positions: PackedVector3Array, facings: Array[Vector3]) -> void:
return global_position for node in get_tree().get_nodes_in_group(&"crowd_marker"):
var sum := Vector3.ZERO var marker := node as Node3D
for p in seats: if marker == null or not marker.has_method(&"facing_dir"):
sum += p continue
return sum / float(seats.size()) positions.append(marker.global_position)
facings.append(marker.facing_dir())
if positions.is_empty():
_fallback_stand(positions, facings)
# Global feet-positions for every spectator: harvested from the source crowd if present, func _fallback_stand(positions: PackedVector3Array, facings: Array[Vector3]) -> void:
# otherwise the generated fallback tribune.
func _seat_positions() -> PackedVector3Array:
var out := PackedVector3Array()
var src := get_node_or_null(source_path)
if src != null:
_collect_figures(src, out)
if out.is_empty():
_fallback_stand(out)
return out
func _collect_figures(node: Node, out: PackedVector3Array) -> void:
if node is Node3D and node != self and (node as Node3D).name.begins_with(figure_prefix):
out.append((node as Node3D).global_position)
return # don't descend into a figure's own rig
for c in node.get_children():
_collect_figures(c, out)
func _fallback_stand(out: PackedVector3Array) -> void:
var placed := 0 var placed := 0
for row in fallback_rows: for row in fallback_rows:
var radius := fallback_inner_radius + float(row) * fallback_row_step var radius := fallback_inner_radius + float(row) * fallback_row_step
@@ -133,5 +113,7 @@ func _fallback_stand(out: PackedVector3Array) -> void:
if placed >= fallback_count: if placed >= fallback_count:
return return
var ang := TAU * float(s) / float(seats_in_row) var ang := TAU * float(s) / float(seats_in_row)
out.append(Vector3(cos(ang) * radius, y, sin(ang) * radius)) var pos := Vector3(cos(ang) * radius, y, sin(ang) * radius)
positions.append(pos)
facings.append(-pos) # face the ring centre (origin)
placed += 1 placed += 1
+89
View File
@@ -0,0 +1,89 @@
@tool
extends Node3D
class_name CrowdMarker
## A designer-placed spot for one billboard spectator. Drop these into a level in the
## editor; crowd_billboards.gd harvests every marker (via the &"crowd_marker" group) into
## its MultiMesh at runtime, using each marker's global position and chosen facing.
##
## The marker draws a lightweight preview (a slab standing in for the sprite + a forward
## arrow) so its position and orientation are visible in the viewport. That preview is
## editor-only and never serialised into the scene, so it costs nothing at runtime.
## The six selectable facings, one every 60° about the Y axis. DEG_0 faces the quad's
## front (+Z) along world +Z; increasing values rotate clockwise looking down.
enum Orientation {
DEG_0,
DEG_60,
DEG_120,
DEG_180,
DEG_240,
DEG_300,
}
const ORIENTATION_STEPS := 6
## Metres — matches crowd_billboards.figure_height so the editor preview reads true to scale.
const PREVIEW_HEIGHT := 2.0
@export var orientation := Orientation.DEG_0:
set(value):
orientation = value
_apply_orientation()
var _preview: Node3D
func _ready() -> void:
add_to_group(&"crowd_marker")
if Engine.is_editor_hint():
_build_preview()
_apply_orientation()
# Facing about Y for the current orientation: 0, 60, 120 … degrees as radians.
func facing_yaw() -> float:
return float(orientation) * (TAU / float(ORIENTATION_STEPS))
# Unit forward the spectator's front (+Z) points along, on the horizontal plane.
func facing_dir() -> Vector3:
var a := facing_yaw()
return Vector3(sin(a), 0.0, cos(a))
func _apply_orientation() -> void:
if _preview != null:
_preview.rotation = Vector3(0.0, facing_yaw(), 0.0)
# Editor-only preview. Children are added without an owner so they are excluded from the
# saved .tscn (Godot only serialises nodes owned by the scene root).
func _build_preview() -> void:
_preview = Node3D.new()
add_child(_preview)
var aspect := 0.5
var slab := MeshInstance3D.new()
var quad := QuadMesh.new()
quad.size = Vector2(PREVIEW_HEIGHT * aspect, PREVIEW_HEIGHT)
quad.center_offset = Vector3(0.0, PREVIEW_HEIGHT * 0.5, 0.0) # stand on the marker
slab.mesh = quad
slab.material_override = _preview_material(Color(0.3, 0.7, 1.0, 0.35))
_preview.add_child(slab)
var arrow := MeshInstance3D.new()
var shaft := BoxMesh.new()
shaft.size = Vector3(0.08, 0.08, PREVIEW_HEIGHT * 0.5)
arrow.mesh = shaft
# Push the shaft out along +Z (the facing) and up to chest height.
arrow.position = Vector3(0.0, PREVIEW_HEIGHT * 0.5, PREVIEW_HEIGHT * 0.25)
arrow.material_override = _preview_material(Color(1.0, 0.85, 0.2, 0.9))
_preview.add_child(arrow)
func _preview_material(col: Color) -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.albedo_color = col
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
return mat
+1
View File
@@ -0,0 +1 @@
uid://70j3ks1emhkd
+5 -1
View File
@@ -10,6 +10,7 @@ extends Node
## ##
## Toggle from the console: ## Toggle from the console:
## show_collisions · show_hitboxes · show_bones · show_raycasts (true / false) ## show_collisions · show_hitboxes · show_bones · show_raycasts (true / false)
## show_hitboxes also surfaces the bull's and bear's body capsules, not just Area3Ds
## show_states — floating AI-state tags + velocity vectors over each matador ## show_states — floating AI-state tags + velocity vectors over each matador
## show_animation_name — floating current-clip tag over every AnimationPlayer host ## show_animation_name — floating current-clip tag over every AnimationPlayer host
## show_stats — 2D corner readout: fps / frame ms / draw calls / matador tally ## show_stats — 2D corner readout: fps / frame ms / draw calls / matador tally
@@ -421,7 +422,10 @@ func _collect(node: Node, col: bool, hit: bool, bone: bool, anim: bool, wanted:
# Ragdoll bone colliders (PhysicalBone3D) are excluded — use show_bones for # Ragdoll bone colliders (PhysicalBone3D) are excluded — use show_bones for
# the skeleton; otherwise every corpse floods the view with capsules. # the skeleton; otherwise every corpse floods the view with capsules.
var is_body := parent is PhysicsBody3D and not (parent is PhysicalBone3D) var is_body := parent is PhysicsBody3D and not (parent is PhysicalBone3D)
if (is_area and hit) or (is_body and col): # The bull's and bear's body capsules are combat volumes too, so they also
# surface under show_hitboxes (not just show_collisions).
var is_combatant := is_body and (parent.is_in_group(&"player") or parent.is_in_group(&"bear"))
if (is_area and hit) or (is_body and col) or (is_combatant and hit):
wanted[cs.get_instance_id()] = [cs, is_area] wanted[cs.get_instance_id()] = [cs, is_area]
for child in node.get_children(): for child in node.get_children():
_collect(child, col, hit, bone, anim, wanted) _collect(child, col, hit, bone, anim, wanted)
+147 -32
View File
@@ -17,7 +17,6 @@ var _values: Dictionary = {}
func _ready() -> void: func _ready() -> void:
_register_all() _register_all()
load_saved()
func _register_all() -> void: func _register_all() -> void:
@@ -46,12 +45,12 @@ func _register_all() -> void:
_reg_f("Dust", "dust_explosiveness", 0.35, 0.0, 1.0) _reg_f("Dust", "dust_explosiveness", 0.35, 0.0, 1.0)
_reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0) _reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0)
_reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0) _reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0)
_reg_f("Dust", "walk_scale_min", 0.35, 0.05, 3.0) _reg_f("Dust", "walk_scale_min", 0.20, 0.05, 3.0)
_reg_f("Dust", "walk_scale_max", 0.65, 0.05, 3.0) _reg_f("Dust", "walk_scale_max", 0.40, 0.05, 3.0)
_reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0) _reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0)
_reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0) _reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0)
_reg_f("Dust", "charge_scale_min", 1.0, 0.05, 6.0) _reg_f("Dust", "charge_scale_min", 0.35, 0.05, 6.0)
_reg_f("Dust", "charge_scale_max", 2.2, 0.05, 6.0) _reg_f("Dust", "charge_scale_max", 0.70, 0.05, 6.0)
_reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0) _reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0)
_reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0) _reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0)
# ── Tail ────────────────────────────────────────────────────────────────── # ── Tail ──────────────────────────────────────────────────────────────────
@@ -99,16 +98,12 @@ func _register_all() -> void:
_reg_f("Camera", "cam_drag_speed", 3.5, 0.5, 20.0) _reg_f("Camera", "cam_drag_speed", 3.5, 0.5, 20.0)
_reg_f("Camera", "cam_follow_lerp", 10.0, 1.0, 40.0) _reg_f("Camera", "cam_follow_lerp", 10.0, 1.0, 40.0)
# ── Matador ─────────────────────────────────────────────────────────────── # ── Matador ───────────────────────────────────────────────────────────────
_reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0) # Wave system: matadors enter the arena in escalating waves (1 → 2 → … → N where N
# Hydra split: a slain matador is replaced on the spot by mat_split_factor fresh # is the number of placed spawn points). mat_spawn_stagger is the delay between each
# matadors, so the horde doubles each generation (1 → 2 → 4 → 8 …) at the default # matador leaping in within a wave; mat_wave_pause is the breather after a wave clears.
# 2. Capped at mat_split_generations deep so the arena stays winnable (factor <2 or _reg_f("Matador", "mat_wave_size", 8.0, 1, 50)
# generations 0 = no splitting, the classic one-and-done arena). _reg_f("Matador", "mat_spawn_stagger", 0.4, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_split_factor", 2.0, 1.0, 6.0, 1.0) _reg_f("Matador", "mat_wave_pause", 2.0, 0.0, 8.0, 0.25)
_reg_f("Matador", "mat_split_generations", 3.0, 0.0, 6.0, 1.0)
# The bull wins outright once it has slain this many matadors, even if the hydra
# still has bodies on the sand. 0 = disabled (win only when the horde dies out)
_reg_f("Matador", "mat_win_kills", 21.0, 0.0, 200.0, 1.0)
_reg_f("Matador", "mat_walk_speed", 5.5, 0.5, 10.0) _reg_f("Matador", "mat_walk_speed", 5.5, 0.5, 10.0)
_reg_f("Matador", "mat_wander_radius", 22.0, 5.0, 50.0) _reg_f("Matador", "mat_wander_radius", 22.0, 5.0, 50.0)
# Radius of the inner fighting floor: bull + matadors spawn on this ring (so # Radius of the inner fighting floor: bull + matadors spawn on this ring (so
@@ -139,7 +134,7 @@ func _register_all() -> void:
_reg_f("Matador", "mat_attack_range", 8.0, 2.0, 20.0) _reg_f("Matador", "mat_attack_range", 8.0, 2.0, 20.0)
_reg_f("Matador", "mat_attack_speed", 7.5, 1.0, 12.0) _reg_f("Matador", "mat_attack_speed", 7.5, 1.0, 12.0)
_reg_f("Matador", "mat_attack_duration", 1.8, 0.5, 5.0, 0.1) _reg_f("Matador", "mat_attack_duration", 1.8, 0.5, 5.0, 0.1)
_reg_f("Matador", "mat_attack_min_dist", 3.2, 0.5, 6.0, 0.1) _reg_f("Matador", "mat_attack_min_dist", 4.2, 0.5, 6.0, 0.1)
# Forward drive during the opening of a melee swing — turns the stationary stab # Forward drive during the opening of a melee swing — turns the stationary stab
# into an estocada lunge so the blade covers ground toward the bull. # into an estocada lunge so the blade covers ground toward the bull.
_reg_f("Matador", "mat_lunge_speed", 13.0, 0.0, 20.0) _reg_f("Matador", "mat_lunge_speed", 13.0, 0.0, 20.0)
@@ -153,7 +148,7 @@ func _register_all() -> void:
# Stops "sword's off to the side but I still died" — you're only gored when the # Stops "sword's off to the side but I still died" — you're only gored when the
# blade is actually presented at you. # blade is actually presented at you.
_reg_f("Matador", "mat_stab_arc", 55.0, 5.0, 180.0, 5.0) _reg_f("Matador", "mat_stab_arc", 55.0, 5.0, 180.0, 5.0)
_reg_f("Matador", "mat_roll_chance", 0.35, 0.0, 1.0, 0.05) _reg_f("Matador", "mat_roll_chance", 0.8, 0.0, 1.0, 0.05)
_reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05) _reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05)
# Playback rate of the Draw_weapon clip used for both drawing and sheathing — # Playback rate of the Draw_weapon clip used for both drawing and sheathing —
# higher = snappier unholster/holster. # higher = snappier unholster/holster.
@@ -190,24 +185,98 @@ func _register_all() -> void:
_reg_f("Matador", "mat_taunt_hold", 8.0, 1.0, 20.0, 0.5) _reg_f("Matador", "mat_taunt_hold", 8.0, 1.0, 20.0, 0.5)
# Small so it smooths dithering without locking the matador into idling. # Small so it smooths dithering without locking the matador into idling.
_reg_f("Matador", "mat_w_commit", 0.15, 0.0, 2.0, 0.05) _reg_f("Matador", "mat_w_commit", 0.15, 0.0, 2.0, 0.05)
# ── Bear (boss) ─────────────────────────────────────────────────────────────
# A Dark-Souls-style boss with a learnable moveset. It soaks bear_max_hp clean hits
# (ram or ability), staggering on each, and only topples when worn down. From NEUTRAL
# it weighs its options by the range band to the bull and commits to one telegraphed
# routine (wind-up → active → recovery); each routine plays to the end (the recovery
# is the player's punish window). Below bear_enrage_frac HP it enters phase 2: faster
# tells and recovery (× bear_enrage_scale) and longer combo chains.
_reg_f("Bear", "bear_max_hp", 30.0, 1.0, 100.0, 1.0)
# HP fraction at which phase 2 ("Fury") triggers, and how much it multiplies every
# wind-up / recovery timer (lower = snappier, more dangerous).
_reg_f("Bear", "bear_enrage_frac", 0.4, 0.0, 1.0, 0.05)
_reg_f("Bear", "bear_enrage_scale", 0.7, 0.3, 1.0, 0.05)
# Range bands (m) that pick the moveset: ≤close it strikes (swipe / smash), ≤mid it
# walks in to close the gap (stalk) or leaps, beyond it lopes in (prowl) or leaps.
# close is deliberately generous — the bear commits to melee "from a bit further" and
# steps in during the swing (see bear_lunge_speed) rather than needing to be on top.
_reg_f("Bear", "bear_close_range", 5.0, 1.0, 12.0, 0.1)
_reg_f("Bear", "bear_mid_range", 9.0, 3.0, 20.0, 0.5)
# Beat spent in NEUTRAL sizing up the bull between routines — kept short so the bear
# flows straight from one action into the next instead of pausing (raise for a more
# hesitant, readable boss; near-0 for relentless pressure). Shrinks again in phase 2.
_reg_f("Bear", "bear_think_time", 0.06, 0.0, 1.0, 0.01)
# Locomotion: the all-fours prowl (close a big gap), the menacing 2-leg walk-in
# (stalk), the facing rate while positioning/winding up, and the softer tracking rate
# a swing keeps on the bull mid-attack so it's harder to just walk out of.
_reg_f("Bear", "bear_run_speed", 11.0, 1.0, 20.0, 0.1)
_reg_f("Bear", "bear_stalk_speed", 5.5, 0.5, 12.0, 0.1)
_reg_f("Bear", "bear_turn_rate", 7.0, 1.0, 20.0, 0.5)
_reg_f("Bear", "bear_track_active", 3.5, 0.0, 12.0, 0.1)
# Forward step-in during a swing so a strike lands even when the bull is a touch out of
# reach (also carries the tracking); 0 = a purely stationary swing.
_reg_f("Bear", "bear_lunge_speed", 7.0, 0.0, 16.0, 0.5)
# Playback multiplier for the swipe/smash clips — >1 makes the punches snappier.
_reg_f("Bear", "bear_attack_anim_speed", 1.35, 0.5, 3.0, 0.05)
# Half-arc (deg) the bear must be facing the bull within for a directional swing to
# connect; slams use their own radius instead of an arc.
_reg_f("Bear", "bear_hit_arc", 70.0, 5.0, 180.0, 5.0)
# Double Swipe (ONE_TWO_HIT) — bread-and-butter close combo, two quick hits.
_reg_f("Bear", "bear_swipe_windup", 0.3, 0.05, 2.0, 0.05)
_reg_f("Bear", "bear_swipe_recover", 0.35, 0.05, 3.0, 0.05)
_reg_f("Bear", "bear_swipe_reach", 4.2, 1.0, 8.0, 0.1)
# Overhead Smash (SMASH_HIT) — slower tell, biggest punish window, AoE on impact.
_reg_f("Bear", "bear_smash_windup", 0.6, 0.1, 3.0, 0.05)
_reg_f("Bear", "bear_smash_recover", 0.75, 0.1, 4.0, 0.05)
_reg_f("Bear", "bear_smash_radius", 4.2, 1.0, 8.0, 0.1)
# Leap Slam (JUMP_SMASH, 47 frames @30fps) — timed to the clip by FRAME: it plays the
# crouch grounded, springs at the takeoff frame, and the arc is solved so the paws hit
# ground exactly on the impact frame (25), leaving the tail frames to settle on the
# ground — no finishing the anim in mid-air. The arc uses its own gravity so peak height
# and airtime are independent: bear_leap_height is the peak reached in that fixed airtime.
_reg_f("Bear", "bear_leap_takeoff_frame", 6.0, 0.0, 46.0, 1.0)
_reg_f("Bear", "bear_leap_impact_frame", 25.0, 1.0, 47.0, 1.0)
_reg_f("Bear", "bear_leap_height", 2.6, 0.5, 8.0, 0.1)
_reg_f("Bear", "bear_leap_recover", 0.35, 0.0, 4.0, 0.05)
_reg_f("Bear", "bear_leap_speed", 14.0, 4.0, 30.0, 0.5)
_reg_f("Bear", "bear_slam_radius", 4.0, 1.0, 8.0, 0.1)
# Stalk (WALK_ON2LEGS) — how long it will keep walking the bull down before re-deciding.
_reg_f("Bear", "bear_stalk_time", 2.5, 0.0, 6.0, 0.1)
# Odds a close combo chains a follow-up smash after the swipe (phase 2 always chains).
_reg_f("Bear", "bear_combo_chance", 0.7, 0.0, 1.0, 0.05)
# Minimum bull speed (ram) that counts as a hit on the bear — a gentle nudge is
# shrugged off, only a real charge or an ability wounds it. (The bear itself never
# damages the bull by colliding — only its swings and slams hurt.)
_reg_f("Bear", "bear_hit_threshold", 6.0, 1.0, 30.0, 0.5)
# How long (s) the bear is rocked back and can't act after taking a hit.
_reg_f("Bear", "bear_stagger_time", 0.5, 0.0, 2.0, 0.05)
# Poise: after a stagger the bear shrugs off staggers for this long (hits still wound
# it), so it can't be perma-stunlocked — it gets a window to commit an attack that then
# rides its hyper-armour. 0 = no poise (stun-locks under sustained fire).
_reg_f("Bear", "bear_stagger_cd", 1.0, 0.0, 4.0, 0.05)
# ── Sword ───────────────────────────────────────────────────────────────── # ── Sword ─────────────────────────────────────────────────────────────────
# Local seating of the sword in the right hand (drawn / fighting). # Local seating of the sword in the right hand (drawn / fighting).
# Defaults are identity — the bone is oriented in Blender to seat the sword # The blade model runs along its local +Z, but weapon_bone (like every rig bone)
# directly; these exist to fine-tune in-game without re-exporting. # points down its local +Y — so identity leaves the blade jutting out sideways.
# rot_x = -90 maps the blade's +Z onto the bone's +Y so it thrusts straight along
# the bone; rot_y is then the free roll (crossguard tilt) about the blade axis.
_reg_f("Sword", "sword_pos_x", 0.0, -1.0, 1.0, 0.01) _reg_f("Sword", "sword_pos_x", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "sword_pos_y", 0.0, -1.0, 1.0, 0.01) _reg_f("Sword", "sword_pos_y", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "sword_pos_z", 0.0, -1.0, 1.0, 0.01) _reg_f("Sword", "sword_pos_z", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "sword_rot_x", 0.0, -180.0, 180.0, 1.0) _reg_f("Sword", "sword_rot_x", -90.0, -180.0, 180.0, 1.0)
_reg_f("Sword", "sword_rot_y", 0.0, -180.0, 180.0, 1.0) _reg_f("Sword", "sword_rot_y", 0.0, -180.0, 180.0, 1.0)
_reg_f("Sword", "sword_rot_z", 0.0, -180.0, 180.0, 1.0) _reg_f("Sword", "sword_rot_z", 0.0, -180.0, 180.0, 1.0)
# Local seating of the sword in the hip holster (sheathed / wandering). # Local seating of the sword in the hip holster (sheathed / wandering).
# Independent of the hand grip so each pose can be tuned without affecting the # Independent of the hand grip so each pose can be tuned without affecting the
# other. Defaults angle the blade down along the matador's side. # other. weapon_rest_bone sits on the left hip; these angle the blade down and
# out along the OUTSIDE of the left thigh (rot_x/rot_y aim the blade, rot_z rolls
# it about its own axis) so it trails back beside the leg instead of clipping it.
_reg_f("Sword", "sword_rest_pos_x", 0.0, -1.0, 1.0, 0.01) _reg_f("Sword", "sword_rest_pos_x", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "sword_rest_pos_y", 0.0, -1.0, 1.0, 0.01) _reg_f("Sword", "sword_rest_pos_y", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "sword_rest_pos_z", 0.0, -1.0, 1.0, 0.01) _reg_f("Sword", "sword_rest_pos_z", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "sword_rest_rot_x", -120.0, -180.0, 180.0, 1.0) _reg_f("Sword", "sword_rest_rot_x", -110.0, -180.0, 180.0, 1.0)
_reg_f("Sword", "sword_rest_rot_y", 0.0, -180.0, 180.0, 1.0) _reg_f("Sword", "sword_rest_rot_y", -133.0, -180.0, 180.0, 1.0)
_reg_f("Sword", "sword_rest_rot_z", 0.0, -180.0, 180.0, 1.0) _reg_f("Sword", "sword_rest_rot_z", 0.0, -180.0, 180.0, 1.0)
# ── Spear throw (matador ranged attack) ────────────────────────────────── # ── Spear throw (matador ranged attack) ──────────────────────────────────
# Local seating of the spear in the throwing hand during the wind-up. Tune to # Local seating of the spear in the throwing hand during the wind-up. Tune to
@@ -238,10 +307,10 @@ func _register_all() -> void:
_reg_f("Sword", "mat_throw_arm_release", -90.0,-180.0, 180.0, 1.0) _reg_f("Sword", "mat_throw_arm_release", -90.0,-180.0, 180.0, 1.0)
_reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0) _reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0)
# ── Abilities ───────────────────────────────────────────────────────────── # ── Abilities ─────────────────────────────────────────────────────────────
_reg_f("Abilities", "kick_cooldown", 1.5, 0.2, 10.0, 0.1) _reg_f("Abilities", "kick_cooldown", 2, 0.2, 600, 0.1)
_reg_f("Abilities", "kick_range", 6.0, 0.5, 8.0, 0.1) _reg_f("Abilities", "kick_range", 6.0, 0.5, 8.0, 0.1)
_reg_f("Abilities", "kick_strength", 12.0, 1.0, 30.0) _reg_f("Abilities", "kick_strength", 12.0, 1.0, 30.0)
_reg_f("Abilities", "slam_cooldown", 4.0, 0.5, 15.0, 0.1) _reg_f("Abilities", "slam_cooldown", 12, 0.5, 600, 0.1)
_reg_f("Abilities", "slam_jump_velocity", 7.0, 2.0, 20.0) _reg_f("Abilities", "slam_jump_velocity", 7.0, 2.0, 20.0)
# Descent gravity multiplier — higher = faster crash, less airtime. # Descent gravity multiplier — higher = faster crash, less airtime.
_reg_f("Abilities", "slam_fall_mult", 3.0, 1.0, 8.0, 0.5) _reg_f("Abilities", "slam_fall_mult", 3.0, 1.0, 8.0, 0.5)
@@ -250,7 +319,7 @@ func _register_all() -> void:
_reg_f("Abilities", "slam_launch_up", 7.0, 0.0, 15.0, 0.5) _reg_f("Abilities", "slam_launch_up", 7.0, 0.0, 15.0, 0.5)
_reg_f("Abilities", "slam_range", 4.0, 1.0, 10.0, 0.1) _reg_f("Abilities", "slam_range", 4.0, 1.0, 10.0, 0.1)
_reg_f("Abilities", "slam_strength", 14.0, 1.0, 30.0) _reg_f("Abilities", "slam_strength", 14.0, 1.0, 30.0)
_reg_f("Abilities", "dash_cooldown", 0.5, 0.1, 10.0, 0.1) _reg_f("Abilities", "dash_cooldown", 3, 0.1, 600.0, 0.1)
_reg_f("Abilities", "dash_speed", 66.0, 5.0, 200.0) _reg_f("Abilities", "dash_speed", 66.0, 5.0, 200.0)
# Exponential rate the dash bleeds from dash_speed back to cruise (max_speed). # Exponential rate the dash bleeds from dash_speed back to cruise (max_speed).
# Higher = a snappier bullet that dies off quicker; lower = a longer glide. # Higher = a snappier bullet that dies off quicker; lower = a longer glide.
@@ -261,7 +330,7 @@ func _register_all() -> void:
# closest matador; on slamming one the ball POPS it (launched skyward via # closest matador; on slamming one the ball POPS it (launched skyward via
# roll_knockup) then CHAINS to the next closest, up to roll_chain_count times. # roll_knockup) then CHAINS to the next closest, up to roll_chain_count times.
# roll_turn is the homing turn rate. When no chain targets remain, the roll ends. # roll_turn is the homing turn rate. When no chain targets remain, the roll ends.
_reg_f("Roll", "roll_cooldown", 3.0, 0.5, 15.0, 0.1) _reg_f("Roll", "roll_cooldown", 20, 0.5, 600, 0.1)
_reg_f("Roll", "roll_duration", 2.5, 0.5, 6.0, 0.1) # max roll time _reg_f("Roll", "roll_duration", 2.5, 0.5, 6.0, 0.1) # max roll time
_reg_f("Roll", "roll_chain_count", 2.0, 0.0, 8.0, 1.0) # extra matadors chained after the first _reg_f("Roll", "roll_chain_count", 2.0, 0.0, 8.0, 1.0) # extra matadors chained after the first
_reg_f("Roll", "roll_base_speed", 28.0, 5.0, 120.0) # speed at the start of the roll _reg_f("Roll", "roll_base_speed", 28.0, 5.0, 120.0) # speed at the start of the roll
@@ -281,8 +350,8 @@ func _register_all() -> void:
# a uniform out of bounds. # a uniform out of bounds.
_reg_b("PS1", "show_ps1", true) _reg_b("PS1", "show_ps1", true)
_reg_f("PS1", "ps1_pixel_scale", 4.0, 1.0, 8.0, 1.0) _reg_f("PS1", "ps1_pixel_scale", 4.0, 1.0, 8.0, 1.0)
_reg_f("PS1", "ps1_color_depth", 5.0, 2.0, 8.0, 1.0) _reg_f("PS1", "ps1_color_depth", 4.5, 2.0, 8.0, 1.0)
_reg_f("PS1", "ps1_dither_strength", 0.8, 0.0, 2.0, 0.05) _reg_f("PS1", "ps1_dither_strength", 1, 0.0, 2.0, 0.05)
_reg_f("PS1", "ps1_scanline_strength", 0.2, 0.0, 1.0, 0.05) _reg_f("PS1", "ps1_scanline_strength", 0.2, 0.0, 1.0, 0.05)
_reg_f("PS1", "ps1_roll_speed", 3.0, 0.0, 10.0, 0.1) _reg_f("PS1", "ps1_roll_speed", 3.0, 0.0, 10.0, 0.1)
_reg_f("PS1", "ps1_roll_strength", 1.0, 0.0, 1.0, 0.05) _reg_f("PS1", "ps1_roll_strength", 1.0, 0.0, 1.0, 0.05)
@@ -299,6 +368,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,
@@ -349,11 +421,54 @@ func get_all() -> Dictionary:
func save() -> void: func save() -> void:
var cfg := ConfigFile.new() var path := "res://debug_params.gd"
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
push_error("DP.save: cannot open " + path)
return
var src := file.get_as_text()
file.close()
for key: String in _params: for key: String in _params:
var p: Dictionary = _params[key] var p: Dictionary = _params[key]
cfg.set_value(p["section"], key, p["value"]) if p["value"] == p["default"]:
cfg.save(SAVE_PATH) continue
var re := RegEx.new()
var literal := ""
if p["type"] == TYPE_FLOAT:
re.compile('(_reg_f\\("[^"]+",\\s*"%s",\\s*)([-\\d.e]+)' % key)
literal = _fmt_float(p["value"] as float)
else:
re.compile('(_reg_b\\("[^"]+",\\s*"%s",\\s*)(true|false)' % key)
literal = "true" if bool(p["value"]) else "false"
var m := re.search(src)
if m == null:
push_warning("DP.save: no _reg_ line found for '%s' — skipped" % key)
continue
# Splice the value in by index rather than RegEx.sub(): sub()'s replacement
# string parses "$n" as a backreference, so "$1"+"12.5" becomes "$112.5"
# (group 112 → PCRE2 error → mangled output). Indexed splice inserts the
# literal verbatim, exactly the value the user entered.
src = src.substr(0, m.get_start(2)) + literal + src.substr(m.get_end(2))
p["default"] = p["value"]
var wfile := FileAccess.open(path, FileAccess.WRITE)
if wfile == null:
push_error("DP.save: cannot write " + path)
return
wfile.store_string(src)
wfile.close()
# Godot's String % operator has no "g" conversion — "%.6g" % v slips past the
# compiler (v is not const) but emits the literal "%.6g" at runtime, which would be
# written straight into the source. "%s" gives the shortest round-tripping form,
# matching the value the user entered; the guard keeps it a float literal.
func _fmt_float(v: float) -> String:
var s := "%s" % v
if not ("." in s or "e" in s):
s += ".0"
return s
func load_saved() -> void: func load_saved() -> void:
+21 -3
View File
@@ -34,10 +34,28 @@ commit=${GIT_COMMIT}
built=${BUILD_DATE} built=${BUILD_DATE}
EOF EOF
# Cache-bust: give wasm/pck unique names so browsers never serve a stale build.
# index.js gets a ?v= query string; wasm/pck get renamed because the engine
# fetches them internally using GODOT_CONFIG.executable as the base name.
STAMP="$GIT_COMMIT"
cp "$BUILD_DIR/index.wasm" "$BUILD_DIR/index.${STAMP}.wasm"
cp "$BUILD_DIR/index.wasm.gz" "$BUILD_DIR/index.${STAMP}.wasm.gz"
cp "$BUILD_DIR/index.pck" "$BUILD_DIR/index.${STAMP}.pck"
sed -i \
-e "s|src=\"index\.js\"|src=\"index.js?v=${STAMP}\"|" \
-e "s|\"executable\":\"index\"|\"executable\":\"index.${STAMP}\"|" \
-e "s|\"index\.pck\"|\"index.${STAMP}.pck\"|g" \
-e "s|\"index\.wasm\"|\"index.${STAMP}.wasm\"|g" \
"$BUILD_DIR/index.html"
# Remove previous build's versioned assets from the server before uploading.
ssh "$SERVER_USER@$SERVER_HOST" "find $SERVER_PATH -maxdepth 1 -name 'index.*.wasm' -o -name 'index.*.wasm.gz' -o -name 'index.*.pck' | xargs -r rm -f"
scp "$BUILD_DIR/index.html" \ scp "$BUILD_DIR/index.html" \
"$BUILD_DIR/index.js" \ "$BUILD_DIR/index.js" \
"$BUILD_DIR/index.wasm.gz" \ "$BUILD_DIR/index.${STAMP}.wasm.gz" \
"$BUILD_DIR/index.pck" \ "$BUILD_DIR/index.${STAMP}.pck" \
"$BUILD_DIR/index.audio.worklet.js" \ "$BUILD_DIR/index.audio.worklet.js" \
"$BUILD_DIR/index.audio.position.worklet.js" \ "$BUILD_DIR/index.audio.position.worklet.js" \
"$BUILD_DIR/index.icon.png" \ "$BUILD_DIR/index.icon.png" \
@@ -46,4 +64,4 @@ scp "$BUILD_DIR/index.html" \
"$BUILD_DIR/version.txt" \ "$BUILD_DIR/version.txt" \
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/" "$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
echo "deployed ${VERSION} to $SERVER_HOST:$SERVER_PATH" echo "deployed ${VERSION} (${STAMP}) to $SERVER_HOST:$SERVER_PATH"
+88 -2
View File
@@ -1,18 +1,30 @@
[runnable_presets]
Web="Web"
"Windows Desktop"="Windows Desktop"
[preset.0] [preset.0]
name="Web" name="Web"
platform="Web" platform="Web"
runnable=true
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/web/index.html" 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_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""
seed=0
encrypt_pck=false encrypt_pck=false
encrypt_directory=false encrypt_directory=false
script_export_mode=2
[preset.0.options] [preset.0.options]
@@ -21,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=""
@@ -29,6 +41,7 @@ html/canvas_resize_policy=2
html/focus_canvas_on_start=true html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false html/experimental_virtual_keyboard=false
progressive_web_app/enabled=false progressive_web_app/enabled=false
progressive_web_app/ensure_cross_origin_isolation_headers=true
progressive_web_app/offline_page="" progressive_web_app/offline_page=""
progressive_web_app/display=1 progressive_web_app/display=1
progressive_web_app/orientation=0 progressive_web_app/orientation=0
@@ -36,3 +49,76 @@ progressive_web_app/icon_144x144=""
progressive_web_app/icon_180x180="" progressive_web_app/icon_180x180=""
progressive_web_app/icon_512x512="" progressive_web_app/icon_512x512=""
progressive_web_app/background_color=Color(0, 0, 0, 1) 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}'"
+40 -5
View File
@@ -71,6 +71,8 @@ var _result_win: bool = false
var _over_root: Control var _over_root: Control
var _over_video: VideoStreamPlayer var _over_video: VideoStreamPlayer
var _over_label: Label var _over_label: Label
var _over_continue: Button
var _over_again: Button
func _ready() -> void: func _ready() -> void:
@@ -119,7 +121,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)
@@ -210,7 +216,10 @@ func _find_spawner() -> void:
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo: if event is InputEventKey and event.pressed and not event.echo:
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]: if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
_restart() if _result_win:
_continue_to_lobby()
else:
_restart()
elif event.physical_keycode == KEY_TAB and not _game_over: elif event.physical_keycode == KEY_TAB and not _game_over:
_toggle_controls() _toggle_controls()
elif event.physical_keycode == KEY_ESCAPE: elif event.physical_keycode == KEY_ESCAPE:
@@ -229,6 +238,17 @@ func _restart() -> void:
get_tree().reload_current_scene() get_tree().reload_current_scene()
# A cleared level advances the run and drops the player into the lobby hub: commit the
# win, flag the lobby as the active level, then reload the shell so it builds the lobby.
# (Choosing the next map node happens from the lobby via trigger areas, added later.)
func _continue_to_lobby() -> void:
get_tree().paused = false
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
Run.complete_current_level()
Run.go_to_lobby()
get_tree().change_scene_to_file(Run.active_level().scene_path)
func _toggle_controls() -> void: func _toggle_controls() -> void:
_controls_visible = not _controls_visible _controls_visible = not _controls_visible
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls" _toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
@@ -240,7 +260,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:
@@ -301,6 +327,9 @@ func _show_game_over(win: bool, _cause: String = "") -> void:
await get_tree().create_timer(_WIN_DELAY if win else _LOSS_DELAY, false, false, true).timeout await get_tree().create_timer(_WIN_DELAY if win else _LOSS_DELAY, false, false, true).timeout
_over_label.text = "Bull Won!" if _result_win else "Matadors Won!" _over_label.text = "Bull Won!" if _result_win else "Matadors Won!"
# A win advances the run (on to the map); a loss ends it (retry from the top).
_over_continue.visible = _result_win
_over_again.visible = not _result_win
var path := _WIN_VIDEO if _result_win else _LOSS_VIDEO var path := _WIN_VIDEO if _result_win else _LOSS_VIDEO
if ResourceLoader.exists(path): if ResourceLoader.exists(path):
@@ -371,9 +400,15 @@ func _build_game_over() -> void:
row.offset_bottom = -48.0 row.offset_bottom = -48.0
_over_root.add_child(row) _over_root.add_child(row)
var again := _make_button("Go Again (Enter)") # Win → Continue to the run map; loss → Go Again from the top. Shown per-result in
again.pressed.connect(_restart) # _show_game_over; both share the Enter shortcut.
row.add_child(again) _over_continue = _make_button("Continue (Enter)")
_over_continue.pressed.connect(_continue_to_lobby)
row.add_child(_over_continue)
_over_again = _make_button("Go Again (Enter)")
_over_again.pressed.connect(_restart)
row.add_child(_over_again)
var menu := _make_button("Main Menu") var menu := _make_button("Main Menu")
menu.pressed.connect(_return_to_menu) menu.pressed.connect(_return_to_menu)
+10
View File
@@ -0,0 +1,10 @@
[gd_scene format=3 uid="uid://map_screen_v1"]
[ext_resource type="Script" path="res://levels/map_screen.gd" id="1_map"]
[node name="MapScreen" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_map")
+773
View File
@@ -0,0 +1,773 @@
[gd_scene format=3 uid="uid://co4tgvdpdte4v"]
[ext_resource type="PackedScene" uid="uid://dio67r8ambe7h" path="res://Assets/arena_placeholder_v01.glb" id="2_nxogm"]
[ext_resource type="PackedScene" uid="uid://cl54dt38yj2x" path="res://Assets/Arena.glb" id="3_fy5k1"]
[ext_resource type="Script" uid="uid://dou5ar6tkmng1" path="res://levels/matador_spawn.gd" id="3_spawn"]
[ext_resource type="Script" uid="uid://70j3ks1emhkd" path="res://crowd_marker.gd" id="9_agtuu"]
[ext_resource type="Script" uid="uid://cj0ytlkuavmf4" path="res://crowd_billboards.gd" id="11_crowd"]
[sub_resource type="BoxShape3D" id="BoxShape3D_ulcgi"]
[sub_resource type="Environment" id="Environment_73fnb"]
[node name="arena_level" type="Node3D" unique_id=701327120]
[node name="Node3D" type="Node3D" parent="." unique_id=1772505910]
[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=2083665760]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=1028681009]
transform = Transform3D(30, 0, 0, 0, 1, 0, 0, 0, 30, 0, -0.5, 0)
shape = SubResource("BoxShape3D_ulcgi")
[node name="arena_placeholder_v01" parent="Node3D" unique_id=173047470 instance=ExtResource("2_nxogm")]
[node name="Arena_draft" parent="Node3D/arena_placeholder_v01" index="0" unique_id=603097973]
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(173047470, 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=1270130903 instance=ExtResource("3_fy5k1")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.017679214, 9.536743e-07, -0.013886452)
[node name="MatadorSpawner" type="Node3D" parent="." unique_id=1524912967]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3, 14)
script = ExtResource("3_spawn")
[node name="MatadorSpawner2" type="Node3D" parent="." unique_id=2081003535]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 3, 0)
script = ExtResource("3_spawn")
[node name="MatadorSpawner4" type="Node3D" parent="." unique_id=497566616]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14, 3, 0)
script = ExtResource("3_spawn")
[node name="MatadorSpawner3" type="Node3D" parent="." unique_id=618446747]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3, -15)
script = ExtResource("3_spawn")
[node name="Lights" type="Node3D" parent="." unique_id=614769858]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=960453715]
transform = Transform3D(0.01535992, 0.56825536, -0.82270896, 0.16280846, 0.8104039, 0.56279576, 0.9865382, -0.14258848, -0.08006904, -4.08134, 9.197985, 0.97087634)
light_energy = 0.4
light_indirect_energy = 0.767
shadow_blur = 0.47
[node name="SpotLight3D2" type="SpotLight3D" parent="Lights" unique_id=1654524086]
transform = Transform3D(0.904133, 0.30962402, -0.2944087, 0.3084627, 0.0037425472, 0.9512291, 0.2956252, -0.95085174, -0.0921237, -3.629381, 10.86347, -2.0124264)
light_energy = 16.0
light_indirect_energy = 26.095
distance_fade_enabled = true
spot_range = 32.90586
spot_angle = 73.93
[node name="SpotLight3D" type="SpotLight3D" parent="Lights" unique_id=1408554387]
transform = Transform3D(0.9763736, -9.445564e-09, 0.21608931, -0.20963383, 0.24260214, 0.9472053, -0.052423734, -0.9701259, 0.23687032, 4.0400305, 13.463301, 3.9410677)
visible = false
light_energy = 13.695
shadow_enabled = true
spot_range = 32.90586
[node name="SUN_DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=1840798568]
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 = 0.106
shadow_enabled = true
shadow_blur = 10.0
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1750284504]
environment = SubResource("Environment_73fnb")
[node name="Crowd" type="Node3D" parent="." unique_id=2013004589]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 14)
[node name="CrowdBillboards" type="MultiMeshInstance3D" parent="Crowd" unique_id=106730614]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -5, -14)
script = ExtResource("11_crowd")
[node name="Left" type="Node3D" parent="Crowd" unique_id=1621845484]
[node name="L_Step3" type="Node3D" parent="Crowd/Left" unique_id=623102737]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.592416, -1.045898, -23.088398)
[node name="CrowdMarker9" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=578926054]
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=1610777090]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.14058113, -2.5608063)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=1861343773]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.809, 0.14058208, 1.755331)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=1555035931]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.14058113, 4.362587)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=1750314761]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.14058161, -4.3905067)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=691141866]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.9175544, 0.14058161, -5.9128)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker15" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=1664612281]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -1.0940075, 0, -1.355114)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker16" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=1677355487]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -3.6482267, 0.14058113, -3.5884771)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker18" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=63826938]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, 1.3811512, 0.14058113, 2.7079024)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker19" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=972576799]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -6.663862, 0.14058161, -5.0203266)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker20" type="Node3D" parent="Crowd/Left/L_Step3" unique_id=1082256967]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -9.743899, 0.14058161, -6.133005)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="L_Step4" type="Node3D" parent="Crowd/Left" unique_id=2012797300]
[node name="CrowdMarker2" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=2100000002]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14, -0.0132164955, -23.088398)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker3" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=563466125]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.533139, -0.0132164955, -24.70021)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker4" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=1529541292]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15.021559, -0.013215542, -21.333067)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker5" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=10514539]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15.887294, -0.0132164955, -18.725811)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker6" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=1864581651]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9.740928, -0.013216019, -28.157803)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker7" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=937874303]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.6115, -0.013216019, -29.924221)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker8" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=1255677084]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.2360125, -0.0132164955, -30.806953)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker9" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=2072287774]
transform = Transform3D(0.9725945, 0, 0.232508, 0, 1, 0, -0.232508, 0, 0.9725945, 13.294765, -0.0132164955, -24.4567)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=1158540011]
transform = Transform3D(0.9725945, 0, 0.232508, 0, 1, 0, -0.232508, 0, 0.9725945, 11.827905, -0.0132164955, -26.068512)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=1862689569]
transform = Transform3D(0.9725945, 0, 0.232508, 0, 1, 0, -0.232508, 0, 0.9725945, 14.316324, -0.013215542, -22.701368)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=494798769]
transform = Transform3D(0.9725945, 0, 0.232508, 0, 1, 0, -0.232508, 0, 0.9725945, 15.182059, -0.0132164955, -20.094112)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=1508360812]
transform = Transform3D(0.9725945, 0, 0.232508, 0, 1, 0, -0.232508, 0, 0.9725945, 9.035693, -0.013216019, -29.526104)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=1811163868]
transform = Transform3D(0.9725945, 0, 0.232508, 0, 1, 0, -0.232508, 0, 0.9725945, 4.9062653, -0.013216019, -31.292522)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker15" type="Node3D" parent="Crowd/Left/L_Step4" unique_id=1866931486]
transform = Transform3D(0.9725945, 0, 0.232508, 0, 1, 0, -0.232508, 0, 0.9725945, 7.489478, -0.0132164955, -29.721859)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="L_Step1" type="Node3D" parent="Crowd/Left" unique_id=1744694409]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 11.379745, -2.091795, -22.703432)
[node name="CrowdMarker9" type="Node3D" parent="Crowd/Left/L_Step1" unique_id=1620001578]
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Left/L_Step1" unique_id=1055150056]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.14058113, -2.5608063)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Left/L_Step1" unique_id=717131032]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.2287855, 0.14058208, 1.9909182)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Left/L_Step1" unique_id=1781479729]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.14058113, 4.362587)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Left/L_Step1" unique_id=1352193990]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.14058161, -4.3905067)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Left/L_Step1" unique_id=743385499]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.9175544, 0.14058161, -5.9128)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="L_Step5" type="Node3D" parent="Crowd/Left" unique_id=670288549]
transform = Transform3D(0.9967379, 0, -0.08070645, 0, 1, 0, 0.08070645, 0, 0.9967379, 11.637655, -3.0939364, -20.899355)
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=274558473]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.14058113, -2.5608063)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=516605334]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.809, 0.14058208, 1.755331)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=1577172273]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.14058113, 4.362587)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=1757843488]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.14058161, -4.3905067)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=1120424592]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.9175544, 0.14058161, -5.9128)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker15" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=1819323657]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -1.0940075, 0, -1.355114)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker16" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=538915422]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -3.6482267, 0.14058113, -3.5884771)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker17" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=1328913359]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -0.053198814, 0.14058208, 0.27350044)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker18" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=878163668]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, 1.3811512, 0.14058113, 2.7079024)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker19" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=1568364605]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -6.663862, 0.14058161, -5.0203266)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker20" type="Node3D" parent="Crowd/Left/L_Step5" unique_id=1367833604]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -9.743899, 0.14058161, -6.133005)
script = ExtResource("9_agtuu")
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="Back" type="Node3D" parent="Crowd" unique_id=638158299]
[node name="B_Step3" type="Node3D" parent="Crowd/Back" unique_id=174422561]
transform = Transform3D(0.098605186, 0, 0.99512655, 0, 1, 0, -0.99512655, 0, 0.098605186, -8.350439, -1.045898, -26.733604)
[node name="CrowdMarker9" type="Node3D" parent="Crowd/Back/B_Step3" unique_id=1376565490]
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Back/B_Step3" unique_id=2134315249]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.14058113, -2.5608063)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Back/B_Step3" unique_id=434884711]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.809, 0.14058208, 1.755331)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Back/B_Step3" unique_id=1901106701]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.14058113, 4.362587)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Back/B_Step3" unique_id=1659701219]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.14058161, -4.3905067)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Back/B_Step3" unique_id=469584679]
transform = Transform3D(0.99999994, 0, 0, 0, 1, 0, 0, 0, 0.99999994, -7.9328146, 0.14058161, -5.5702662)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="B_Step4" type="Node3D" parent="Crowd/Back" unique_id=792967648]
transform = Transform3D(0.09860524, 0, 0.9951266, 0, 1, 0, -0.9951266, 0, 0.09860524, 13.383762, 0, -11.9259205)
[node name="CrowdMarker2" type="Node3D" parent="Crowd/Back/B_Step4" unique_id=409183135]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14, -0.0132164955, -23.088398)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker3" type="Node3D" parent="Crowd/Back/B_Step4" unique_id=1443859159]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.533139, -0.0132164955, -24.70021)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker4" type="Node3D" parent="Crowd/Back/B_Step4" unique_id=1737861319]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15.021559, -0.013215542, -21.333067)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker5" type="Node3D" parent="Crowd/Back/B_Step4" unique_id=1991036767]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15.887294, -0.0132164955, -18.725811)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker6" type="Node3D" parent="Crowd/Back/B_Step4" unique_id=1075095199]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9.740928, -0.013216019, -28.157803)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker7" type="Node3D" parent="Crowd/Back/B_Step4" unique_id=1699892650]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.6115, -0.013216019, -29.924221)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker8" type="Node3D" parent="Crowd/Back/B_Step4" unique_id=565692575]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.2360125, -0.0132164955, -30.806953)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="B_Step" type="Node3D" parent="Crowd/Back" unique_id=414907102]
transform = Transform3D(-0.04323627, 0, 0.99906486, 0, 1, 0, -0.99906486, 0, -0.04323627, -8.380329, -1.827393, -25.818556)
[node name="CrowdMarker9" type="Node3D" parent="Crowd/Back/B_Step" unique_id=1943968269]
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Back/B_Step" unique_id=2061244199]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.14058113, -2.5608063)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Back/B_Step" unique_id=977400271]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.2287855, 0.14058208, 1.9909182)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Back/B_Step" unique_id=1522748404]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.14058113, 4.362587)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Back/B_Step" unique_id=791374389]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.14058161, -4.3905067)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Back/B_Step" unique_id=1457299400]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.9175544, 0.14058161, -5.9128)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="L_Step5" type="Node3D" parent="Crowd/Back" unique_id=1665844535]
transform = Transform3D(0.022304952, 0, 0.99975127, 0, 1, 0, -0.99975127, 0, 0.022304952, -6.9624662, -3.0939345, -25.679592)
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=1629480429]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.3974409, -2.5608063)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=1506450411]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.809, 0.39744186, 1.755331)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=622605512]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.3974409, 4.362587)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=257326812]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.3974414, -4.3905067)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=1211459457]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.9175544, 0.3974414, -5.9128)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker15" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=155390702]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -1.0940075, 0.25685978, -1.355114)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker16" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=34227892]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -3.6482267, 0.3974409, -3.5884771)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker17" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=1904591453]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -0.053198814, 0.39744186, 0.27350044)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker18" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=687360614]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, 1.3811512, 0.3974409, 2.7079024)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker19" type="Node3D" parent="Crowd/Back/L_Step5" unique_id=1751401910]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -6.663862, 0.3974414, -5.0203266)
script = ExtResource("9_agtuu")
orientation = 1
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="Right" type="Node3D" parent="Crowd" unique_id=1277806052]
[node name="R_Step3" type="Node3D" parent="Crowd/Right" unique_id=1092390193]
transform = Transform3D(-0.9993231, 0, -0.036786973, 0, 1, 0, 0.036786973, 0, -0.9993231, -12.105062, -1.045898, -5.1461663)
[node name="CrowdMarker9" type="Node3D" parent="Crowd/Right/R_Step3" unique_id=550527812]
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Right/R_Step3" unique_id=417838082]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.14058113, -2.5608063)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Right/R_Step3" unique_id=562183345]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.809, 0.14058208, 1.755331)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Right/R_Step3" unique_id=59142166]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.14058113, 4.362587)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Right/R_Step3" unique_id=1970971214]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.14058161, -4.3905067)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Right/R_Step3" unique_id=622146302]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.9175544, 0.14058161, -5.9128)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="R_Step4" type="Node3D" parent="Crowd/Right" unique_id=1081871135]
transform = Transform3D(-0.99684, 0, 0.07943541, 0, 1, 0, -0.07943541, 0, -0.99684, 2.0997996, 0, -28.147049)
[node name="CrowdMarker2" type="Node3D" parent="Crowd/Right/R_Step4" unique_id=384688095]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14, -0.0132164955, -23.088398)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker3" type="Node3D" parent="Crowd/Right/R_Step4" unique_id=794438773]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.533139, -0.0132164955, -24.70021)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker4" type="Node3D" parent="Crowd/Right/R_Step4" unique_id=318354120]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15.021559, -0.013215542, -21.333067)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker5" type="Node3D" parent="Crowd/Right/R_Step4" unique_id=1139375927]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15.887294, -0.0132164955, -18.725811)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker6" type="Node3D" parent="Crowd/Right/R_Step4" unique_id=468384482]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9.740928, -0.013216019, -28.157803)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker7" type="Node3D" parent="Crowd/Right/R_Step4" unique_id=1219838852]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.6115, -0.013216019, -29.924221)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker8" type="Node3D" parent="Crowd/Right/R_Step4" unique_id=876634044]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.2360125, -0.0132164955, -30.806953)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="R_Step1" type="Node3D" parent="Crowd/Right" unique_id=1804391941]
transform = Transform3D(-0.9983546, 0, -0.05734154, 0, 1, 0, 0.05734154, 0, -0.9983546, -11.76964, -2.091795, -5.703799)
[node name="CrowdMarker9" type="Node3D" parent="Crowd/Right/R_Step1" unique_id=1122311940]
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Right/R_Step1" unique_id=107523420]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.14058113, -2.5608063)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Right/R_Step1" unique_id=265148663]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.2287855, 0.14058208, 1.9909182)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Right/R_Step1" unique_id=592546407]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.14058113, 4.362587)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Right/R_Step1" unique_id=514985832]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.14058161, -4.3905067)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Right/R_Step1" unique_id=416317204]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.9175544, 0.14058161, -5.9128)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="L_Step5" type="Node3D" parent="Crowd/Right" unique_id=1434949087]
transform = Transform3D(-0.99935067, 0, -0.036034077, 0, 1, 0, 0.036034077, 0, -0.99935067, -10.943644, -3.0939355, -6.015876)
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=1951969393]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.2258072, 0.3974409, -2.5608063)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker11" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=1614578381]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.809, 0.39744186, 1.755331)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker12" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=2079795403]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8979845, 0.3974409, 4.362587)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker13" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=18703002]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0180187, 0.3974414, -4.3905067)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker14" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=619627641]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.9175544, 0.3974414, -5.9128)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker15" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=46013841]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -1.0940075, 0.25685978, -1.355114)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker16" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=1130198232]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -3.6482267, 0.3974409, -3.5884771)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker17" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=744258528]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -0.053198814, 0.39744186, 0.27350044)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker18" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=774368922]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, 1.3811512, 0.3974409, 2.7079024)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker19" type="Node3D" parent="Crowd/Right/L_Step5" unique_id=545323927]
transform = Transform3D(0.9906587, 0, 0.13636485, 0, 1, 0, -0.13636485, 0, 0.9906587, -6.663862, 0.3974414, -5.0203266)
script = ExtResource("9_agtuu")
orientation = 2
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="Front_Right" type="Node3D" parent="Crowd" unique_id=1683981332]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16.440575, 0, -11.451455)
[node name="CrowdMarker3" type="Node3D" parent="Crowd/Front_Right" unique_id=943299423]
script = ExtResource("9_agtuu")
orientation = 4
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker4" type="Node3D" parent="Crowd/Front_Right" unique_id=1123847296]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.05489731, 0, 1.3246164)
script = ExtResource("9_agtuu")
orientation = 4
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker5" type="Node3D" parent="Crowd/Front_Right" unique_id=640400607]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7841702, -0.93699646, 0)
script = ExtResource("9_agtuu")
orientation = 4
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker6" type="Node3D" parent="Crowd/Front_Right" unique_id=603945551]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8390675, -0.93699646, 1.3246164)
script = ExtResource("9_agtuu")
orientation = 4
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker7" type="Node3D" parent="Crowd/Front_Right" unique_id=26053676]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.6784592, -2.0288038, 0.21630049)
script = ExtResource("9_agtuu")
orientation = 4
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker8" type="Node3D" parent="Crowd/Front_Right" unique_id=983869769]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.7333565, -2.0288038, 1.5409169)
script = ExtResource("9_agtuu")
orientation = 4
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker9" type="Node3D" parent="Crowd/Front_Right" unique_id=2131633869]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.7693996, -2.7652984, 0)
script = ExtResource("9_agtuu")
orientation = 4
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker10" type="Node3D" parent="Crowd/Front_Right" unique_id=393732717]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.824297, -2.7652984, 1.3246164)
script = ExtResource("9_agtuu")
orientation = 4
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="Front_Left" type="Node3D" parent="Crowd" unique_id=1426987879]
[node name="CrowdMarker" type="Node3D" parent="Crowd/Front_Left" unique_id=884656972]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.1676445, 0, 1.5733547)
script = ExtResource("9_agtuu")
orientation = 3
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker2" type="Node3D" parent="Crowd/Front_Left" unique_id=1281756834]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.1122522, 0, 1.5733547)
script = ExtResource("9_agtuu")
orientation = 3
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker3" type="Node3D" parent="Crowd/Front_Left" unique_id=287020937]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.483545, -1.8461189, -0.5833006)
script = ExtResource("9_agtuu")
orientation = 3
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker4" type="Node3D" parent="Crowd/Front_Left" unique_id=1628869900]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.7205377, -1.8461189, -0.36581516)
script = ExtResource("9_agtuu")
orientation = 3
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker5" type="Node3D" parent="Crowd/Front_Left" unique_id=375235166]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.245149, -0.9629531, 0.27452564)
script = ExtResource("9_agtuu")
orientation = 3
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[node name="CrowdMarker6" type="Node3D" parent="Crowd/Front_Left" unique_id=1951144178]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.7495923, -0.9629531, 0.48515797)
script = ExtResource("9_agtuu")
orientation = 3
metadata/_custom_type_script = "uid://70j3ks1emhkd"
[editable path="Node3D/arena_placeholder_v01"]
+105
View File
@@ -0,0 +1,105 @@
extends CanvasLayer
const _GOLD := Color(1.00, 0.78, 0.22)
const _CREAM := Color(0.93, 0.88, 0.72)
const _BAR_W: float = 1260.0
const _BAR_H: float = 18.0
const _BAR_BG: Color = Color(0.12, 0.04, 0.04, 0.88)
const _BAR_FILL: Color = Color(0.65, 0.08, 0.06)
const _BAR_ENRAGE: Color = Color(0.90, 0.32, 0.06)
var _bear: Node = null
var _fill: ColorRect = null
var _label: Label = null
var _max_hp: int = 1
var _current_hp: int = 1
func _ready() -> void:
layer = 10
_build_ui()
_find_bear.call_deferred()
func _find_bear() -> void:
var bears := get_tree().get_nodes_in_group(&"bear")
if bears.is_empty():
return
_bear = bears[0]
_bear.health_changed.connect(_on_health_changed)
_bear.killed.connect(_on_bear_killed)
var max_hp := int(DP.f("bear_max_hp"))
_on_health_changed(_bear.get_hp(), max_hp)
func _on_health_changed(current: int, max_hp: int) -> void:
_current_hp = current
_max_hp = max_hp
_update_bar()
func _on_bear_killed() -> void:
_current_hp = 0
_update_bar()
func _update_bar() -> void:
if _fill == null:
return
var frac := 0.0 if _max_hp <= 0 else clampf(float(_current_hp) / float(_max_hp), 0.0, 1.0)
_fill.size.x = _BAR_W * frac
var enrage_frac: float = DP.f("bear_enrage_frac")
_fill.color = _BAR_FILL.lerp(_BAR_ENRAGE, 1.0 - frac / enrage_frac if frac < enrage_frac else 0.0)
if _label != null:
_label.text = "BEAR" % [maxi(_current_hp, 0), _max_hp]
## for hp display in numbers write %d / %d
func _build_ui() -> void:
var panel := PanelContainer.new()
panel.add_theme_stylebox_override(
"panel", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 14.0, 8.0))
panel.anchor_left = 0.5
panel.anchor_right = 0.5
panel.anchor_top = 0.0
panel.anchor_bottom = 0.0
panel.grow_horizontal = Control.GROW_DIRECTION_BOTH
panel.offset_top = 16.0
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(panel)
var col := VBoxContainer.new()
col.add_theme_constant_override("separation", 5)
col.mouse_filter = Control.MOUSE_FILTER_IGNORE
panel.add_child(col)
_label = Label.new()
_label.text = "BEAR"
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.add_theme_font_override("font", UiFonts.body())
_label.add_theme_color_override("font_color", _GOLD)
_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
_label.add_theme_constant_override("outline_size", 3)
_label.add_theme_font_size_override("font_size", 30)
_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
col.add_child(_label)
var bar_bg := PanelContainer.new()
bar_bg.custom_minimum_size = Vector2(_BAR_W, _BAR_H)
bar_bg.clip_contents = true
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = _BAR_BG
bg_style.set_corner_radius_all(4)
bg_style.corner_detail = 4
bg_style.border_width_bottom = 2
bg_style.border_color = Color(UiTheme.GOLD.r, UiTheme.GOLD.g, UiTheme.GOLD.b, 0.35)
bar_bg.add_theme_stylebox_override("panel", bg_style)
bar_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
col.add_child(bar_bg)
_fill = ColorRect.new()
_fill.color = _BAR_FILL
_fill.size = Vector2(_BAR_W, _BAR_H)
_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
bar_bg.add_child(_fill)
+1
View File
@@ -0,0 +1 @@
uid://dfk7dcvki7rqu
+49
View File
@@ -0,0 +1,49 @@
[gd_scene format=3 uid="uid://dfxigg3cr6nl5"]
[ext_resource type="PackedScene" uid="uid://bm6rmt3xprhjs" path="res://Assets/bear_level_arena.glb" id="1_beararena"]
[ext_resource type="Script" uid="uid://bbtlwihtlc1wp" path="res://levels/bear_spawn.gd" id="2_spawn"]
[ext_resource type="PackedScene" uid="uid://o8ocjfprxa7u" path="res://Bear.tscn" id="3_bear"]
[ext_resource type="Script" uid="uid://dfk7dcvki7rqu" path="res://levels/bear_hud.gd" id="4_bearhud"]
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
[node name="bear_level" type="Node3D" unique_id=261730746]
[node name="Node3D" type="Node3D" parent="." unique_id=333447532]
[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=1466540248]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=575454169]
transform = Transform3D(30, 0, 0, 0, 1, 0, 0, 0, 30, 0, -0.5, 0)
shape = SubResource("BoxShape3D_floor")
[node name="bear_level_arena" parent="." unique_id=1529242945 instance=ExtResource("1_beararena")]
transform = Transform3D(1.94, 0, 0, 0, 1.94, 0, 0, 0, 1.94, 0, 0, 0)
[node name="floor" parent="bear_level_arena/floor" parent_id_path=PackedInt32Array(1529242945, 294762985) index="0" unique_id=1266729531]
visible = false
[node name="wall" parent="bear_level_arena/floor" parent_id_path=PackedInt32Array(1529242945, 294762985) index="1" unique_id=1445338369]
visible = false
[node name="BearSpawner" type="Node3D" parent="." unique_id=47931969]
script = ExtResource("2_spawn")
enemy_scene = ExtResource("3_bear")
[node name="BearHud" type="CanvasLayer" parent="." unique_id=102938471]
script = ExtResource("4_bearhud")
[node name="Lights" type="Node3D" parent="." unique_id=937122836]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=1786336124]
transform = Transform3D(1, 0, 0, 0, 0.7071067, 0.70710677, 0, -0.70710677, 0.7071067, 0, 36.945946, 70)
light_color = Color(1, 0.75782466, 0.7332154, 1)
light_energy = 1.796
shadow_enabled = true
[node name="DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=1308313006]
transform = Transform3D(1, 0, 0, 0, -0.8660254, 0.49999994, 0, -0.49999994, -0.8660254, 0, 112.56815, 93.37274)
light_color = Color(0, 0.33367383, 0.57971, 1)
light_energy = 0.616
[editable path="bear_level_arena"]
+33
View File
@@ -0,0 +1,33 @@
extends Node3D
signal matador_killed
signal all_defeated
@export var enemy_scene: PackedScene = null
var _alive: int = 0
var _resolved: bool = false
func _ready() -> void:
add_to_group(&"matador_spawn")
_spawn.call_deferred()
func _spawn() -> void:
if enemy_scene == null:
return
var bear: Node3D = enemy_scene.instantiate()
bear.position = Vector3(-15,0,0)
get_parent().add_child(bear)
if bear.has_signal(&"killed"):
bear.killed.connect(_on_bear_killed)
_alive += 1
func _on_bear_killed() -> void:
matador_killed.emit()
_alive -= 1
if not _resolved:
_resolved = true
all_defeated.emit()
+1
View File
@@ -0,0 +1 @@
uid://bbtlwihtlc1wp
+26
View File
@@ -0,0 +1,26 @@
extends Node3D
## The reusable game shell: player, camera, HUD and the PS1 filter — everything that
## outlives a single level. On load it instances the active level's module (geometry,
## spawners, lighting, colliders) into LevelRoot. Switching levels frees the whole
## previous module first, so no old mesh or collider can survive into the next fight.
@onready var _level_root: Node3D = $LevelRoot
func _ready() -> void:
Run.ensure_run()
load_active_level()
## Tear down the current level module and build the run's active one. Freeing the whole
## subtree (not hiding it) is the point: a level's colliders and spawners cease to exist
## before the next level's are created.
func load_active_level() -> void:
for child: Node in _level_root.get_children():
_level_root.remove_child(child)
child.queue_free()
var level := Run.active_level()
if level == null or level.level_scene == null:
push_warning("game_shell: active level has no level_scene")
return
_level_root.add_child(level.level_scene.instantiate())
+1
View File
@@ -0,0 +1 @@
uid://ptsfs3wq5bje
+26
View File
@@ -0,0 +1,26 @@
class_name LevelDef
extends Resource
## Definition of one playable level on the run map — the arena today, later a cave with
## a bear, a china shop to smash, and so on. Every map node references a LevelDef, and
## the map/flow code never hard-codes a scene: adding a level is a new LevelDef (plus its
## scene) handed out by the map generator in run_state.gd.
@export var id: StringName = &""
@export var display_name: String = "Level"
## The shell scene loaded when the player enters a node with this level — the reusable
## frame (player, camera, HUD, PS1 filter). Shared by every level; the level's own
## geometry rides in `level_scene`, which the shell instances at runtime.
@export_file("*.tscn") var scene_path: String = ""
## The level module instanced into the shell's LevelRoot: spawners, lighting, meshes,
## colliders and any level-specific subscripts. Swapped wholesale between levels so no
## previous level's geometry (or its colliders) can leak into the next.
@export var level_scene: PackedScene = null
## Placeholder pip tint on the map until per-level icon art exists.
@export var color: Color = Color(0.80, 0.62, 0.24)
## A boss level caps a run — its node is drawn larger and clearing it ends the map.
@export var is_boss: bool = false
## The enemy this level pits the bull against. Null → the usual matador wave (the
## spawner's default). Set → a single boss enemy of this scene is spawned instead (e.g.
## the bear), and the fight is won when it falls. The arena scene is shared either way;
## only the opponent changes.
@export var enemy_scene: PackedScene = null
+1
View File
@@ -0,0 +1 @@
uid://c6sef0tfl26sr
+25
View File
@@ -0,0 +1,25 @@
[gd_scene format=3 uid="uid://32nyipcwrab5"]
[ext_resource type="PackedScene" uid="uid://cwmo5vh738x6w" path="res://Assets/Lobby_level.glb" id="1_lobby"]
[sub_resource type="Environment" id="Environment_lobby"]
[node name="lobby_level" type="Node3D" unique_id=1511255302]
[node name="Lobby_level" parent="." unique_id=1119577247 instance=ExtResource("1_lobby")]
[node name="Lights" type="Node3D" parent="." unique_id=562527333]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=837461706]
transform = Transform3D(1, 0, 0, 0, 0.7071067, 0.70710677, 0, -0.70710677, 0.7071067, 0, 36.945946, 70)
light_color = Color(1, 0.9137255, 0.827451, 1)
light_energy = 1.6
shadow_enabled = true
[node name="DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=513226199]
transform = Transform3D(1, 0, 0, 0, -0.8660254, 0.49999994, 0, -0.49999994, -0.8660254, 0, 112.56815, 93.37274)
light_color = Color(0.4784314, 0.6156863, 0.8, 1)
light_energy = 0.45
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=939963056]
environment = SubResource("Environment_lobby")
+13
View File
@@ -0,0 +1,13 @@
class_name MapNode
extends RefCounted
## One node on the run map. Regenerated every run and never persisted, so RefCounted
## rather than Resource. `pos` is normalized (0-1) so the map screen can lay the node out
## at any resolution; `links` are column indices in the *next* row this node connects to
## (the Slay-the-Spire branching path).
var row: int = 0
var col: int = 0
var pos: Vector2 = Vector2.ZERO
var level: LevelDef = null
var links: PackedInt32Array = PackedInt32Array()
var cleared: bool = false
+1
View File
@@ -0,0 +1 @@
uid://emq8i2bmkekn
+280
View File
@@ -0,0 +1,280 @@
extends Control
## Slay-the-Spire-style run map shown between fights. Placeholder art — coloured pips
## joined by paths — but fully interactive: the player picks one of the reachable nodes
## and is sent into that level. Every node is the arena for now (see run_state.gd); the
## screen is level-agnostic, so richer levels just need a LevelDef with its own colour.
const _GOLD := Color(1.00, 0.78, 0.22)
const _CREAM := Color(0.93, 0.88, 0.72)
const _EDGE_DIM := Color(0.45, 0.36, 0.18, 0.55)
const _EDGE_ACTIVE := Color(1.00, 0.82, 0.32, 0.95)
const _NODE_SIZE := 58.0
const _BOSS_SIZE := 88.0
const _MENU_SCENE := "res://MainMenu.tscn"
var _map_area: Control
var _completion: PanelContainer
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
get_tree().paused = false
Run.ensure_run()
_build_ui()
_relayout.call_deferred() # defer so _map_area has its real size first
func _build_ui() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
var bg := ColorRect.new()
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.color = Color(0.09, 0.06, 0.03) # opaque leather, no scene showing through
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(bg)
var title := Label.new()
title.text = "Choose Your Path"
title.set_anchors_preset(Control.PRESET_TOP_WIDE)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.offset_top = 22.0
title.add_theme_font_override("font", UiFonts.title())
title.add_theme_font_size_override("font_size", 46)
title.add_theme_color_override("font_color", _GOLD)
title.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
title.add_theme_constant_override("outline_size", 6)
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(title)
_map_area = Control.new()
_map_area.set_anchors_preset(Control.PRESET_FULL_RECT)
_map_area.offset_top = 108.0
_map_area.offset_bottom = -92.0
_map_area.offset_left = 48.0
_map_area.offset_right = -48.0
_map_area.clip_contents = false
add_child(_map_area)
_map_area.resized.connect(_relayout)
var menu_btn := _make_button("Main Menu")
menu_btn.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
menu_btn.grow_vertical = Control.GROW_DIRECTION_BEGIN
menu_btn.offset_left = 32.0
menu_btn.offset_bottom = -26.0
menu_btn.pressed.connect(_to_menu)
add_child(menu_btn)
_build_completion()
# ── Layout ──────────────────────────────────────────────────────────────────
# Rebuilt from the run state on first show and whenever the map area resizes. Edges are
# added first so the node pips draw on top of the paths.
func _relayout() -> void:
if _map_area == null:
return
for child in _map_area.get_children():
child.queue_free()
var size := _map_area.size
if size.x <= 0.0 or size.y <= 0.0:
return
var reachable := Run.reachable()
var reach_set := {}
for n: MapNode in reachable:
reach_set[n] = true
var here := Run.current_node()
if Run.player_row < 0:
var start_pos := Vector2(size.x * 0.5, size.y - 8.0)
_add_start_pip(start_pos)
for n: MapNode in reachable:
_add_edge(start_pos, _local(n, size), true)
for row: Array in Run.map:
for from_node: MapNode in row:
for to_col: int in from_node.links:
var to_node := Run.node_at(from_node.row + 1, to_col)
if to_node == null:
continue
var active := from_node == here and reach_set.has(to_node)
_add_edge(_local(from_node, size), _local(to_node, size), active)
for row: Array in Run.map:
for node: MapNode in row:
_add_node(node, size, reach_set.has(node))
_completion.visible = Run.run_complete() or reachable.is_empty()
func _local(node: MapNode, size: Vector2) -> Vector2:
return Vector2(node.pos.x * size.x, node.pos.y * size.y)
func _add_edge(a: Vector2, b: Vector2, active: bool) -> void:
var line := Line2D.new()
line.add_point(a)
line.add_point(b)
line.width = 4.0 if active else 2.5
line.default_color = _EDGE_ACTIVE if active else _EDGE_DIM
line.antialiased = true
_map_area.add_child(line)
func _add_start_pip(center: Vector2) -> void:
var pip := PanelContainer.new()
var sz := 22.0
pip.custom_minimum_size = Vector2(sz, sz)
pip.size = Vector2(sz, sz)
pip.position = center - Vector2(sz, sz) * 0.5
pip.mouse_filter = Control.MOUSE_FILTER_IGNORE
pip.add_theme_stylebox_override("panel", UiTheme.disc(_GOLD, _CREAM, 2))
_map_area.add_child(pip)
func _add_node(node: MapNode, size: Vector2, is_reachable: bool) -> void:
var is_current := node.row == Run.player_row and node.col == Run.player_col
var sz := _BOSS_SIZE if node.level.is_boss else _NODE_SIZE
var center := _local(node, size)
var btn := Button.new()
btn.custom_minimum_size = Vector2(sz, sz)
btn.size = Vector2(sz, sz)
btn.position = center - Vector2(sz, sz) * 0.5
btn.focus_mode = Control.FOCUS_NONE
btn.disabled = not is_reachable
btn.mouse_filter = Control.MOUSE_FILTER_STOP if is_reachable else Control.MOUSE_FILTER_IGNORE
var fill := node.level.color
var border := UiTheme.BORDER
if is_current:
border = _CREAM
elif is_reachable:
border = _GOLD
elif node.cleared:
fill = fill.darkened(0.35)
border = Color(_GOLD.r, _GOLD.g, _GOLD.b, 0.5)
else: # locked — a future row not yet reachable
fill = fill.darkened(0.6)
border = Color(0.40, 0.34, 0.20, 0.6)
var border_w := 4 if (is_reachable or is_current) else 2
var normal := UiTheme.disc(fill, border, border_w)
var hover := UiTheme.disc(fill.lightened(0.18), _CREAM, border_w)
btn.add_theme_stylebox_override("normal", normal)
btn.add_theme_stylebox_override("hover", hover)
btn.add_theme_stylebox_override("pressed", hover)
btn.add_theme_stylebox_override("disabled", normal)
_map_area.add_child(btn)
if is_reachable:
btn.pressed.connect(_enter_node.bind(node))
_pulse(btn)
if is_reachable or is_current or node.level.is_boss:
_add_caption(node.level.display_name, center + Vector2(0.0, sz * 0.5 + 6.0), is_reachable)
if is_current:
_add_caption("You are here", center - Vector2(0.0, sz * 0.5 + 18.0), false)
func _add_caption(text: String, center_top: Vector2, bright: bool) -> void:
var lbl := Label.new()
lbl.text = text
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lbl.size = Vector2(200.0, 0.0)
lbl.position = center_top - Vector2(100.0, 0.0)
lbl.add_theme_font_override("font", UiFonts.body())
lbl.add_theme_font_size_override("font_size", 15)
lbl.add_theme_color_override("font_color", _CREAM if bright else UiTheme.MUTED)
lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
lbl.add_theme_constant_override("outline_size", 4)
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
_map_area.add_child(lbl)
# Slow glow on the reachable pips so the eye lands on where the player can go next.
func _pulse(node: CanvasItem) -> void:
var tw := create_tween().set_loops()
tw.tween_property(node, "modulate:a", 0.6, 0.75).set_trans(Tween.TRANS_SINE)
tw.tween_property(node, "modulate:a", 1.0, 0.75).set_trans(Tween.TRANS_SINE)
# ── Actions ───────────────────────────────────────────────────────────────────
func _enter_node(node: MapNode) -> void:
Run.select_node(node)
get_tree().paused = false
get_tree().change_scene_to_file(node.level.scene_path)
func _to_menu() -> void:
get_tree().paused = false
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
get_tree().change_scene_to_file(_MENU_SCENE)
# ── Run-complete overlay ──────────────────────────────────────────────────────
func _build_completion() -> void:
_completion = PanelContainer.new()
_completion.set_anchors_preset(Control.PRESET_CENTER)
_completion.grow_horizontal = Control.GROW_DIRECTION_BOTH
_completion.grow_vertical = Control.GROW_DIRECTION_BOTH
_completion.add_theme_stylebox_override(
"panel", UiTheme.plaque(UiTheme.LEATHER, _GOLD, 4, 32.0, 24.0)
)
_completion.visible = false
add_child(_completion)
var col := VBoxContainer.new()
col.add_theme_constant_override("separation", 16)
col.alignment = BoxContainer.ALIGNMENT_CENTER
_completion.add_child(col)
var heading := Label.new()
heading.text = "Run Complete!"
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
heading.add_theme_font_override("font", UiFonts.title())
heading.add_theme_font_size_override("font_size", 40)
heading.add_theme_color_override("font_color", _GOLD)
heading.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
heading.add_theme_constant_override("outline_size", 5)
col.add_child(heading)
var row := HBoxContainer.new()
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 16)
col.add_child(row)
var again := _make_button("New Run")
again.pressed.connect(_new_run)
row.add_child(again)
var menu := _make_button("Main Menu")
menu.pressed.connect(_to_menu)
row.add_child(menu)
func _new_run() -> void:
Run.start_new_run()
_relayout()
# Shared thematic button, matching the HUD / menu plaques.
func _make_button(text: String) -> Button:
var btn := Button.new()
btn.text = text
btn.focus_mode = Control.FOCUS_NONE
btn.add_theme_stylebox_override(
"normal", UiTheme.plaque(Color(0.10, 0.07, 0.03, 0.90), _GOLD, 3, 18.0, 9.0)
)
var hover := UiTheme.plaque(Color(0.16, 0.10, 0.04, 0.96), _CREAM, 3, 18.0, 9.0)
btn.add_theme_stylebox_override("hover", hover)
btn.add_theme_stylebox_override("pressed", hover)
btn.add_theme_font_override("font", UiFonts.body())
btn.add_theme_color_override("font_color", _GOLD)
btn.add_theme_color_override("font_hover_color", _CREAM)
btn.add_theme_font_size_override("font_size", 18)
return btn
+1
View File
@@ -0,0 +1 @@
uid://dt3ffakbwlkmc
+90
View File
@@ -0,0 +1,90 @@
extends Node3D
signal matador_killed
signal all_defeated
const MATADOR := preload("res://Matador.tscn")
var _alive: int = 0
var _wave: int = 0
var _resolved: bool = false
var _spawn_points: Array[Node3D] = []
func _ready() -> void:
add_to_group(&"matador_spawn")
_spawn.call_deferred()
# Each spawner node is a spawn-point marker. The first in tree order (the one the HUD
# connects its all_defeated to) runs the wave logic and uses every marker — itself and
# its siblings — as an entry point; the rest stay passive so waves aren't run four times.
func _spawn() -> void:
var markers := get_tree().get_nodes_in_group(&"matador_spawn")
if markers.is_empty() or markers[0] != self:
return
for node: Node in markers:
if node is Node3D:
_spawn_points.append(node as Node3D)
_place_bull(randf() * TAU, DP.f("arena_spawn_radius"))
_start_next_wave()
# Wave 1 sends 1 matador, wave 2 sends 2, … up to the number of placed spawn points.
# Matadors are assigned to a random subset of spawn points each wave and enter staggered.
func _start_next_wave() -> void:
_wave += 1
# 8 waves total.
if _wave > DP.f("mat_wave_size"):
return
# Wave 1 = 1 matador, Wave 2 = 2, ... Wave 8 = 8.
var count := _wave
# Shuffle the spawn points so the spawn locations are random.
var points: Array[Node3D] = _spawn_points.duplicate()
points.shuffle()
for i in range(count):
# Reuse spawn points when we have more matadors than points.
var sp: Node3D = points[i % points.size()]
var m := _spawn_at(sp.global_position)
m.call_deferred(&"enter_spawn_roll", Vector3.ZERO)
if i < count - 1:
await get_tree().create_timer(
DP.f("mat_spawn_stagger")
).timeout
func _place_bull(angle: float, arena_r: float) -> void:
var bull := get_tree().get_first_node_in_group(&"player") as Node3D
if not bull:
return
bull.global_position = Vector3(
cos(angle) * arena_r, bull.global_position.y, sin(angle) * arena_r
)
func _spawn_at(pos: Vector3) -> Node3D:
var m: Node3D = MATADOR.instantiate()
m.position = pos
get_parent().add_child(m)
m.killed.connect(_on_matador_killed)
_alive += 1
return m
func _on_matador_killed() -> void:
matador_killed.emit()
_alive -= 1
if _alive > 0 or _resolved:
return
if _wave >= DP.f("mat_wave_size"):
_resolved = true
all_defeated.emit()
else:
await get_tree().create_timer(DP.f("mat_wave_pause")).timeout
_start_next_wave()
+267
View File
@@ -0,0 +1,267 @@
extends Node
## Persistent run progression across scene changes — the Slay-the-Spire-style map and
## where the player sits on it. The arena scene is reloaded for every fight, so this
## state must outlive scene changes: genuinely global run state earns an autoload (`Run`).
##
## Flow: menu Play → start_new_run() → arena (intro fight) → win → MapScreen shows the
## first frontier → pick a node → its level scene → win → back to MapScreen advanced one
## row, and on until a boss node ends the map. Every node is the arena for now; adding a
## level later is a new LevelDef in the catalog and pointing the generator at it.
signal run_started
# Every level loads the same reusable shell (player, camera, HUD, PS1 filter); the shell
# then instances the level's own `level_scene` module into its LevelRoot.
const _SHELL_SCENE := "res://scene.tscn"
const _ARENA_LEVEL := preload("res://levels/arena_level.tscn")
const _BEAR_LEVEL := preload("res://levels/bear_level.tscn")
const _LOBBY_LEVEL := preload("res://levels/lobby_level.tscn")
# Map shape. Small + placeholder for now; the final row is a single boss node.
const _ROWS := 5
const _MIN_PER_ROW := 2
const _MAX_PER_ROW := 4
## map[row] is an Array[MapNode]; empty until a run starts.
var map: Array = []
## Node the player currently occupies. (-1, -1) = the start gate below the first row —
## i.e. straight after the intro fight, before any map node has been chosen.
var player_row: int = -1
var player_col: int = -1
## Node picked on the map whose level is being played now; committed to on the next win.
var pending_row: int = -1
var pending_col: int = -1
## Debug override — when set, active_level() returns this regardless of map state.
## Set via the console `level` command; cleared on start_new_run().
var debug_level_override: LevelDef = null
## The lobby is an interstitial hub between fights, not a map node: the player is sent
## here after clearing a level (go_to_lobby()) to move around freely before choosing the
## next node. While set, active_level() serves the lobby; cleared once a node is picked
## (select_node) or a fresh run begins.
var enter_lobby: bool = false
# Level catalog. Every level shares the shell scene; what changes is the level module
# (its geometry + spawners) and the opponent. The arena (matador wave, normal + boss
# flavours) and the bear (a lone boss enemy that can turn up as a normal node) live here.
# Future levels — china shop — slot in the same way: a new module + a LevelDef entry.
const _BEAR_SCENE := preload("res://Bear.tscn")
var _arena: LevelDef
var _arena_boss: LevelDef
var _bear: LevelDef
var _lobby: LevelDef
func _ready() -> void:
_arena = _make_level(&"arena", "The Arena", _ARENA_LEVEL, Color(0.80, 0.62, 0.24), false)
_arena_boss = _make_level(
&"arena_boss", "Grand Arena", _ARENA_LEVEL, Color(0.88, 0.30, 0.20), true
)
_bear = _make_level(&"bear", "Bear's Den", _BEAR_LEVEL, Color(0.55, 0.38, 0.22), false)
_bear.enemy_scene = _BEAR_SCENE
# The lobby is a fightless hub — no enemy_scene, so no wave spawns and no win/lose.
_lobby = _make_level(&"lobby", "Lobby", _LOBBY_LEVEL, Color(0.62, 0.66, 0.72), false)
func _make_level(
id: StringName, level_name: String, level_scene: PackedScene, color: Color, boss: bool
) -> LevelDef:
var l := LevelDef.new()
l.id = id
l.display_name = level_name
l.scene_path = _SHELL_SCENE
l.level_scene = level_scene
l.color = color
l.is_boss = boss
return l
## The level being fought right now: debug override wins, then the pending/current map
## node, falling back to the plain arena for the intro fight or a direct scene launch.
func active_level() -> LevelDef:
if debug_level_override != null:
return debug_level_override
if enter_lobby:
return _lobby
var n := node_at(pending_row, pending_col)
if n == null:
n = current_node()
return n.level if n != null and n.level != null else _arena
## Return a level by id, or null if unknown. Used by the debug console `level` command.
func get_level(id: StringName) -> LevelDef:
match id:
&"arena": return _arena
&"arena_boss": return _arena_boss
&"bear": return _bear
&"lobby": return _lobby
return null
## All known level ids — shown by `level` in the console.
func level_ids() -> Array[StringName]:
return [&"arena", &"arena_boss", &"bear", &"lobby"]
## Begin a fresh run: build a new map and place the player at the start gate. Called from
## the main-menu Play button, not from the arena scene (which reloads for every fight).
func start_new_run() -> void:
_generate_map()
player_row = -1
player_col = -1
pending_row = -1
pending_col = -1
debug_level_override = null
enter_lobby = false
run_started.emit()
## Build a run if none exists — a safety net for entering the map or arena directly (e.g.
## running scene.tscn straight from the editor) without coming through the menu.
func ensure_run() -> void:
if map.is_empty():
start_new_run()
func has_run() -> bool:
return not map.is_empty()
func node_at(row: int, col: int) -> MapNode:
if row < 0 or row >= map.size():
return null
var r: Array = map[row]
if col < 0 or col >= r.size():
return null
return r[col]
func current_node() -> MapNode:
return node_at(player_row, player_col)
## Nodes the player may pick next: the whole first row from the start gate, otherwise the
## nodes the current node links forward to.
func reachable() -> Array:
if map.is_empty():
return []
if player_row < 0:
return (map[0] as Array).duplicate()
var here := current_node()
if here == null or player_row + 1 >= map.size():
return []
var out: Array = []
for c: int in here.links:
var n := node_at(player_row + 1, c)
if n != null:
out.append(n)
return out
## True once the player has cleared a boss node — the map is finished.
func run_complete() -> bool:
var here := current_node()
return here != null and here.level != null and here.level.is_boss
## Mark a node as the one being played (its level scene is about to load).
func select_node(node: MapNode) -> void:
if node == null:
return
enter_lobby = false
pending_row = node.row
pending_col = node.col
## Send the player to the lobby hub instead of a fight — active_level() serves the lobby
## until a node is picked. Called after clearing a level so the player lands in the lobby
## rather than jumping straight to the next fight.
func go_to_lobby() -> void:
enter_lobby = true
## Commit the level just won: move the player onto the pending node (or leave them at the
## start gate after the intro fight, which has no pending node).
func complete_current_level() -> void:
if pending_row >= 0:
player_row = pending_row
player_col = pending_col
var n := current_node()
if n != null:
n.cleared = true
pending_row = -1
pending_col = -1
# ── Map generation ────────────────────────────────────────────────────────────
# A small branching lattice: each row gets a handful of nodes, each node links to the
# nearest node(s) in the next row, and every next-row node is guaranteed a parent so the
# path is always connected (no orphan the player can never reach). The final row is a
# single boss node the whole map funnels toward.
func _generate_map() -> void:
map.clear()
for r: int in _ROWS:
var count := 1 if r == _ROWS - 1 else randi_range(_MIN_PER_ROW, _MAX_PER_ROW)
var y := 1.0 - float(r + 1) / float(_ROWS + 1) # row 0 near the bottom, boss on top
var row_nodes: Array = []
for c: int in count:
var node := MapNode.new()
node.row = r
node.col = c
node.pos = Vector2(_column_x(c, count), y)
# Boss row is always the Grand Arena; normal nodes are a mix of matador
# arenas and the occasional Bear's Den for variety.
if r == _ROWS - 1:
node.level = _arena_boss
else:
node.level = _bear if randf() < 0.35 else _arena
row_nodes.append(node)
map.append(row_nodes)
_link_rows()
func _column_x(col: int, count: int) -> float:
if count <= 1:
return 0.5
return lerpf(0.18, 0.82, float(col) / float(count - 1))
func _link_rows() -> void:
for r: int in _ROWS - 1:
var cur: Array = map[r]
var nxt: Array = map[r + 1]
var incoming := PackedInt32Array()
incoming.resize(nxt.size()) # zero-filled
for from_node: MapNode in cur:
var nearest := _nearest_index(from_node, nxt)
_add_link(from_node, nearest, incoming)
# Occasionally fan out to an adjacent next-row node so paths branch.
if nxt.size() > 1 and randf() < 0.45:
var step := 1 if randf() < 0.5 else -1
_add_link(from_node, clampi(nearest + step, 0, nxt.size() - 1), incoming)
# Guarantee every next-row node has at least one parent (no orphans).
for c: int in nxt.size():
if incoming[c] == 0:
_add_link(cur[_nearest_index(nxt[c], cur)], c, incoming)
func _add_link(from_node: MapNode, to_col: int, incoming: PackedInt32Array) -> void:
if to_col in from_node.links:
return
from_node.links.append(to_col)
incoming[to_col] += 1
# Index of the node in `others` whose column position is closest to `node` on the x axis —
# used both to link a node forward and to adopt an orphan back to its nearest parent.
func _nearest_index(node: MapNode, others: Array) -> int:
var best := 0
var best_d := INF
for i: int in others.size():
var d: float = absf((others[i] as MapNode).pos.x - node.pos.x)
if d < best_d:
best_d = d
best = i
return best
+1
View File
@@ -0,0 +1 @@
uid://u4fartm52bk3
+68 -6
View File
@@ -190,13 +190,74 @@ func _process(_delta: float) -> void:
play_button.anchor_bottom = cape.y play_button.anchor_bottom = cape.y
# Player-facing option toggles, seated above the rebind list in the Options panel: # Player-facing option toggles and audio sliders, seated above the rebind list.
# the master screen-shake switch and the gore toggle (both persisted via Settings).
func _build_options_toggles() -> void: func _build_options_toggles() -> void:
var scroll_idx := options_scroll.get_index() var idx := options_scroll.get_index()
# Insert in reverse so both land directly above the rebind scroll, in listed order. _add_section_label("── Audio ──", idx); idx += 1
_add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, scroll_idx) _add_slider_row("Master Volume", Settings.volume_master, Settings.set_volume_master, idx); idx += 1
_add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, scroll_idx) _add_slider_row("Ambient Volume", Settings.volume_ambient, Settings.set_volume_ambient, idx); idx += 1
_add_slider_row("Effects Volume", Settings.volume_effects, Settings.set_volume_effects, idx); idx += 1
_add_toggle_row("Mute All", Settings.mute_all, Settings.set_mute_all, idx); idx += 1
_add_section_label("── Gameplay ──", idx); idx += 1
_add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, idx); idx += 1
_add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, idx); idx += 1
_add_section_label("── Controls ──", idx)
func _add_section_label(text: String, at_index: int) -> void:
var lbl := Label.new()
lbl.text = text
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
lbl.add_theme_font_override("font", UiFonts.title())
lbl.add_theme_font_size_override("font_size", 18)
lbl.add_theme_color_override("font_color", MENU_GOLD)
lbl.add_theme_color_override("font_outline_color", Color.BLACK)
lbl.add_theme_constant_override("outline_size", 4)
options_vbox.add_child(lbl)
options_vbox.move_child(lbl, at_index)
func _add_slider_row(label_text: String, value: float, setter: Callable, at_index: int) -> void:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var name_label := Label.new()
name_label.text = label_text
name_label.custom_minimum_size = Vector2(160, 0)
name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
name_label.add_theme_font_override("font", UiFonts.body())
name_label.add_theme_font_size_override("font_size", 20)
name_label.add_theme_color_override("font_color", MENU_CREAM)
name_label.add_theme_color_override("font_outline_color", Color.BLACK)
name_label.add_theme_constant_override("outline_size", 5)
row.add_child(name_label)
var slider := HSlider.new()
slider.min_value = 0.0
slider.max_value = 1.0
slider.step = 0.01
slider.value = value
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
slider.custom_minimum_size = Vector2(0, 28)
row.add_child(slider)
var pct_label := Label.new()
pct_label.text = "%d%%" % roundi(value * 100.0)
pct_label.custom_minimum_size = Vector2(46, 0)
pct_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
pct_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
pct_label.add_theme_font_override("font", UiFonts.body())
pct_label.add_theme_font_size_override("font_size", 18)
pct_label.add_theme_color_override("font_color", MENU_GOLD)
row.add_child(pct_label)
slider.value_changed.connect(func(v: float) -> void:
setter.call(v)
pct_label.text = "%d%%" % roundi(v * 100.0))
options_vbox.add_child(row)
options_vbox.move_child(row, at_index)
func _add_toggle_row(label_text: String, value: bool, setter: Callable, at_index: int) -> void: func _add_toggle_row(label_text: String, value: bool, setter: Callable, at_index: int) -> void:
@@ -254,6 +315,7 @@ func _build_rebind_rows() -> void:
func _on_play_pressed() -> void: func _on_play_pressed() -> void:
_set_buttons_disabled(true) _set_buttons_disabled(true)
Run.start_new_run() # fresh map for this run; the arena is the intro fight
var tween := create_tween() var tween := create_tween()
tween.tween_property(fade_rect, "color:a", 1.0, FADE_TIME) tween.tween_property(fade_rect, "color:a", 1.0, FADE_TIME)
tween.tween_callback(func() -> void: tween.tween_callback(func() -> void:
+33 -1
View File
@@ -35,6 +35,7 @@ var _swing_timer: float = 0.0
var _lunge_timer: float = 0.0 var _lunge_timer: float = 0.0
var _roll_timer: float = 0.0 var _roll_timer: float = 0.0
var _roll_dir: Vector3 = Vector3.ZERO var _roll_dir: Vector3 = Vector3.ZERO
var _is_spawning: bool = false
var _throw_timer: float = 0.0 var _throw_timer: float = 0.0
var _throw_aim_cd: float = 0.0 var _throw_aim_cd: float = 0.0
var _throw_dir: Vector3 = Vector3.ZERO var _throw_dir: Vector3 = Vector3.ZERO
@@ -60,6 +61,7 @@ var _blood_burst: CPUParticles3D = null
@onready var _mesh: Node3D = $matador2 @onready var _mesh: Node3D = $matador2
@onready var _hit_area: Area3D = $HitArea @onready var _hit_area: Area3D = $HitArea
const _ANIM_RUN: StringName = &"Run" const _ANIM_RUN: StringName = &"Run"
const _ANIM_IDLE: StringName = &"Taunt" const _ANIM_IDLE: StringName = &"Taunt"
const _ANIM_ATTACK: StringName = &"Attack" const _ANIM_ATTACK: StringName = &"Attack"
@@ -92,12 +94,19 @@ const _TURN_FACE: float = 16.0 # face the bull while engaging
const _TURN_SHARP: float = 24.0 # snap onto a sidestep / roll direction const _TURN_SHARP: float = 24.0 # snap onto a sidestep / roll direction
const RigidSkin = preload("res://rigid_skin.gd")
func _ready() -> void: func _ready() -> void:
add_to_group(&"matador") add_to_group(&"matador")
_skeleton = _find_skeleton(_mesh) _skeleton = _find_skeleton(_mesh)
_anim_player = _find_anim_player(_mesh) _anim_player = _find_anim_player(_mesh)
if _skeleton: if _skeleton:
_sim = MatadorRagdoll.build(_skeleton) _sim = MatadorRagdoll.build(_skeleton)
# Web (Mali/ANGLE) can't run Compatibility vertex-skinning; rebuild as non-skinned
# bone-attached pieces. Runs after the ragdoll sim is built (both drive the same bones).
if OS.has_feature("web"):
RigidSkin.convert_tree(self)
if _anim_player: if _anim_player:
for anim in [_ANIM_RUN, _ANIM_ATTACK] + _ANIM_TAUNTS: for anim in [_ANIM_RUN, _ANIM_ATTACK] + _ANIM_TAUNTS:
_ensure_loop(anim) _ensure_loop(anim)
@@ -118,11 +127,13 @@ func _ready() -> void:
_ole_player = AudioStreamPlayer.new() _ole_player = AudioStreamPlayer.new()
_ole_player.stream = load("res://sounds/ole.ogg") _ole_player.stream = load("res://sounds/ole.ogg")
_ole_player.volume_db = 0.0 _ole_player.volume_db = 0.0
_ole_player.bus = &"Effects"
add_child(_ole_player) add_child(_ole_player)
if ResourceLoader.exists("res://sounds/matador_death.ogg"): if ResourceLoader.exists("res://sounds/matador_death.ogg"):
_death_player = AudioStreamPlayer.new() _death_player = AudioStreamPlayer.new()
_death_player.stream = load("res://sounds/matador_death.ogg") _death_player.stream = load("res://sounds/matador_death.ogg")
_death_player.volume_db = 2.0 _death_player.volume_db = 2.0
_death_player.bus = &"Effects"
add_child(_death_player) add_child(_death_player)
@@ -143,6 +154,10 @@ func _acquire_bull() -> void:
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
_acquire_bull() _acquire_bull()
if _is_spawning and is_on_floor():
_is_spawning = false
collision_layer = 1
if DP.b("show_raycasts") and _steer_dir != Vector3.ZERO: if DP.b("show_raycasts") and _steer_dir != Vector3.ZERO:
var o := global_position + Vector3.UP * 0.6 var o := global_position + Vector3.UP * 0.6
DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9)) DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9))
@@ -537,7 +552,7 @@ func _tick_attack(delta: float) -> void:
if _swing_timer > 0.0: if _swing_timer > 0.0:
_lunge_timer = maxf(0.0, _lunge_timer - delta) _lunge_timer = maxf(0.0, _lunge_timer - delta)
if _lunge_timer > 0.0 and dist > 0.1: if _lunge_timer > 0.0 and dist > DP.f("mat_attack_min_dist"):
_accelerate(to_bull.normalized(), DP.f("mat_lunge_speed"), delta) _accelerate(to_bull.normalized(), DP.f("mat_lunge_speed"), delta)
else: else:
_decelerate(22.0, delta) _decelerate(22.0, delta)
@@ -564,6 +579,23 @@ func _enter_roll(hit_dir: Vector3) -> void:
_anim_player.seek(0.0, true) _anim_player.seek(0.0, true)
# Called by the spawner (deferred) to make the matador leap-roll toward a target
# point on entry — the cinematic arena entrance. After the roll it transitions to
# normal AI via the standard ROLL → FLEE path.
func enter_spawn_roll(toward: Vector3) -> void:
var dir := toward - global_position
dir.y = 0.0
_roll_dir = dir.normalized() if dir.length() > 0.1 else -_mesh.global_transform.basis.z
_state = State.ROLL
_roll_timer = _anim_length(_ANIM_ROLL, DP.f("mat_roll_duration"))
_is_spawning = true
collision_layer = 0
velocity.y = 4.0
if _anim_player and _anim_player.has_animation(_ANIM_ROLL):
_anim_player.play(_ANIM_ROLL)
_anim_player.seek(0.0, true)
func _tick_roll(delta: float) -> void: func _tick_roll(delta: float) -> void:
_roll_timer -= delta _roll_timer -= delta
if _roll_timer <= 0.0: if _roll_timer <= 0.0:
-142
View File
@@ -1,142 +0,0 @@
extends Node3D
signal matador_killed
signal all_defeated
const MATADOR := preload("res://Matador.tscn")
var _spawn_count: int = 1
var _alive: int = 0
var _kills: int = 0
var _resolved: bool = false
# Flat (x,z) positions of the most recent batch spawned. A fresh wave keeps at least
# _SPAWN_MIN_SEP away from these so it never lands on top of where the last one did.
const _SPAWN_MIN_SEP: float = 3.5
var _last_spawn_positions: Array[Vector3] = []
func _ready() -> void:
add_to_group(&"matador_spawn")
_spawn_count = maxi(1, int(DP.f("mat_spawn_count")))
# Deferred so it's safe from _ready and matches the previous spawn timing.
_spawn.call_deferred()
func _spawn() -> void:
# Bull and matadors start at opposite ends of a random diameter, so they always
# begin as far apart as the arena allows (edge-to-edge across the centre).
var arena_r := DP.f("arena_spawn_radius")
var bull_angle := randf() * TAU
_place_bull(bull_angle, arena_r)
var placed: Array[Vector3] = _spread_positions(_spawn_count, bull_angle + PI, arena_r)
for pos: Vector3 in placed:
_spawn_at(pos)
_last_spawn_positions = placed
func _place_bull(angle: float, arena_r: float) -> void:
var bull := get_tree().get_first_node_in_group(&"player") as Node3D
if not bull:
return
bull.global_position = Vector3(
cos(angle) * arena_r, bull.global_position.y, sin(angle) * arena_r
)
# Positions for the matadors, spread across the far side (centred on `center_angle`)
# so every matador stays near the point diametrically opposite the bull. Each slot gets
# its own angular band and a staggered radius, then rejection-samples inside that band so
# no two matadors land closer than _SPAWN_MIN_SEP — otherwise they pile up on top of
# each other on spawn. The arc widens with the crowd so a big wave doesn't get cramped.
func _spread_positions(count: int, center_angle: float, arena_r: float) -> Array[Vector3]:
var positions: Array[Vector3] = []
if count <= 1:
positions.append(Vector3(cos(center_angle) * arena_r, 1.0, sin(center_angle) * arena_r))
return positions
var arc := clampf(TAU * 0.33 + float(count) * 0.25, TAU * 0.33, TAU * 0.92)
var slot := arc / float(count)
var inner := maxf(2.5, arena_r * 0.5)
for i: int in count:
# Give each matador its own angular slot for an even fan, but sample the radius
# freely across the whole far-side band so the rejection test can push crowded
# neighbours onto a different ring instead of piling them at one distance.
var base_angle := center_angle - arc * 0.5 + slot * (float(i) + 0.5)
var pos := Vector3(cos(base_angle) * arena_r, 1.0, sin(base_angle) * arena_r)
for attempt: int in 16:
var angle := base_angle + randf_range(-0.5, 0.5) * slot
var radius := randf_range(inner, arena_r)
pos = Vector3(cos(angle) * radius, 1.0, sin(angle) * radius)
if _clear_of(pos, positions, _SPAWN_MIN_SEP):
break
positions.append(pos)
return positions
func _spawn_at(pos: Vector3, generation: int = 0) -> void:
var m: Node3D = MATADOR.instantiate()
m.position = pos
m.set_meta(&"generation", generation)
get_parent().add_child(m)
m.killed.connect(_on_matador_killed.bind(m))
_alive += 1
# A slain matador is replaced by mat_split_factor fresh ones on the spot (a hydra),
# so the horde doubles each generation, up to mat_split_generations deep. The bull
# wins only when a whole generation dies out without splitting — i.e. once the spawns
# stop and the last body drops.
func _on_matador_killed(dead: Node3D) -> void:
matador_killed.emit()
_alive -= 1
_kills += 1
_split(dead)
var win_kills := int(DP.f("mat_win_kills"))
var reached_quota := win_kills > 0 and _kills >= win_kills
if (reached_quota or _alive <= 0) and not _resolved:
_resolved = true
all_defeated.emit()
func _split(dead: Node3D) -> void:
var count := int(DP.f("mat_split_factor"))
var generation := int(dead.get_meta(&"generation", 0))
if count < 2 or generation >= int(DP.f("mat_split_generations")):
return
var arena_r := DP.f("arena_spawn_radius")
var origin := dead.global_position
origin.y = 0.0
var wave: Array[Vector3] = []
for i: int in count:
var pos := _pick_spawn(origin, arena_r, wave)
wave.append(pos)
_spawn_at(pos, generation + 1)
_last_spawn_positions = wave
# Rejection-sample a spawn point ringed around `origin`, pushing the ring outward on
# each failed attempt until it clears both the previous wave (_last_spawn_positions)
# and the spots already taken this wave — so a fresh wave never lands where the last
# one did. Falls back to the last candidate if the arena is too cramped to satisfy it.
func _pick_spawn(origin: Vector3, arena_r: float, taken: Array[Vector3]) -> Vector3:
var pos := origin
for attempt: int in 12:
var angle := randf() * TAU
var r := randf_range(2.0, 3.5) + float(attempt) * 0.4
pos = origin + Vector3(cos(angle) * r, 0.0, sin(angle) * r)
var flat := Vector2(pos.x, pos.z)
if flat.length() > arena_r:
flat = flat.normalized() * arena_r
pos = Vector3(flat.x, 0.0, flat.y)
if _clear_of(pos, _last_spawn_positions, _SPAWN_MIN_SEP) \
and _clear_of(pos, taken, _SPAWN_MIN_SEP):
break
pos.y = 1.0
return pos
func _clear_of(pos: Vector3, others: Array[Vector3], min_sep: float) -> bool:
for o: Vector3 in others:
if Vector2(pos.x - o.x, pos.z - o.z).length() < min_sep:
return false
return true
+63
View File
@@ -0,0 +1,63 @@
extends CanvasLayer
## Universal "rotate to landscape" prompt (autoload `OrientationGuard`). The cross-device
## half of landscape enforcement: screen.orientation.lock (controls_manager.gd) handles
## Android, but iOS Safari has no lock API, and some browsers only lock in fullscreen — so
## wherever the device ends up portrait, we simply ask the player to turn it. No canvas
## rotation or input remapping (those break touch coordinates), so it works on every phone
## and browser cleanly. Only ever shows on touch-primary devices, only while portrait.
var _panel: ColorRect
func _ready() -> void:
layer = 128 # above HUD, touch UI, everything
process_mode = Node.PROCESS_MODE_ALWAYS
_panel = ColorRect.new()
_panel.color = Color(0.05, 0.03, 0.02, 0.97)
_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
_panel.mouse_filter = Control.MOUSE_FILTER_STOP # swallow taps to the game behind it
_panel.visible = false
add_child(_panel)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
_panel.add_child(center)
var box := VBoxContainer.new()
box.alignment = BoxContainer.ALIGNMENT_CENTER
box.add_theme_constant_override("separation", 14)
center.add_child(box)
var title := Label.new()
title.text = "Rotate your device"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 44)
title.add_theme_color_override("font_color", Color(1.0, 0.78, 0.22))
box.add_child(title)
var sub := Label.new()
sub.text = "Turn it sideways to play in landscape"
sub.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
sub.add_theme_font_size_override("font_size", 22)
sub.add_theme_color_override("font_color", Color(0.85, 0.85, 0.85))
box.add_child(sub)
get_viewport().size_changed.connect(_update)
_update.call_deferred()
func _update() -> void:
_panel.visible = _should_guard() and _is_portrait()
func _is_portrait() -> bool:
var s := get_viewport().get_visible_rect().size
return s.y > s.x
# Touch-primary devices only — never a desktop browser with a tall window (they can't rotate
# a monitor). Reuses the same conservative detection the touch UI uses.
func _should_guard() -> bool:
return Controls.use_touch_ui()
+1
View File
@@ -0,0 +1 @@
uid://cmh7kd85wi5gx
+74 -16
View File
@@ -14,7 +14,9 @@ const _ANIM_IDLE: StringName = &"Armature|IDLE"
const _ANIM_KICK: StringName = &"Armature|KICK" const _ANIM_KICK: StringName = &"Armature|KICK"
const _ANIM_SLAM: StringName = &"Armature|SLAM" const _ANIM_SLAM: StringName = &"Armature|SLAM"
const _ANIM_DASH: StringName = &"Armature|DASH" const _ANIM_DASH: StringName = &"Armature|DASH"
const _ANIM_WALK: StringName = &"Armature|WALK"
const _ANIM_ROLL: StringName = &"Armature|ROLL" const _ANIM_ROLL: StringName = &"Armature|ROLL"
const _ANIM_DEATH: StringName = &"Armature|DEATH"
@onready var camera_pivot: Node3D = $Camera3D @onready var camera_pivot: Node3D = $Camera3D
@onready var cube_guy: Node3D = $bull @onready var cube_guy: Node3D = $bull
@@ -77,8 +79,18 @@ var _walk_dust_ramp: Gradient = null
var _charge_dust_ramp: Gradient = null var _charge_dust_ramp: Gradient = null
const RigidSkin = preload("res://rigid_skin.gd")
var _hit_flash_tween: Tween
func _ready() -> void: func _ready() -> void:
add_to_group(&"player") add_to_group(&"player")
# Mali/ANGLE mobile GPUs can't run Godot's Compatibility vertex-skinning (transform
# feedback) — skinned meshes render invisible on the web build. Rebuild the bull as
# non-skinned bone-attached pieces there; desktop keeps smooth GPU skinning.
if OS.has_feature("web"):
RigidSkin.convert_tree(self)
_max_hp = maxi(1, int(DP.f("bull_max_hp"))) _max_hp = maxi(1, int(DP.f("bull_max_hp")))
_hp = _max_hp _hp = _max_hp
_cube_scale = cube_guy.scale _cube_scale = cube_guy.scale
@@ -100,16 +112,16 @@ func _setup_legs() -> void:
func _setup_hoof_dust() -> void: func _setup_hoof_dust() -> void:
var sphere := _particle_sphere(0.10, 4, 2, _unshaded_material()) var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material())
_walk_dust_ramp = Gradient.new() _walk_dust_ramp = Gradient.new()
_walk_dust_ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0)) _walk_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
_walk_dust_ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0)) _walk_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
# Light blue dust while charging # White-beige dust while running
_charge_dust_ramp = Gradient.new() _charge_dust_ramp = Gradient.new()
_charge_dust_ramp.set_color(0, Color(0.20, 0.40, 0.85, 1.0)) _charge_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
_charge_dust_ramp.set_color(1, Color(0.20, 0.40, 0.85, 0.0)) _charge_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
var scale_curve := Curve.new() var scale_curve := Curve.new()
scale_curve.add_point(Vector2(0.0, 0.2)) scale_curve.add_point(Vector2(0.0, 0.2))
@@ -213,6 +225,7 @@ func _setup_audio() -> void:
_huff_player = AudioStreamPlayer.new() _huff_player = AudioStreamPlayer.new()
_huff_player.stream = load("res://sounds/bull_huff.ogg") _huff_player.stream = load("res://sounds/bull_huff.ogg")
_huff_player.volume_db = 0.0 _huff_player.volume_db = 0.0
_huff_player.bus = &"Effects"
add_child(_huff_player) add_child(_huff_player)
if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"): if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"):
_crowd_player = AudioStreamPlayer.new() _crowd_player = AudioStreamPlayer.new()
@@ -222,6 +235,7 @@ func _setup_audio() -> void:
_crowd_player.stream = s _crowd_player.stream = s
_crowd_player.volume_db = -14.0 _crowd_player.volume_db = -14.0
_crowd_player.autoplay = true _crowd_player.autoplay = true
_crowd_player.bus = &"Ambient"
add_child(_crowd_player) add_child(_crowd_player)
@@ -251,8 +265,8 @@ func _set_dust_state(new_state: DustState) -> void:
p.initial_velocity_min = DP.f("charge_vel_min") p.initial_velocity_min = DP.f("charge_vel_min")
p.initial_velocity_max = DP.f("charge_vel_max") p.initial_velocity_max = DP.f("charge_vel_max")
p.emitting = true p.emitting = true
if _charge_trail_emitter: # Blue energy trail belongs to the roll/dash ability only, not to fast WASD
_charge_trail_emitter.emitting = _dust_state == DustState.CHARGE # running — driven explicitly in the roll loop / _end_roll, not by dust state.
func _on_dp_changed(_key: String, _val: Variant) -> void: func _on_dp_changed(_key: String, _val: Variant) -> void:
@@ -294,7 +308,7 @@ func _play_idle() -> void:
return return
_bull_anim.speed_scale = 1.0 _bull_anim.speed_scale = 1.0
if _bull_anim.has_animation(_ANIM_IDLE): if _bull_anim.has_animation(_ANIM_IDLE):
_bull_anim.play(_ANIM_IDLE, 0.2) _bull_anim.play(_ANIM_IDLE, 0.05)
# Drive the WASD locomotion clip: the looping DASH doubles as the run cycle while a # Drive the WASD locomotion clip: the looping DASH doubles as the run cycle while a
@@ -306,9 +320,11 @@ func _update_locomotion_anim(moving: bool) -> void:
return return
_locomotion_moving = moving _locomotion_moving = moving
if moving: if moving:
if _bull_anim.has_animation(_ANIM_DASH): if _bull_anim.has_animation(_ANIM_WALK):
_bull_anim.speed_scale = 1.0 _bull_anim.speed_scale = 2.0
_bull_anim.play(_ANIM_DASH, 0.25) _bull_anim.play(_ANIM_WALK, 1)
var anim = _bull_anim.get_animation(_ANIM_WALK)
anim.loop_mode = Animation.LOOP_LINEAR
else: else:
_play_idle() _play_idle()
@@ -490,6 +506,7 @@ func _tick_roll(delta: float, _steer: Vector3) -> void:
_set_dust_state(DustState.CHARGE) _set_dust_state(DustState.CHARGE)
if _charge_trail_emitter: if _charge_trail_emitter:
_charge_trail_emitter.emitting = true
_charge_trail_emitter.direction = (-_roll_dir + Vector3(0.0, 0.25, 0.0)).normalized() _charge_trail_emitter.direction = (-_roll_dir + Vector3(0.0, 0.25, 0.0)).normalized()
move_and_slide() move_and_slide()
@@ -571,6 +588,8 @@ func _end_roll() -> void:
_legs.set(&"external_rotation", false) _legs.set(&"external_rotation", false)
if _roll_spark_emitter: if _roll_spark_emitter:
_roll_spark_emitter.emitting = false _roll_spark_emitter.emitting = false
if _charge_trail_emitter:
_charge_trail_emitter.emitting = false
velocity.x *= 0.15 velocity.x *= 0.15
velocity.z *= 0.15 velocity.z *= 0.15
_play_idle() _play_idle()
@@ -1022,11 +1041,13 @@ func _physics_process(delta: float) -> void:
velocity.z = flat_n.z * exit_spd velocity.z = flat_n.z * exit_spd
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z) pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
# ── Bounce on landing ───────────────────────────────────────────────────── # ── Landing. Don't bounce ─────────────────────────────────────────────────────
if is_on_floor() and not _was_on_floor: if is_on_floor() and not _was_on_floor:
var impact := -_pre_slide_vel_y velocity.y = 0.0
if impact > DP.f("bounce_min_impact") and not _slam_pending: #Bouncing landing below
velocity.y = impact * DP.f("bounce_restitution") #var impact := -_pre_slide_vel_y
#if impact > DP.f("bounce_min_impact") and not _slam_pending:
#velocity.y = impact * DP.f("bounce_restitution")
# ── Slam landing ────────────────────────────────────────────────────────── # ── Slam landing ──────────────────────────────────────────────────────────
if _slam_pending and is_on_floor() and not _was_on_floor: if _slam_pending and is_on_floor() and not _was_on_floor:
@@ -1073,6 +1094,7 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void
if _dead or _hit_iframes > 0.0: if _dead or _hit_iframes > 0.0:
return return
_hp -= 1 _hp -= 1
_hit_flash()
_hit_iframes = DP.f("bull_hit_iframes") _hit_iframes = DP.f("bull_hit_iframes")
health_changed.emit(_hp, _max_hp) health_changed.emit(_hp, _max_hp)
hit_taken.emit(cause) hit_taken.emit(cause)
@@ -1085,6 +1107,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.pitch_scale = randf_range(0.7, 0.9)
_huff_player.play() _huff_player.play()
if _hp <= 0: if _hp <= 0:
if _bull_anim.current_animation != _ANIM_DEATH:
_bull_anim.speed_scale = 0.8
_bull_anim.play(_ANIM_DEATH)
_dead = true _dead = true
died.emit(cause) died.emit(cause)
@@ -1130,3 +1155,36 @@ func _spawn_blood(hit_pos: Vector3) -> void:
p.global_position = hit_pos p.global_position = hit_pos
p.restart() p.restart()
_free_after(p, 1.5) _free_after(p, 1.5)
#hit flashing
func _hit_flash() -> void:
var meshes := find_children("*", "MeshInstance3D", true, false)
for node in meshes:
var mesh := node as MeshInstance3D
if mesh:
mesh.material_overlay = _make_hit_overlay()
if _hit_flash_tween:
_hit_flash_tween.kill()
_hit_flash_tween = create_tween()
_hit_flash_tween.tween_interval(0.08)
_hit_flash_tween.tween_callback(_clear_hit_flash)
func _make_hit_overlay() -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.albedo_color = Color(1.0, 0.03, 0.03, 1.0)
return mat
func _clear_hit_flash() -> void:
var meshes := find_children("*", "MeshInstance3D", true, false)
for node in meshes:
var mesh := node as MeshInstance3D
if mesh:
mesh.material_overlay = null
+22 -13
View File
@@ -13,13 +13,11 @@ config_version=5
config/name="Bullosseum" config/name="Bullosseum"
config/version="0.2.0" config/version="0.2.0"
run/main_scene="res://MainMenu.tscn" run/main_scene="res://MainMenu.tscn"
config/features=PackedStringArray("4.7", "Forward Plus") config/features=PackedStringArray("4.7", "GL Compatibility")
config/icon="res://icon.svg"
config/run/main_scene="res://MainMenu.tscn"
boot_splash/bg_color=Color(0, 0, 0, 1) boot_splash/bg_color=Color(0, 0, 0, 1)
boot_splash/image="res://Assets/loading_screen.png" boot_splash/image="res://Assets/loading_screen.png"
boot_splash/fullsize=true config/icon="uid://dct1d88g015el"
boot_splash/use_filter=true config/run/main_scene="res://MainMenu.tscn"
[autoload] [autoload]
@@ -28,6 +26,13 @@ Console="*res://console.gd"
Controls="*res://controls_manager.gd" Controls="*res://controls_manager.gd"
DebugDraw="*res://debug_draw.gd" DebugDraw="*res://debug_draw.gd"
Settings="*res://settings.gd" Settings="*res://settings.gd"
Run="*res://levels/run_state.gd"
OrientationGuard="*res://orientation_guard.gd"
[display]
window/size/mode=3
window/handheld/orientation=4
[editor_plugins] [editor_plugins]
@@ -55,7 +60,7 @@ move_left={
"deadzone": 0.2, "deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":-1.0,"script":null) , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":13,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
@@ -63,18 +68,20 @@ move_right={
"deadzone": 0.2, "deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":1.0,"script":null) , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
zoom_in={ zoom_in={
"deadzone": 0.2, "deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(726, 8),"global_position":Vector2(735, 56),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null) "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(726, 8),"global_position":Vector2(735, 56),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
zoom_out={ zoom_out={
"deadzone": 0.2, "deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(400, 21),"global_position":Vector2(409, 69),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null) "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(400, 21),"global_position":Vector2(409, 69),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":4,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
toggle_mouse_capture={ toggle_mouse_capture={
@@ -90,14 +97,14 @@ ability_kick={
} }
ability_slam={ ability_slam={
"deadzone": 0.2, "deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":104,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
ability_dash={ ability_dash={
"deadzone": 0.2, "deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":104,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
ability_roll={ ability_roll={
@@ -117,5 +124,7 @@ pointing/emulate_touch_from_mouse=true
[rendering] [rendering]
rendering_device/driver.windows="d3d12" rendering_device/driver.windows="opengl3"
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
+128
View File
@@ -0,0 +1,128 @@
extends RefCounted
## Converts Skeleton3D-skinned meshes into NON-skinned mesh pieces parented to
## BoneAttachment3D nodes, so rendering never uses GPU vertex-skinning (transform
## feedback). Required on ANGLE/Vulkan/Mali mobile GPUs (e.g. Mali-G720 in mobile
## Chrome/Brave), where Godot's Compatibility skinning path renders skinned meshes
## invisible while non-skinned meshes draw fine.
##
## Each triangle is assigned wholesale to its dominant bone (highest summed weight),
## so segments stay watertight; joints become rigid (no smooth bending), which suits
## the low-poly look. Call RigidSkin.convert_tree(character_root) once after the scene
## is instanced (bones don't need to be posed yet — pieces are baked in bind space).
static func convert_tree(root: Node) -> int:
var converted := 0
for skel: Skeleton3D in _find(root, "Skeleton3D", []):
for mi: Node in skel.get_children():
if mi is MeshInstance3D and (mi as MeshInstance3D).skin != null \
and (mi as MeshInstance3D).mesh is ArrayMesh:
if _convert(skel, mi as MeshInstance3D):
converted += 1
return converted
static func _find(n: Node, klass: String, acc: Array) -> Array:
if n.is_class(klass):
acc.append(n)
for c: Node in n.get_children():
_find(c, klass, acc)
return acc
static func _convert(skel: Skeleton3D, mi: MeshInstance3D) -> bool:
var mesh := mi.mesh as ArrayMesh
var skin := mi.skin
# bind index -> (bone index, bind pose = mesh-space -> bone-local)
var bind_bone: PackedInt32Array = []
var bind_pose: Array[Transform3D] = []
for b: int in skin.get_bind_count():
var bone := skin.get_bind_bone(b)
if bone < 0:
bone = skel.find_bone(skin.get_bind_name(b))
bind_bone.append(bone)
bind_pose.append(skin.get_bind_pose(b))
# Accumulate SurfaceTool geometry per destination bone.
var st_by_bone: Dictionary = {} # bone index -> SurfaceTool
var mat_by_bone: Dictionary = {} # bone index -> Material (first seen)
for s: int in mesh.get_surface_count():
var arr := mesh.surface_get_arrays(s)
var verts: PackedVector3Array = arr[Mesh.ARRAY_VERTEX]
var norms: PackedVector3Array = arr[Mesh.ARRAY_NORMAL]
var uvs: PackedVector2Array = arr[Mesh.ARRAY_TEX_UV] if arr[Mesh.ARRAY_TEX_UV] != null else PackedVector2Array()
var cols: PackedColorArray = arr[Mesh.ARRAY_COLOR] if arr[Mesh.ARRAY_COLOR] != null else PackedColorArray()
var bones: PackedInt32Array = arr[Mesh.ARRAY_BONES]
var weights: PackedFloat32Array = arr[Mesh.ARRAY_WEIGHTS]
var idx: PackedInt32Array = arr[Mesh.ARRAY_INDEX]
var mat := mesh.surface_get_material(s)
var infl := 8 if (mesh.surface_get_format(s) & Mesh.ARRAY_FLAG_USE_8_BONE_WEIGHTS) else 4
var tri := PackedInt32Array()
if idx != null and idx.size() > 0:
tri = idx
else:
tri.resize(verts.size())
for i: int in verts.size():
tri[i] = i
for t: int in range(0, tri.size(), 3):
var a := tri[t]
var b := tri[t + 1]
var c := tri[t + 2]
var bind := _dominant_bind([a, b, c], bones, weights, infl)
var bone := bind_bone[bind]
var pose := bind_pose[bind]
var st: SurfaceTool = st_by_bone.get(bone)
if st == null:
st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
st_by_bone[bone] = st
mat_by_bone[bone] = mat
for v: int in [a, b, c]:
if cols.size() > v:
st.set_color(cols[v])
if uvs.size() > v:
st.set_uv(uvs[v])
if norms.size() > v:
st.set_normal((pose.basis * norms[v]).normalized())
st.add_vertex(pose * verts[v])
if st_by_bone.is_empty():
return false
for bone: int in st_by_bone:
var att := BoneAttachment3D.new()
att.bone_name = skel.get_bone_name(bone)
skel.add_child(att)
var piece := MeshInstance3D.new()
var st: SurfaceTool = st_by_bone[bone]
var m := st.commit()
if mat_by_bone[bone] != null:
m.surface_set_material(0, mat_by_bone[bone])
piece.mesh = m
att.add_child(piece)
# Hide (don't free) the original: keeps the node for any gameplay code that references
# it, and a hidden skinned mesh isn't drawn so it never hits the transform-feedback path.
mi.visible = false
return true
static func _dominant_bind(vs: Array, bones: PackedInt32Array, weights: PackedFloat32Array, infl: int) -> int:
var acc: Dictionary = {}
for v: int in vs:
for k: int in infl:
var w := weights[v * infl + k]
if w <= 0.0:
continue
var bind := bones[v * infl + k]
acc[bind] = float(acc.get(bind, 0.0)) + w
var best := 0
var best_w := -1.0
for bind: int in acc:
if acc[bind] > best_w:
best_w = acc[bind]
best = bind
return best
+1
View File
@@ -0,0 +1 @@
uid://dymd8vcfmtjts
+5 -1
View File
@@ -8,7 +8,7 @@ SKIP_GAMEPLAY=0
for arg in "$@"; do [[ "$arg" == "--no-gameplay" ]] && SKIP_GAMEPLAY=1; done for arg in "$@"; do [[ "$arg" == "--no-gameplay" ]] && SKIP_GAMEPLAY=1; done
echo "=== gdlint ===" echo "=== gdlint ==="
gdlint *.gd tests/*.gd gdlint *.gd levels/*.gd tests/*.gd
echo "" echo ""
echo "=== Logic tests ===" echo "=== Logic tests ==="
@@ -18,6 +18,10 @@ echo ""
echo "=== Console focus tests ===" echo "=== Console focus tests ==="
"$GODOT" --headless --script tests/console_focus_test.gd "$GODOT" --headless --script tests/console_focus_test.gd
echo ""
echo "=== Level switch tests (no cross-level geometry/collider leaks) ==="
"$GODOT" --headless --script tests/level_switch_test.gd
echo "" echo ""
echo "=== Performance tests (frame-budget regression guard) ===" echo "=== Performance tests (frame-budget regression guard) ==="
"$GODOT" --headless --script tests/performance_test.gd "$GODOT" --headless --script tests/performance_test.gd

Some files were not shown because too many files have changed in this diff Show More