55 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
richard 56593c58cf Release 0.2.0: device-aware controls, spear, settings, shaders
Show touch controls only on touch platforms and keyboard hints only on
desktop, via a shared Controls.use_touch_ui() gate (is_touchscreen_available
is unreliable with emulate_touch_from_mouse). Bumps version to 0.2.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-09 19:39:37 +03:00
richard ce6f3d7075 Deployment script update 2026-08-09 15:26:01 +03:00
siim a74182255b light tim 2026-08-08 11:47:36 +03:00
siim ebf5046461 credits 2026-07-30 20:21:10 +03:00
135 changed files with 18590 additions and 1400 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"
uid="uid://dk6xuwwa0mfpe"
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
path.etc2="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@@ -15,7 +16,7 @@ generator_parameters={
[deps]
source_file="res://Assets/Arena_sand.png"
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"]
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex", "res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"]
[params]
+3 -2
View File
@@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://b4gkpa7jypcsm"
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
path.etc2="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@@ -15,7 +16,7 @@ generator_parameters={
[deps]
source_file="res://Assets/Arena_wall.png"
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"]
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex", "res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"]
[params]
+3 -2
View File
@@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://yercpq3y2o0x"
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
path.etc2="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@@ -15,7 +16,7 @@ generator_parameters={
[deps]
source_file="res://Assets/Arena_wood.png"
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"]
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex", "res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"]
[params]
BIN
View File
Binary file not shown.
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_version=1
type="PackedScene"
uid="uid://dxe72ig8gfbcg"
path="res://.godot/imported/arena model2026.glb-299db01cae891e5005f8ac1e6c83d0a6.scn"
uid="uid://cwmo5vh738x6w"
path="res://.godot/imported/Lobby_level.glb-a7e896ecf7377d9530a80d5e63140cc3.scn"
[deps]
source_file="res://Assets/arena model2026.glb"
dest_files=["res://.godot/imported/arena model2026.glb-299db01cae891e5005f8ac1e6c83d0a6.scn"]
source_file="res://Assets/Lobby_level.glb"
dest_files=["res://.godot/imported/Lobby_level.glb-a7e896ecf7377d9530a80d5e63140cc3.scn"]
[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/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
meshes/force_disable_compression=true
skins/use_named_skins=true
animation/import=true
animation/fps=30
Binary file not shown.
+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/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
meshes/force_disable_compression=true
skins/use_named_skins=true
animation/import=true
animation/fps=30
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ml0ntjv5injp"
path="res://.godot/imported/bull_hud_bg.png-4645abe7114935a3d4a8e0f88ccce15c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/bull_hud_bg.png"
dest_files=["res://.godot/imported/bull_hud_bg.png-4645abe7114935a3d4a8e0f88ccce15c.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.
+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.

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.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://jepc74w8opbu"
path="res://.godot/imported/spear.glb-0139879d219c93f9dd35db2304db7d5d.scn"
[deps]
source_file="res://Assets/spear.glb"
dest_files=["res://.godot/imported/spear.glb-0139879d219c93f9dd35db2304db7d5d.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
+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.
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 |
|---|---|
| `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.gd` | Movement, charge, turning logic |
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
| `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`) |
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
| `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
# 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/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/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):
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://bxm7c6ldrdiq5"
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"
path.astc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.astc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"]
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex", "res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.astc.ctex"]
[params]
+3 -2
View File
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://3syhacswepyj"
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"
path.astc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"]
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex", "res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"]
[params]
+3 -3
View File
@@ -51,7 +51,7 @@ dof_blur_near_transition = 5.0
transform = Transform3D(-0.56715536, 0, -0.8236109, 0, 1, 0, 0.8236109, 0, -0.56715536, 5.2114463, -0.08998108, 1.8927763)
script = ExtResource("2_5jr0i")
[node name="Skeleton3D" parent="Matador/Armature" parent_id_path=PackedInt32Array(166852612, 1269629432) index="0" unique_id=978366810]
[node name="Skeleton3D" parent="Matador/Armature" parent_id_path=PackedInt32Array(166852612, 1447304122) index="0" unique_id=357848305]
bones/0/position = Vector3(0.056660548, 0.6424767, -0.0167365)
bones/0/rotation = Quaternion(0.013179442, -0.2660563, 0.073319815, 0.9610747)
bones/1/rotation = Quaternion(0.06234612, 0.049236443, 0.08183311, 0.99347484)
@@ -81,10 +81,10 @@ bones/23/rotation = Quaternion(0.9993525, -0.0021175682, 0.03502641, -0.00795782
bones/24/rotation = Quaternion(0.08874816, -0.018719967, -0.04671622, 0.99478185)
bones/25/rotation = Quaternion(0.09634846, -0.0268489, -0.04050248, 0.9941608)
[node name="matador" parent="Matador/Armature/Skeleton3D" index="0" unique_id=256196413]
[node name="matador" parent="Matador/Armature/Skeleton3D" index="0" unique_id=593562242]
transform = Transform3D(0.8642424, 0, 0.50307554, 0, 1, 0, -0.50307554, 0, 0.8642424, 0, 0, 0)
[node name="Muleta" parent="Matador/Armature/Skeleton3D" index="1" unique_id=988244930]
[node name="Muleta" parent="Matador/Armature/Skeleton3D" index="1" unique_id=978680467]
cast_shadow = 0
[node name="Camera3D" type="Camera3D" parent="." unique_id=1531255459]
+82 -39
View File
@@ -1,7 +1,13 @@
[gd_scene load_steps=5 format=3 uid="uid://bmnmenu0intr1"]
[gd_scene format=3 uid="uid://bmnmenu0intr1"]
[ext_resource type="Script" path="res://main_menu.gd" id="1_menu"]
[ext_resource type="VideoStream" path="res://Assets/intro_bg.ogv" id="2_video"]
[ext_resource type="Script" uid="uid://46y10alai56q" path="res://main_menu.gd" id="1_menu"]
[ext_resource type="VideoStream" uid="uid://csxxlnq74fej4" path="res://Assets/intro_bg.ogv" id="2_video"]
[ext_resource type="Shader" uid="uid://bmdtg6qlh7wri" path="res://menu_blur.gdshader" id="3_blur"]
[sub_resource type="ShaderMaterial" id="mat_blur"]
shader = ExtResource("3_blur")
shader_parameter/blur_radius = 3.0
shader_parameter/darken = 0.35
[sub_resource type="StyleBoxFlat" id="sb_normal"]
content_margin_left = 16.0
@@ -30,7 +36,7 @@ corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[node name="MainMenu" type="Control"]
[node name="MainMenu" type="Control" unique_id=371457725]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@@ -39,29 +45,54 @@ grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_menu")
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="."]
[node name="VideoBlur" type="AspectRatioContainer" parent="." unique_id=374854263]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
ratio = 1.7778
stretch_mode = 3
[node name="BlurVideo" type="VideoStreamPlayer" parent="VideoBlur" unique_id=374854262]
material = SubResource("mat_blur")
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
stream = ExtResource("2_video")
expand = true
[node name="MainPanel" type="Control" parent="."]
[node name="VideoAspect" type="AspectRatioContainer" parent="." unique_id=374854264]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
ratio = 1.7778
[node name="PlayButton" type="Button" parent="MainPanel"]
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="VideoAspect" unique_id=374854265]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
stream = ExtResource("2_video")
expand = true
[node name="CapeLayer" type="Control" parent="VideoAspect" unique_id=374854266]
layout_mode = 2
mouse_filter = 2
[node name="PlayButton" type="Button" parent="VideoAspect/CapeLayer" unique_id=743089847]
layout_mode = 1
anchor_left = 0.4530
anchors_preset = -1
anchor_left = 0.453
anchor_top = 0.6513
anchor_right = 0.4530
anchor_right = 0.453
anchor_bottom = 0.6513
offset_left = -120.0
offset_top = -55.0
@@ -74,13 +105,23 @@ theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 8
theme_override_font_sizes/font_size = 56
theme_override_styles/normal = SubResource("sb_normal")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/pressed = SubResource("sb_hover")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/focus = SubResource("sb_hover")
text = "PLAY"
[node name="OptionsButton" type="Button" parent="MainPanel"]
[node name="MainPanel" type="Control" parent="." unique_id=1263320918]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="OptionsButton" type="Button" parent="MainPanel" unique_id=673211179]
layout_mode = 1
anchors_preset = -1
anchor_left = 0.11
anchor_top = 0.94
anchor_right = 0.11
@@ -96,13 +137,14 @@ theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 6
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("sb_normal")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/pressed = SubResource("sb_hover")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/focus = SubResource("sb_hover")
text = "OPTIONS"
[node name="CreditsButton" type="Button" parent="MainPanel"]
[node name="CreditsButton" type="Button" parent="MainPanel" unique_id=882339461]
layout_mode = 1
anchors_preset = -1
anchor_left = 0.489
anchor_top = 0.951
anchor_right = 0.489
@@ -118,13 +160,14 @@ theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 6
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("sb_normal")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/pressed = SubResource("sb_hover")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/focus = SubResource("sb_hover")
text = "CREDITS"
[node name="ExitButton" type="Button" parent="MainPanel"]
[node name="ExitButton" type="Button" parent="MainPanel" unique_id=1064586744]
layout_mode = 1
anchors_preset = -1
anchor_left = 0.92
anchor_top = 0.951
anchor_right = 0.92
@@ -140,12 +183,12 @@ theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 6
theme_override_font_sizes/font_size = 30
theme_override_styles/normal = SubResource("sb_normal")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/pressed = SubResource("sb_hover")
theme_override_styles/hover = SubResource("sb_hover")
theme_override_styles/focus = SubResource("sb_hover")
text = "EXIT"
[node name="OptionsPanel" type="CenterContainer" parent="."]
[node name="OptionsPanel" type="CenterContainer" parent="." unique_id=593823446]
visible = false
layout_mode = 1
anchors_preset = 15
@@ -154,17 +197,17 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Backdrop" type="ColorRect" parent="OptionsPanel"]
[node name="Backdrop" type="ColorRect" parent="OptionsPanel" unique_id=1811982549]
layout_mode = 2
color = Color(0, 0, 0, 0.55)
mouse_filter = 2
color = Color(0, 0, 0, 0.55)
[node name="VBox" type="VBoxContainer" parent="OptionsPanel"]
[node name="VBox" type="VBoxContainer" parent="OptionsPanel" unique_id=2125052135]
custom_minimum_size = Vector2(560, 480)
layout_mode = 2
theme_override_constants/separation = 16
[node name="TitleLabel" type="Label" parent="OptionsPanel/VBox"]
[node name="TitleLabel" type="Label" parent="OptionsPanel/VBox" unique_id=853979265]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 8
@@ -172,7 +215,7 @@ theme_override_font_sizes/font_size = 44
text = "Controls"
horizontal_alignment = 1
[node name="Hint" type="Label" parent="OptionsPanel/VBox"]
[node name="Hint" type="Label" parent="OptionsPanel/VBox" unique_id=1809504670]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 5
@@ -180,24 +223,24 @@ theme_override_font_sizes/font_size = 20
text = "Click a key, then press a new key. Esc cancels."
horizontal_alignment = 1
[node name="Scroll" type="ScrollContainer" parent="OptionsPanel/VBox"]
[node name="Scroll" type="ScrollContainer" parent="OptionsPanel/VBox" unique_id=683738956]
custom_minimum_size = Vector2(0, 340)
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="RebindList" type="VBoxContainer" parent="OptionsPanel/VBox/Scroll"]
[node name="RebindList" type="VBoxContainer" parent="OptionsPanel/VBox/Scroll" unique_id=1261520279]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 8
[node name="BackButton" type="Button" parent="OptionsPanel/VBox"]
[node name="BackButton" type="Button" parent="OptionsPanel/VBox" unique_id=1587524951]
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Back"
[node name="CreditsPanel" type="CenterContainer" parent="."]
[node name="CreditsPanel" type="CenterContainer" parent="." unique_id=175743902]
visible = false
layout_mode = 1
anchors_preset = 15
@@ -206,17 +249,17 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Backdrop" type="ColorRect" parent="CreditsPanel"]
[node name="Backdrop" type="ColorRect" parent="CreditsPanel" unique_id=1950253735]
layout_mode = 2
color = Color(0, 0, 0, 0.55)
mouse_filter = 2
color = Color(0, 0, 0, 0.55)
[node name="VBox" type="VBoxContainer" parent="CreditsPanel"]
[node name="VBox" type="VBoxContainer" parent="CreditsPanel" unique_id=901836808]
custom_minimum_size = Vector2(520, 360)
layout_mode = 2
theme_override_constants/separation = 20
[node name="TitleLabel" type="Label" parent="CreditsPanel/VBox"]
[node name="TitleLabel" type="Label" parent="CreditsPanel/VBox" unique_id=825593160]
layout_mode = 2
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 8
@@ -224,36 +267,36 @@ theme_override_font_sizes/font_size = 44
text = "Credits"
horizontal_alignment = 1
[node name="Names" type="RichTextLabel" parent="CreditsPanel/VBox"]
[node name="Names" type="RichTextLabel" parent="CreditsPanel/VBox" unique_id=1355198629]
layout_mode = 2
size_flags_vertical = 3
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_colors/default_color = Color(0.93, 0.88, 0.72, 1)
theme_override_constants/outline_size = 5
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/line_separation = 6
theme_override_constants/outline_size = 5
theme_override_font_sizes/normal_font_size = 26
theme_override_font_sizes/bold_font_size = 30
bbcode_enabled = true
fit_content = true
scroll_active = false
text = "[center][font_size=32][color=#d9a833]Bullosseum[/color][/font_size]
[b][color=#d9a833]Siim Raud[/color][/b] [url=https://instagram.com/siimpressionist]@siimpressionist[/url]
[font_size=20]Creative, art, idea guy[/font_size]
[font_size=20]Artist / Animator[/font_size]
[b][color=#d9a833]Richard Aasa[/color][/b] [url=https://instagram.com/aasarichard]@aasarichard[/url]
[font_size=20]Programmer[/font_size]
[b][color=#d9a833]Kadi Rebane[/color][/b] [url=https://instagram.com/kadirebane]@kadirebane[/url]
[b][color=#d9a833]Kadi Rebane[/color][/b] [url=https://x.com/_barkseven]@_barkseven[/url]
[font_size=20]Arena asset[/font_size][/center]"
fit_content = true
scroll_active = false
[node name="BackButton" type="Button" parent="CreditsPanel/VBox"]
[node name="BackButton" type="Button" parent="CreditsPanel/VBox" unique_id=1194791074]
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Back"
[node name="FadeRect" type="ColorRect" parent="."]
[node name="FadeRect" type="ColorRect" parent="." unique_id=15381366]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
+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="PackedScene" uid="uid://bun14xmk20pcj" path="res://Assets/bull.fbx" id="4_e80uo"]
[sub_resource type="SphereShape3D" id="SphereShape3D_chest"]
radius = 0.35
[sub_resource type="SphereShape3D" id="SphereShape3D_belly"]
radius = 0.4
[sub_resource type="SphereShape3D" id="SphereShape3D_hips"]
radius = 0.3
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_kne1u"]
radius = 0.7495117
height = 1.845398
[node name="Player" type="CharacterBody3D" unique_id=165510044]
collision_layer = 3
script = ExtResource("1_xhfnw")
[node name="CollisionChest" type="CollisionShape3D" parent="." unique_id=447032296]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, -0.2)
shape = SubResource("SphereShape3D_chest")
[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="CollisionBull" type="CollisionShape3D" parent="." unique_id=571780801]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.29183745, 0.3197594)
shape = SubResource("CapsuleShape3D_kne1u")
[node name="Camera3D" type="Camera3D" parent="." unique_id=1120161087]
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")]
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"]
+1 -1
View File
@@ -8,5 +8,5 @@ script="rider-plugin.gd"
[presets]
active="off"
active="on"
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
+11
View File
@@ -42,6 +42,15 @@ func _unhandled_input(event: InputEvent) -> void:
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
# Pinch-to-zoom hook for the touch UI. `ratio` is the two-finger spread this frame over
# last frame: >1 (fingers parting) narrows the FOV to zoom in, <1 widens it to pull back,
# mirroring the scroll-wheel zoom_in/zoom_out feel.
func adjust_zoom(ratio: float) -> void:
if ratio <= 0.0:
return
_user_fov = clampf(_user_fov / ratio, fov_min, fov_max)
func _process(delta: float) -> void:
fov = lerpf(fov, _user_fov, delta * 8.0)
@@ -92,6 +101,8 @@ func _drag_focus(target: Node3D, delta: float) -> Vector3:
func trigger_hit(strength: float) -> void:
if not Settings.screen_shake:
return
_shake_amount = strength * 0.5
_shake_decay = _shake_amount / 0.4
-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",
"toggle": "flip a bool param",
"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",
"help": "show this help",
}
@@ -472,6 +474,12 @@ func _run_command(parts: PackedStringArray) -> bool:
_cmd_diff()
"list":
_cmd_list(parts)
"level":
_cmd_level(parts)
"restart":
_log_ok("Restarting…")
get_tree().paused = false
get_tree().reload_current_scene()
"clear":
_log.clear()
"help":
@@ -581,9 +589,9 @@ func _set_param(key: String, meta: Dictionary, raw: String) -> void:
var lo := meta["min"] as float
var hi := meta["max"] as 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
var v := snappedf(clampf(raw.to_float(), lo, hi), meta["step"] as float)
var v := raw.to_float()
DP.set_value(key, v)
_log_ok("[b]%s[/b] ← [color=#ffe080]%s[/color]" % [key, _fmt_num(v)])
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"])
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:
var filter := parts[1].to_lower() if parts.size() > 1 else ""
var all := DP.get_all()
+93 -1
View File
@@ -15,8 +15,8 @@ const REBINDABLE_ACTIONS: PackedStringArray = [
&"move_left",
&"move_right",
&"ability_kick",
&"ability_slam",
&"ability_dash",
&"ability_slam",
&"ability_roll",
]
@@ -25,6 +25,98 @@ func _ready() -> void:
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)
## instead of keyboard hints. Platform detection is cached (it can't change mid-run)
## so the per-frame/per-input callers don't repay the cost — but the DP force flag is
## re-read every call so the console can toggle the layout live on a desktop build.
var _touch_ui_cached: int = -1 # -1 = not yet detected, 0 = false, 1 = true
func use_touch_ui() -> bool:
if DP.b("force_touch_controls"):
return true
if _touch_ui_cached == -1:
_touch_ui_cached = 1 if _detect_touch_ui() else 0
return _touch_ui_cached == 1
## True only on touch-primary devices (phones, tablets). Detection is deliberately
## conservative so a desktop never gets the on-screen thumb UI.
##
## DisplayServer.is_touchscreen_available() is unusable here on two fronts: on native
## desktop `emulate_touch_from_mouse` (on for testing) forces it true, and on the web build
## it maps to `'ontouchstart' in window`, which desktop Chromium reports true even with no
## touchscreen. So on web we ask the browser two questions instead: does it report touch
## points, AND is the *primary* pointer coarse? A desktop-with-mouse (even a touch laptop)
## answers no to the second and keeps the desktop HUD; a phone/tablet answers yes to both.
## maxTouchPoints also catches iPadOS, which masquerades as desktop Safari.
func _detect_touch_ui() -> bool:
if (
OS.has_feature("mobile")
or OS.has_feature("web_android")
or OS.has_feature("web_ios")
):
return true
if not OS.has_feature("web"):
return false
var js := "(navigator.maxTouchPoints > 0) && window.matchMedia('(pointer: coarse)').matches"
var touch_primary: Variant = JavaScriptBridge.eval(js, true)
if touch_primary is bool:
return touch_primary
if touch_primary is int or touch_primary is float:
return int(touch_primary) != 0
return false
## Whether on-screen debug overlays (touch state + render diagnostics) should show. True on
## the DP "touch_debug" flag, or — so it can be switched on from a phone where no console is
## reachable — when the page URL carries a `debug` query param (append `?debug=1`).
var _url_debug: int = -1
func debug_overlay() -> bool:
if DP.b("touch_debug"):
return true
if _url_debug == -1:
_url_debug = 0
if OS.has_feature("web"):
var has: Variant = JavaScriptBridge.eval(
"String(window.location.search).indexOf('debug') >= 0", true)
if (has is bool and has) or ((has is int or has is float) and int(has) != 0):
_url_debug = 1
return _url_debug == 1
## Returns the first keyboard event bound to an action, or null.
func get_key_event(action: StringName) -> InputEventKey:
for event: InputEvent in InputMap.action_get_events(action):
+37 -55
View File
@@ -1,19 +1,16 @@
extends MultiMeshInstance3D
## A whole stand of cheering spectators rendered as camera-facing billboards in a
## single draw call — the performant replacement for dozens of skinned crowd skeletons.
## The sprite sheet is baked from the existing crowd figure by tools/bake_crowd_sheet.gd.
## A whole stand of cheering spectators rendered as flat billboards in a single draw
## call — the performant replacement for dozens of skinned crowd skeletons. The sprite
## 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
## billboards land exactly where the crowd was authored; if that node is missing it
## falls back to a generated tribune so the scene still populates.
## Placement comes from CrowdMarker nodes the designer drops into the level (collected
## via the &"crowd_marker" group): each marker gives one spectator its world position and
## 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 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.
@export var figure_height: float = 2.0
@export var frames: int = 8
@@ -36,7 +33,8 @@ var _rng := RandomNumberGenerator.new()
func _ready() -> void:
_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:
@@ -58,71 +56,53 @@ func _build() -> void:
mat.set_shader_parameter("fps", clap_fps)
material_override = mat
var seats := _seat_positions()
var center := _arena_center(seats)
# Each seat is a (global feet-position, unit forward) pair.
var positions := PackedVector3Array()
var facings: Array[Vector3] = []
_collect_seats(positions, facings)
var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.use_custom_data = true
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
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)
mm.set_instance_custom_data(i, Color(_rng.randf(), b, b * 0.99, b * 0.98))
multimesh = mm
# Orientation for a seat: the sprite's forward (+Z, the quad's face) points horizontally
# inward at the arena centre, so each ring segment faces the action instead of the
# camera. Upright (world +Y), uniformly scaled by `s`.
func _facing_basis(seat: Vector3, center: Vector3, s: float) -> Basis:
var look := center - seat
# Upright basis whose forward (+Z, the quad's face) points along `look`, uniformly
# scaled by `s`. World +Y stays up so spectators never tilt.
func _upright_basis(look: Vector3, s: float) -> Basis:
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 right := up.cross(look).normalized()
var basis := Basis()
basis.x = right
basis.x = up.cross(look).normalized()
basis.y = up
basis.z = look
return basis.scaled(Vector3.ONE * s)
# Horizontal centre the crowd faces — the mean of the seat positions (the ring/stand
# centroid), which for an arena crowd is the middle of the pitch.
func _arena_center(seats: PackedVector3Array) -> Vector3:
if seats.is_empty():
return global_position
var sum := Vector3.ZERO
for p in seats:
sum += p
return sum / float(seats.size())
# Seats from designer-placed CrowdMarker nodes: each contributes its world position and
# its chosen 60°-spaced facing. With no markers present, fall back to a generated ring
# that faces inward so the scene still shows a crowd.
func _collect_seats(positions: PackedVector3Array, facings: Array[Vector3]) -> void:
for node in get_tree().get_nodes_in_group(&"crowd_marker"):
var marker := node as Node3D
if marker == null or not marker.has_method(&"facing_dir"):
continue
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,
# 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:
func _fallback_stand(positions: PackedVector3Array, facings: Array[Vector3]) -> void:
var placed := 0
for row in fallback_rows:
var radius := fallback_inner_radius + float(row) * fallback_row_step
@@ -133,5 +113,7 @@ func _fallback_stand(out: PackedVector3Array) -> void:
if placed >= fallback_count:
return
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
+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
+16
View File
@@ -0,0 +1,16 @@
shader_type canvas_item;
// Faint red gradient hugging the screen edges, flashed on a hit. `flash` is tweened
// 0.9 → 0 by the HUD; the vignette is transparent in the centre and reddens toward
// the corners so the middle of the screen stays clear.
uniform float flash : hint_range(0.0, 1.0) = 0.0;
uniform vec3 tint : source_color = vec3(0.75, 0.0, 0.0);
uniform float inner : hint_range(0.0, 1.0) = 0.45;
uniform float outer : hint_range(0.0, 1.0) = 1.0;
void fragment() {
// Distance from centre normalised so the corners land at ~1.0.
float dist = length(UV - vec2(0.5)) / 0.70710678;
float v = smoothstep(inner, outer, dist);
COLOR = vec4(tint, v * flash);
}
+1
View File
@@ -0,0 +1 @@
uid://dys23pu1kw5fl
+5 -1
View File
@@ -10,6 +10,7 @@ extends Node
##
## Toggle from the console:
## 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_animation_name — floating current-clip tag over every AnimationPlayer host
## 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
# the skeleton; otherwise every corpse floods the view with capsules.
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]
for child in node.get_children():
_collect(child, col, hit, bone, anim, wanted)
+205 -41
View File
@@ -17,14 +17,13 @@ var _values: Dictionary = {}
func _ready() -> void:
_register_all()
load_saved()
func _register_all() -> void:
# ── Movement ──────────────────────────────────────────────────────────────
# WASD/arrows drive the bull: move_accel ramps flat velocity toward max_speed in
# the input direction; move_friction bleeds it off when no direction is held.
_reg_f("Movement", "max_speed", 50.0, 2.0, 1000.0)
_reg_f("Movement", "max_speed", 10.0, 2.0, 1000.0)
_reg_f("Movement", "move_accel", 120.0, 5.0, 600.0)
_reg_f("Movement", "move_friction", 90.0, 5.0, 600.0)
_reg_f("Movement", "jump_velocity", 4.5, 1.0, 200.0)
@@ -33,6 +32,11 @@ func _register_all() -> void:
_reg_f("Movement", "bounce_min_impact", 1.0, 0.0, 20.0)
_reg_f("Movement", "wall_bounce_restitution", 0.6, 0.0, 1.5, 0.05)
_reg_f("Movement", "wall_min_bounce_speed", 6.0, 0.0, 30.0)
# ── Bull health ───────────────────────────────────────────────────────────
# The bull soaks bull_max_hp clean hits before it dies; each hit grants
# bull_hit_iframes seconds of invulnerability so a single pass can't drain it.
_reg_f("Bull", "bull_max_hp", 10.0, 1.0, 100.0, 1.0)
_reg_f("Bull", "bull_hit_iframes", 0.8, 0.0, 3.0, 0.1)
# ── Dust speed thresholds ─────────────────────────────────────────────────
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
_reg_f("Dust", "dust_charge_spd", 7.0, 1.0, 20.0)
@@ -41,12 +45,12 @@ func _register_all() -> void:
_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_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_max", 0.65, 0.05, 3.0)
_reg_f("Dust", "walk_scale_min", 0.20, 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_max", 1.8, 0.1, 12.0)
_reg_f("Dust", "charge_scale_min", 1.0, 0.05, 6.0)
_reg_f("Dust", "charge_scale_max", 2.2, 0.05, 6.0)
_reg_f("Dust", "charge_scale_min", 0.35, 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_max", 5.5, 0.1, 20.0)
# ── Tail ──────────────────────────────────────────────────────────────────
@@ -94,14 +98,19 @@ func _register_all() -> void:
_reg_f("Camera", "cam_drag_speed", 3.5, 0.5, 20.0)
_reg_f("Camera", "cam_follow_lerp", 10.0, 1.0, 40.0)
# ── 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
# is the number of placed spawn points). mat_spawn_stagger is the delay between each
# matador leaping in within a wave; mat_wave_pause is the breather after a wave clears.
_reg_f("Matador", "mat_wave_size", 8.0, 1, 50)
_reg_f("Matador", "mat_spawn_stagger", 0.4, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_wave_pause", 2.0, 0.0, 8.0, 0.25)
_reg_f("Matador", "mat_walk_speed", 5.5, 0.5, 10.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
# they stay off the benches). Tune in the console to hug the bench edge.
_reg_f("Matador", "arena_spawn_radius", 10.0, 2.0, 22.0, 0.5)
_reg_f("Matador", "mat_idle_min", 0.5, 0.0, 5.0, 0.1)
_reg_f("Matador", "mat_idle_max", 2.5, 0.5, 10.0, 0.1)
_reg_f("Matador", "mat_idle_min", 1.2, 0.0, 5.0, 0.1)
_reg_f("Matador", "mat_idle_max", 4.0, 0.5, 10.0, 0.1)
_reg_f("Matador", "mat_hit_threshold", 4.0, 1.0, 30.0)
_reg_f("Matador", "mat_ragdoll_impulse", 6.0, 0.5, 20.0)
# Ragdoll joint limits — lower swing/twist = stiffer corpse that keeps its
@@ -125,7 +134,7 @@ func _register_all() -> void:
_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_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
# into an estocada lunge so the blade covers ground toward the bull.
_reg_f("Matador", "mat_lunge_speed", 13.0, 0.0, 20.0)
@@ -139,7 +148,7 @@ func _register_all() -> void:
# Stops "sword's off to the side but I still died" — you're only gored when the
# blade is actually presented at you.
_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)
# Playback rate of the Draw_weapon clip used for both drawing and sheathing —
# higher = snappier unholster/holster.
@@ -153,43 +162,144 @@ func _register_all() -> void:
# bullfighter, caution for a slippery one. mat_w_commit is a stickiness bonus for
# the current choice so it doesn't dither between two near-tied options.
_reg_f("Matador", "mat_decide_interval", 0.2, 0.05, 1.0, 0.05)
_reg_f("Matador", "mat_w_aggression", 1.4, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_w_aggression", 1.1, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_w_caution", 1.0, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_w_flee", 0.8, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_w_showmanship", 1.0, 0.0, 3.0, 0.05)
# Showmanship (strut, circle the bull, taunt) — the supporting matadors lean on
# this to spread out and give the lead room, so a whole crowd reads as a bullring,
# not a swarm. Crank it for more posturing, drop it for a relentless mob.
_reg_f("Matador", "mat_w_showmanship", 1.6, 0.0, 3.0, 0.05)
# Weight on hurling the blade. Kept low — a thrown sword is an occasional flourish,
# not a barrage; raising this brings back the pelting.
_reg_f("Matador", "mat_w_throw", 0.35, 0.0, 3.0, 0.05)
# How many matadors press the bull at once. The nearest this-many engage (close,
# throw); everyone else keeps their distance and taunts, taking turns as the bull
# moves. 1 = strict one-on-one, higher = a bolder pack.
_reg_f("Matador", "mat_engage_slots", 2.0, 1.0, 8.0, 1.0)
# Preferred distance (m) a taunting matador circles the bull at while waiting its
# turn — the showmanship ring the crowd spreads out onto.
_reg_f("Matador", "mat_taunt_ring", 7.0, 3.0, 15.0, 0.5)
# How long (s) a matador safely out of the action holds a taunt before drifting to
# a new spot — much longer than the between-passes idle, so bystanders mostly pose
# for the crowd instead of milling about. Actual hold is randomised ±30%.
_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.
_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 ─────────────────────────────────────────────────────────────────
# Local seating of the sword in the right hand (drawn / fighting).
# Defaults are identity — the bone is oriented in Blender to seat the sword
# directly; these exist to fine-tune in-game without re-exporting.
# The blade model runs along its local +Z, but weapon_bone (like every rig bone)
# 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_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_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_z", 0.0, -180.0, 180.0, 1.0)
# 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
# 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_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_rot_x", -120.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_x", -110.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)
# ── Sword throw (matador ranged attack) ──────────────────────────────────
# Delay before a fresh sword appears in the holster after one is thrown.
# ── Spear throw (matador ranged attack) ──────────────────────────────────
# Local seating of the spear in the throwing hand during the wind-up. Tune to
# seat the shaft in the grip without re-exporting; flight orientation is derived
# from the aim direction at release, not from this pose.
_reg_f("Sword", "spear_pos_x", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "spear_pos_y", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "spear_pos_z", 0.0, -1.0, 1.0, 0.01)
_reg_f("Sword", "spear_rot_x", 0.0, -180.0, 180.0, 1.0)
_reg_f("Sword", "spear_rot_y", 0.0, -180.0, 180.0, 1.0)
_reg_f("Sword", "spear_rot_z", 0.0, -180.0, 180.0, 1.0)
# Delay before a fresh sword reappears in the holster if one is ever lost.
_reg_f("Sword", "mat_resword_delay", 2.0, 0.0, 10.0, 0.5)
_reg_f("Sword", "mat_throw_range", 18.0, 5.0, 40.0)
_reg_f("Sword", "mat_throw_min_dist", 5.0, 2.0, 15.0, 0.1)
_reg_f("Sword", "mat_throw_windup", 0.5, 0.2, 2.0, 0.05)
_reg_f("Sword", "mat_throw_release", 0.55, 0.1, 0.95, 0.01)
_reg_f("Sword", "mat_throw_speed", 24.0, 5.0, 60.0)
_reg_f("Sword", "mat_throw_spin", 14.0, 0.0, 40.0)
# Gravity scale on the thrown blade (low = flies straight to the aim point) and
# the vertical aim offset from the bull origin down onto its low collision body.
_reg_f("Sword", "mat_throw_gravity", 0.25, 0.0, 1.0, 0.05)
# Horizontal speed of the thrown spear; the vertical launch is solved from this
# and the gravity below so the arc lands on the bull. Lower = a higher, slower lob.
_reg_f("Sword", "mat_throw_speed", 11.0, 5.0, 60.0)
# Gravity scale on the spear (higher = a more pronounced arc) and the vertical aim
# offset from the bull origin down onto its low collision body.
_reg_f("Sword", "mat_throw_gravity", 1.0, 0.1, 3.0, 0.05)
_reg_f("Sword", "mat_throw_aim_y", -0.45,-1.5, 0.5, 0.05)
# Aim scatter (± degrees) so thrown blades can be read and dodged on the move.
_reg_f("Sword", "mat_throw_spread", 11.0, 0.0, 30.0, 0.5)
@@ -197,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_elbow", 80.0,-180.0, 180.0, 1.0)
# ── Abilities ─────────────────────────────────────────────────────────────
_reg_f("Abilities", "kick_cooldown", 1.5, 0.2, 10.0, 0.1)
_reg_f("Abilities", "kick_range", 3.5, 0.5, 8.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_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)
# Descent gravity multiplier — higher = faster crash, less airtime.
_reg_f("Abilities", "slam_fall_mult", 3.0, 1.0, 8.0, 0.5)
@@ -209,19 +319,24 @@ func _register_all() -> void:
_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_strength", 14.0, 1.0, 30.0)
_reg_f("Abilities", "dash_cooldown", 2.0, 0.5, 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)
# ── Roll (Rammus Powerball) ───────────────────────────────────────────────
# 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.
_reg_f("Abilities", "dash_decel", 7.0, 0.5, 30.0, 0.5)
# ── Roll (homing chain ball) ──────────────────────────────────────────────
# The bull curls into a ball and ACCELERATES the longer it rolls, from
# roll_base_speed up to roll_max_speed over roll_rampup_time. Turning is wide
# (momentum steering via roll_turn). On slamming into a matador the ball POPS:
# the target is launched skyward (roll_knockup) and the roll ends.
_reg_f("Roll", "roll_cooldown", 3.0, 0.5, 15.0, 0.1)
# roll_base_speed up to roll_max_speed over roll_rampup_time. It HOMES on the
# 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_turn is the homing turn rate. When no chain targets remain, the roll ends.
_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_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_max_speed", 95.0, 10.0, 200.0) # top speed once fully ramped
_reg_f("Roll", "roll_rampup_time", 2.0, 0.2, 6.0, 0.1) # time to reach top speed
_reg_f("Roll", "roll_turn", 7.0, 0.2, 20.0) # steer rate (WASD/arrows; higher = tighter)
_reg_f("Roll", "roll_turn", 7.0, 0.2, 20.0) # homing turn rate (higher = tighter tracking)
_reg_f("Roll", "roll_spin_radius", 1.4, 0.3, 4.0, 0.1) # ball radius for tumble spin (smaller = faster)
_reg_f("Roll", "roll_hit_radius", 2.4, 0.5, 6.0, 0.1) # contact radius that pops the ball
_reg_f("Roll", "roll_hit_strength", 22.0, 1.0, 40.0) # horizontal launch on the pop
@@ -235,8 +350,8 @@ func _register_all() -> void:
# a uniform out of bounds.
_reg_b("PS1", "show_ps1", true)
_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_dither_strength", 0.8, 0.0, 2.0, 0.05)
_reg_f("PS1", "ps1_color_depth", 4.5, 2.0, 8.0, 1.0)
_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_roll_speed", 3.0, 0.0, 10.0, 0.1)
_reg_f("PS1", "ps1_roll_strength", 1.0, 0.0, 1.0, 0.05)
@@ -250,6 +365,12 @@ func _register_all() -> void:
_reg_b("Debug", "show_wireframe", false)
_reg_b("Debug", "show_grid", false)
_reg_b("Debug", "show_axes", false)
# Force the on-screen touch controls (joystick + ability buttons) on even on a
# desktop build — otherwise they appear only when a touchscreen is detected.
_reg_b("Touch", "force_touch_controls", false)
# On-screen readout of live touch state (event count, last position, active zone) so
# touch problems can be diagnosed on the actual device, where no console is reachable.
_reg_b("Touch", "touch_debug", false)
func _reg_f(section: String, key: String, default: float,
@@ -300,11 +421,54 @@ func get_all() -> Dictionary:
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:
var p: Dictionary = _params[key]
cfg.set_value(p["section"], key, p["value"])
cfg.save(SAVE_PATH)
if p["value"] == p["default"]:
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:
+44 -3
View File
@@ -1,26 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
# Run from the project root regardless of where the script is invoked.
cd "$(dirname "$0")"
SERVER_USER="richard"
SERVER_HOST="192.168.1.168"
SERVER_PATH="/opt/bulloseum/data"
BUILD_DIR="build/web"
# Derive the version the same way GameVersion does (single source of truth):
# numeric version from project.godot, pre-release channel from game_version.gd.
VERSION_NUMBER="$(grep -oP 'config/version="\K[^"]+' project.godot)"
CHANNEL="$(grep -oP 'const CHANNEL: String = "\K[^"]*' game_version.gd || true)"
VERSION="v${VERSION_NUMBER}"
[ -n "$CHANNEL" ] && VERSION="${VERSION}-${CHANNEL}"
GIT_COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "building Bullosseum ${VERSION} (${GIT_COMMIT})"
mkdir -p "$BUILD_DIR"
godot --headless --export-release "Web" "$BUILD_DIR/index.html"
gzip -kf "$BUILD_DIR/index.wasm"
# Version stamp shipped alongside the flat build so the server records what's live.
cat > "$BUILD_DIR/version.txt" <<EOF
version=${VERSION}
commit=${GIT_COMMIT}
built=${BUILD_DATE}
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" \
"$BUILD_DIR/index.js" \
"$BUILD_DIR/index.wasm.gz" \
"$BUILD_DIR/index.pck" \
"$BUILD_DIR/index.${STAMP}.wasm.gz" \
"$BUILD_DIR/index.${STAMP}.pck" \
"$BUILD_DIR/index.audio.worklet.js" \
"$BUILD_DIR/index.audio.position.worklet.js" \
"$BUILD_DIR/index.icon.png" \
"$BUILD_DIR/index.apple-touch-icon.png" \
"$BUILD_DIR/index.png" \
"$BUILD_DIR/version.txt" \
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
echo "deployed 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]
name="Web"
platform="Web"
runnable=true
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/web/index.html"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.0.options]
@@ -21,7 +33,7 @@ custom_template/release=""
variant/extensions_support=false
variant/thread_support=true
vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=false
vram_texture_compression/for_mobile=true
html/export_icon=true
html/custom_html_shell=""
html/head_include=""
@@ -29,6 +41,7 @@ html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
progressive_web_app/enabled=false
progressive_web_app/ensure_cross_origin_isolation_headers=true
progressive_web_app/offline_page=""
progressive_web_app/display=1
progressive_web_app/orientation=0
@@ -36,3 +49,76 @@ progressive_web_app/icon_144x144=""
progressive_web_app/icon_180x180=""
progressive_web_app/icon_512x512=""
progressive_web_app/background_color=Color(0, 0, 0, 1)
threads/emscripten_pool_size=8
threads/godot_pool_size=4
[preset.1]
name="Windows Desktop"
platform="Windows Desktop"
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../Export/Bullosseum_020.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.1.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
Start-ScheduledTask -TaskName godot_remote_debug
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"
+349 -31
View File
@@ -5,25 +5,25 @@ const _CREAM := Color(0.93, 0.88, 0.72)
const _MUTED := Color(0.66, 0.60, 0.46)
# Per-ability trim colour, tying each tile (and its key cap) to that ability's FX.
# Order matches _ABILITY_ICONS: kick, slam, dash, roll.
# Order matches _ABILITY_ICONS: kick, dash, slam, roll.
const _ABILITY_ACCENT: Array[Color] = [
Color(1.00, 0.62, 0.16), # Kick — ember orange
Color(0.98, 0.34, 0.20), # Slam — crimson
Color(0.45, 0.75, 1.00), # Dash — sky blue
Color(0.98, 0.34, 0.20), # Slam — crimson
Color(1.00, 0.82, 0.32), # Roll — gold
]
const _ABILITY_ICONS: Array[String] = [
"res://HUD/KICK.png",
"res://HUD/SLAM.png",
"res://HUD/DASH.png",
"res://HUD/SLAM.png",
"res://HUD/ROLL.png",
]
const _ABILITY_ACTIONS: Array[StringName] = [
&"ability_kick", &"ability_slam", &"ability_dash", &"ability_roll",
&"ability_kick", &"ability_dash", &"ability_slam", &"ability_roll",
]
# Fallback label shown in a slot whose icon file fails to load.
const _ABILITY_NAMES: Array[String] = ["Kick", "Slam", "Dash", "Roll"]
const _ABILITY_NAMES: Array[String] = ["Kick", "Dash", "Slam", "Roll"]
var _controls_visible: bool = false
var _controls_panel: PanelContainer
@@ -34,18 +34,45 @@ var _player: Node = null
var _died_connected: bool = false
var _cd_overlays: Array[ColorRect] = []
var _cd_labels: Array[Label] = []
var _hp_label: Label
var _hp_segments: HBoxContainer
var _hp_seg_fills: Array[ColorRect] = []
var _bottom_col: VBoxContainer
var _ability_bar: HBoxContainer
var _ability_panels: Array[PanelContainer] = []
var _bull_bg: TextureRect
# Red damage vignette — a full-screen shader ColorRect flashed on every hit.
const _VIGNETTE_SHADER := "res://damage_vignette.gdshader"
var _vignette: ColorRect
var _vignette_mat: ShaderMaterial
# Bull-head backdrop for the gameplay HUD. The image is a wide horned silhouette; its
# solid "face" occupies the centred middle band, with horns flaring above. We size the
# image so that face band wraps the ability+HP cluster (horns crowning it above).
const _BULL_BG := "res://Assets/bull_hud_bg.png"
const _BULL_FACE_W: float = 0.529 # face width as a fraction of image width
const _BULL_ASPECT: float = 0.2741 # image height / width (267 / 974)
const _BULL_FACE_PAD: float = 1.15 # breathing room around the cluster within the face
# Win/lose screen. _game_over latches so the result is shown once; _result_win
# records which way it went (read by the gameplay test).
const _WIN_VIDEO := "res://videos/bull_win.ogv" # the bull triumphs
const _LOSS_VIDEO := "res://videos/matador_win.ogv" # the matador triumphs
# Beat between the run ending and the outcome screen. A loss is a short freeze on the
# dead bull; a win gives the player a longer stretch to strut a victory lap around the
# arena before the screen (and the pause) takes over.
const _LOSS_DELAY := 2.0
const _WIN_DELAY := 3.0
var _game_over: bool = false
var _result_win: bool = false
var _over_root: Control
var _over_video: VideoStreamPlayer
var _over_label: Label
var _over_continue: Button
var _over_again: Button
func _ready() -> void:
@@ -53,12 +80,89 @@ func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
_build_controls_panel()
_build_tab_hint()
_build_bottom_hud()
_build_bull_hud_bg()
_build_ability_bar()
_build_health()
_build_version_label()
_build_damage_vignette()
_build_game_over()
_build_touch_controls()
_update_control_hints()
_find_spawner.call_deferred()
# On-screen joystick + ability buttons for touch devices. Self-gating: the overlay
# only shows itself when a touchscreen is present (or DP force_touch_controls is on),
# so this is a no-op on desktop.
func _build_touch_controls() -> void:
add_child((load("res://touch_controls.gd") as Script).new())
# Bottom-centre column that groups the gameplay HUD (abilities on top, bull health
# directly beneath) so both read as one cluster instead of scattered corners.
func _build_bottom_hud() -> void:
_bottom_col = VBoxContainer.new()
_bottom_col.add_theme_constant_override("separation", 8)
_bottom_col.alignment = BoxContainer.ALIGNMENT_END
# Collapsed horizontal anchors so the column shrinks to its content and stays
# bottom-centred; the bull backdrop is sized against this content rect.
_bottom_col.anchor_left = 0.5
_bottom_col.anchor_right = 0.5
_bottom_col.anchor_top = 1.0
_bottom_col.anchor_bottom = 1.0
_bottom_col.grow_horizontal = Control.GROW_DIRECTION_BOTH
_bottom_col.grow_vertical = Control.GROW_DIRECTION_BEGIN
_bottom_col.offset_bottom = -16.0
_bottom_col.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_bottom_col)
# Bull-head plate that sits behind the ability + health cluster. It's a sibling of the
# cluster (not a parent) so it never constrains the cluster's layout; we size and place
# it against the cluster's rect in _position_bull_bg.
# Skipped on touch/phone: the on-screen thumb cluster already crowds the bottom edge, so
# the wide horned plate just adds clutter there.
func _build_bull_hud_bg() -> void:
if Controls.use_touch_ui():
return
_bull_bg = TextureRect.new()
if ResourceLoader.exists(_BULL_BG):
_bull_bg.texture = load(_BULL_BG)
# We size the rect ourselves to the image's aspect, so STRETCH_SCALE fills it
# exactly with no letterboxing.
_bull_bg.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_bull_bg.stretch_mode = TextureRect.STRETCH_SCALE
_bull_bg.modulate.a = 0.95
_bull_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_bull_bg)
# Draw the plate behind the cluster.
move_child(_bull_bg, _bottom_col.get_index())
_bottom_col.resized.connect(_position_bull_bg)
get_viewport().size_changed.connect(_position_bull_bg)
_position_bull_bg.call_deferred()
# Wrap the bull's solid "face" band around the current cluster rect: scale the image up
# so its face spans the cluster (plus padding), centre it horizontally on the cluster,
# and pin its bottom edge to the bottom of the screen so the plate is always seated flush
# against the screen edge on any resolution (horns crowning the cluster above).
func _position_bull_bg() -> void:
if _bull_bg == null or _bottom_col == null:
return
var r := _bottom_col.get_global_rect()
if r.size.x <= 1.0:
return
var vp_h := get_viewport().get_visible_rect().size.y
var bw := (r.size.x * _BULL_FACE_PAD) / _BULL_FACE_W
var bh := bw * _BULL_ASPECT
_bull_bg.size = Vector2(bw, bh)
_bull_bg.position = Vector2(
r.position.x + r.size.x * 0.5 - bw * 0.5,
vp_h - bh)
# Unobtrusive early-build tag in the bottom-right corner.
func _build_version_label() -> void:
var lbl := Label.new()
@@ -75,6 +179,7 @@ func _build_version_label() -> void:
func _process(_delta: float) -> void:
_update_control_hints()
if _player == null:
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
@@ -83,6 +188,9 @@ func _process(_delta: float) -> void:
if not _died_connected:
_died_connected = true
_player.died.connect(_on_player_died)
_player.health_changed.connect(_on_health_changed)
_player.hit_taken.connect(_on_hit_taken)
_on_health_changed(_player.call(&"get_hp"), _player.call(&"get_max_hp"))
for i: int in _cd_overlays.size():
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
var ov := _cd_overlays[i]
@@ -108,6 +216,9 @@ func _find_spawner() -> void:
func _unhandled_input(event: InputEvent) -> void:
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 _result_win:
_continue_to_lobby()
else:
_restart()
elif event.physical_keycode == KEY_TAB and not _game_over:
_toggle_controls()
@@ -127,11 +238,67 @@ func _restart() -> void:
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:
_controls_visible = not _controls_visible
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
_update_control_hints()
# The Tab hint and the keyboard controls plaque only make sense with a keyboard, so
# they stand down on touch devices — where touch_controls.gd shows the on-screen
# joystick and ability buttons instead. Shares Controls.use_touch_ui() with that
# overlay so exactly one control scheme shows on any given device.
func _update_control_hints() -> void:
var touch := Controls.use_touch_ui()
# The desktop tiles carry keyboard keycaps that mean nothing on a phone, and their
# STOP-filtered panels would sit over the joystick zone eating touches — touch_controls
# shows its own ability buttons instead, so drop the desktop bar on touch devices.
if _ability_bar != null:
_ability_bar.visible = not touch
if _game_over or touch:
_controls_panel.visible = false
_tab_hint.visible = false
else:
_controls_panel.visible = _controls_visible
_tab_hint.visible = not _controls_visible
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
# ── Damage vignette ───────────────────────────────────────────────────────────
# A full-screen ColorRect driven by damage_vignette.gdshader: transparent centre, red
# toward the edges, alpha ("flash") tweened up on a hit and eased back to nothing. Built
# before the game-over layer so the outcome screen always draws over it.
func _build_damage_vignette() -> void:
_vignette = ColorRect.new()
_vignette.set_anchors_preset(Control.PRESET_FULL_RECT)
_vignette.color = Color.WHITE # the shader writes COLOR outright; this is just a base
_vignette.mouse_filter = Control.MOUSE_FILTER_IGNORE
if ResourceLoader.exists(_VIGNETTE_SHADER):
_vignette_mat = ShaderMaterial.new()
_vignette_mat.shader = load(_VIGNETTE_SHADER)
_vignette_mat.set_shader_parameter("flash", 0.0)
_vignette.material = _vignette_mat
add_child(_vignette)
func _on_hit_taken(_cause: String) -> void:
if _vignette_mat == null:
return
var set_flash := func(v: float) -> void:
_vignette_mat.set_shader_parameter("flash", v)
create_tween().tween_method(set_flash, 0.9, 0.0, 0.5)\
.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
# ── Win / lose ──────────────────────────────────────────────────────────────
@@ -157,8 +324,14 @@ func _show_game_over(win: bool, _cause: String = "") -> void:
if _tab_hint:
_tab_hint.visible = false
# Swap in the matching outcome clip and loop it behind the buttons.
var path := _WIN_VIDEO if win else _LOSS_VIDEO
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!"
# 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
if ResourceLoader.exists(path):
_over_video.stream = load(path)
_over_video.play()
@@ -179,13 +352,40 @@ func _build_game_over() -> void:
add_child(_over_root)
# Full-screen outcome clip (bull win / matador win), looping.
# AspectRatioContainer keeps the 16:9 video's ratio and crops overflow (COVER),
# so it fills any screen shape without stretching or leaving dead bars.
var arc := AspectRatioContainer.new()
arc.set_anchors_preset(Control.PRESET_FULL_RECT)
arc.ratio = 16.0 / 9.0
arc.stretch_mode = AspectRatioContainer.STRETCH_COVER
arc.alignment_horizontal = AspectRatioContainer.ALIGNMENT_CENTER
arc.alignment_vertical = AspectRatioContainer.ALIGNMENT_CENTER
arc.process_mode = Node.PROCESS_MODE_ALWAYS
arc.mouse_filter = Control.MOUSE_FILTER_IGNORE
_over_root.add_child(arc)
_over_video = VideoStreamPlayer.new()
_over_video.set_anchors_preset(Control.PRESET_FULL_RECT)
_over_video.expand = true
_over_video.process_mode = Node.PROCESS_MODE_ALWAYS
_over_video.mouse_filter = Control.MOUSE_FILTER_IGNORE
_over_video.finished.connect(_over_video.play) # loop the outcome clip
_over_root.add_child(_over_video)
arc.add_child(_over_video)
# Outcome text centred over the video.
_over_label = Label.new()
_over_label.set_anchors_preset(Control.PRESET_CENTER_TOP)
_over_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
_over_label.grow_vertical = Control.GROW_DIRECTION_END
_over_label.offset_top = 80.0
_over_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_over_label.add_theme_font_override("font", UiFonts.title())
_over_label.add_theme_font_size_override("font_size", 64)
_over_label.add_theme_color_override("font_color", _GOLD)
_over_label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.9))
_over_label.add_theme_constant_override("outline_size", 6)
_over_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_over_root.add_child(_over_label)
# Buttons overlaid at the bottom of the clip.
var row := HBoxContainer.new()
@@ -200,14 +400,111 @@ func _build_game_over() -> void:
row.offset_bottom = -48.0
_over_root.add_child(row)
var again := _make_button("Go Again (Enter)")
again.pressed.connect(_restart)
row.add_child(again)
# Win → Continue to the run map; loss → Go Again from the top. Shown per-result in
# _show_game_over; both share the Enter shortcut.
_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")
menu.pressed.connect(_return_to_menu)
row.add_child(menu)
# ── Bull health ───────────────────────────────────────────────────────────────
# A row of gold-socketed pips beneath the ability bar — one pip per HP, so raising or
# lowering max HP visibly grows or shrinks the row. Lit pips run red and shade toward
# crimson as the bull is worn down; spent pips read as empty dark sockets.
const _HP_FULL: Color = Color(0.82, 0.16, 0.12)
const _HP_EMPTY: Color = Color(0.35, 0.05, 0.05)
const _HP_SOCKET: Color = Color(0.05, 0.02, 0.02, 0.92)
const _HP_PIP_W: float = 22.0
const _HP_PIP_H: float = 14.0
func _build_health() -> void:
var panel := PanelContainer.new()
panel.add_theme_stylebox_override(
"panel", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 12.0, 6.0))
panel.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
_bottom_col.add_child(panel)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
panel.add_child(row)
_hp_label = Label.new()
_hp_label.text = "BULL"
_hp_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_hp_label.add_theme_font_override("font", UiFonts.body())
_hp_label.add_theme_color_override("font_color", _GOLD)
_hp_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
_hp_label.add_theme_constant_override("outline_size", 3)
_hp_label.add_theme_font_size_override("font_size", 14)
row.add_child(_hp_label)
_hp_segments = HBoxContainer.new()
_hp_segments.add_theme_constant_override("separation", 4)
_hp_segments.size_flags_vertical = Control.SIZE_SHRINK_CENTER
_hp_segments.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(_hp_segments)
# The gold-underlined socket behind each pip — shared, non-mutating, so one instance
# is safe across every pip.
func _hp_socket_style() -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = _HP_SOCKET
s.set_corner_radius_all(3)
s.corner_detail = 4
s.border_width_bottom = 2
s.border_color = Color(UiTheme.GOLD.r, UiTheme.GOLD.g, UiTheme.GOLD.b, 0.45)
s.content_margin_left = 2.0
s.content_margin_right = 2.0
s.content_margin_top = 2.0
s.content_margin_bottom = 2.0
return s
func _rebuild_hp_pips(max_hp: int) -> void:
for child in _hp_segments.get_children():
child.queue_free()
_hp_seg_fills.clear()
var socket_style := _hp_socket_style()
for _i: int in maxi(max_hp, 0):
var socket := PanelContainer.new()
socket.custom_minimum_size = Vector2(_HP_PIP_W, _HP_PIP_H)
socket.clip_contents = true
socket.add_theme_stylebox_override("panel", socket_style)
socket.mouse_filter = Control.MOUSE_FILTER_IGNORE
_hp_segments.add_child(socket)
var fill := ColorRect.new()
fill.color = _HP_FULL
fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
socket.add_child(fill)
_hp_seg_fills.append(fill)
func _on_health_changed(current: int, max_hp: int) -> void:
if _hp_segments == null:
return
if _hp_seg_fills.size() != max_hp:
_rebuild_hp_pips(max_hp)
var cur := maxi(current, 0)
var frac := 0.0 if max_hp <= 0 else clampf(float(cur) / float(max_hp), 0.0, 1.0)
var lit := _HP_FULL.lerp(_HP_EMPTY, 1.0 - frac)
for i: int in _hp_seg_fills.size():
var fill := _hp_seg_fills[i]
fill.visible = i < cur
fill.color = lit
const _SLOT_SEPARATION: int = 10
@@ -215,14 +512,9 @@ const _SLOT_SEPARATION: int = 10
func _build_ability_bar() -> void:
_ability_bar = HBoxContainer.new()
_ability_bar.add_theme_constant_override("separation", _SLOT_SEPARATION)
_ability_bar.anchor_left = 0.5
_ability_bar.anchor_right = 0.5
_ability_bar.anchor_top = 1.0
_ability_bar.anchor_bottom = 1.0
_ability_bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
_ability_bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
_ability_bar.offset_bottom = -20.0
add_child(_ability_bar)
_ability_bar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
_ability_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
_bottom_col.add_child(_ability_bar)
for i: int in 4:
_ability_bar.add_child(_build_ability_slot(i))
_layout_ability_bar()
@@ -230,20 +522,17 @@ func _build_ability_bar() -> void:
# Size the ability slots as a fraction of the viewport (clamped) so the bar scales
# with the window, and keep it horizontally centred by matching the HBox offsets.
# with the window; the bottom column keeps the bar horizontally centred.
func _layout_ability_bar() -> void:
var vp := get_viewport().get_visible_rect().size
var slot := clampf(minf(vp.x * 0.055, vp.y * 0.11), 56.0, 104.0)
for panel: PanelContainer in _ability_panels:
panel.custom_minimum_size = Vector2(slot, slot)
var total := slot * 4.0 + float(_SLOT_SEPARATION) * 3.0
_ability_bar.offset_left = -total * 0.5
_ability_bar.offset_right = total * 0.5
func _build_ability_slot(idx: int) -> Control:
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 4)
vbox.add_theme_constant_override("separation", 3)
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(72.0, 72.0)
@@ -298,19 +587,48 @@ func _build_ability_slot(idx: int) -> Control:
stack.add_child(cd_lbl)
_cd_labels.append(cd_lbl)
# Keycap tucked directly beneath the tile — a solid dark plate wearing the
# ability's accent underline, so it reads as attached to its box without ever
# covering the artwork. Centred under the tile and tightly spaced.
var keycap := PanelContainer.new()
keycap.add_theme_stylebox_override("panel", _keycap_style(idx))
keycap.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
keycap.mouse_filter = Control.MOUSE_FILTER_IGNORE
vbox.add_child(keycap)
var key_lbl := Label.new()
key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
key_lbl.add_theme_font_override("font", UiFonts.body())
key_lbl.add_theme_color_override("font_color", _ABILITY_ACCENT[idx].lerp(_CREAM, 0.35))
key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
key_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
key_lbl.add_theme_font_override("font", UiFonts.body_bold())
key_lbl.add_theme_color_override("font_color", _CREAM)
key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
key_lbl.add_theme_constant_override("outline_size", 3)
key_lbl.add_theme_font_size_override("font_size", 13)
vbox.add_child(key_lbl)
key_lbl.add_theme_font_size_override("font_size", 14)
keycap.add_child(key_lbl)
return vbox
# Solid keycap plate for an ability bind: near-black fill with the ability's accent
# as a chunky top edge, so the accent bridges up to the tile it sits under (reads as
# attached) while the key stays on a high-contrast background against busy scenes.
func _keycap_style(idx: int) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = Color(0.04, 0.03, 0.02, 0.94)
s.set_corner_radius_all(4)
s.corner_detail = 4
s.set_border_width_all(1)
s.border_width_top = 3
s.border_color = _ABILITY_ACCENT[idx]
s.content_margin_left = 7.0
s.content_margin_right = 7.0
s.content_margin_top = 2.0
s.content_margin_bottom = 1.0
return s
func _build_tab_hint() -> void:
_tab_hint = Label.new()
_tab_hint.text = "Press 'Tab' to see controls"
@@ -353,8 +671,8 @@ func _build_controls_panel() -> void:
]
_row(vbox, move_keys, "Move", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_kick"), "Kick", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_slam"), "Slam", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_dash"), "Dash", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_slam"), "Slam", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_roll"), "Roll (Powerball: ram the matador!)", _CREAM)
_row(vbox, "Scroll", "Zoom", _CREAM)
_row(vbox, "Tab", "Toggle controls", _MUTED)
+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
+109 -3
View File
@@ -57,23 +57,26 @@ const ACTION_LABELS: Dictionary = {
&"move_left": "Move Left",
&"move_right": "Move Right",
&"ability_kick": "Kick",
&"ability_slam": "Slam",
&"ability_dash": "Dash",
&"ability_slam": "Slam",
&"ability_roll": "Roll",
}
@onready var main_panel: Control = $MainPanel
@onready var options_panel: Control = $OptionsPanel
@onready var credits_panel: Control = $CreditsPanel
@onready var options_vbox: VBoxContainer = $OptionsPanel/VBox
@onready var options_scroll: ScrollContainer = $OptionsPanel/VBox/Scroll
@onready var rebind_list: VBoxContainer = $OptionsPanel/VBox/Scroll/RebindList
@onready var fade_rect: ColorRect = $FadeRect
@onready var play_button: Button = $MainPanel/PlayButton
@onready var play_button: Button = $VideoAspect/CapeLayer/PlayButton
@onready var options_button: Button = $MainPanel/OptionsButton
@onready var credits_button: Button = $MainPanel/CreditsButton
@onready var exit_button: Button = $MainPanel/ExitButton
@onready var options_back: Button = $OptionsPanel/VBox/BackButton
@onready var credits_back: Button = $CreditsPanel/VBox/BackButton
@onready var video: VideoStreamPlayer = $VideoStreamPlayer
@onready var video: VideoStreamPlayer = $VideoAspect/VideoStreamPlayer
@onready var blur_video: VideoStreamPlayer = $VideoBlur/BlurVideo
var _listening_action: StringName = &""
var _listening_button: Button = null
@@ -87,6 +90,9 @@ func _ready() -> void:
if video.stream != null:
video.play()
video.finished.connect(video.play) # loop the background clip
if blur_video.stream != null:
blur_video.play()
blur_video.finished.connect(blur_video.play) # loop the blurred backdrop
play_button.pressed.connect(_on_play_pressed)
options_button.pressed.connect(_on_options_pressed)
@@ -96,6 +102,7 @@ func _ready() -> void:
credits_back.pressed.connect(_close_subpanels)
_apply_theme()
_build_version_label()
_build_options_toggles()
_build_rebind_rows()
play_button.grab_focus()
@@ -183,6 +190,101 @@ func _process(_delta: float) -> void:
play_button.anchor_bottom = cape.y
# Player-facing option toggles and audio sliders, seated above the rebind list.
func _build_options_toggles() -> void:
var idx := options_scroll.get_index()
_add_section_label("── Audio ──", idx); idx += 1
_add_slider_row("Master Volume", Settings.volume_master, Settings.set_volume_master, idx); idx += 1
_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:
var row := HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var name_label := Label.new()
name_label.text = label_text
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
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 toggle := CheckButton.new()
toggle.button_pressed = value
toggle.add_theme_color_override("font_color", MENU_GOLD)
toggle.toggled.connect(func(on: bool) -> void: setter.call(on))
row.add_child(toggle)
options_vbox.add_child(row)
options_vbox.move_child(row, at_index)
func _build_rebind_rows() -> void:
for child in rebind_list.get_children():
child.queue_free()
@@ -213,6 +315,7 @@ func _build_rebind_rows() -> void:
func _on_play_pressed() -> void:
_set_buttons_disabled(true)
Run.start_new_run() # fresh map for this run; the arena is the intro fight
var tween := create_tween()
tween.tween_property(fade_rect, "color:a", 1.0, FADE_TIME)
tween.tween_callback(func() -> void:
@@ -221,12 +324,14 @@ func _on_play_pressed() -> void:
func _on_options_pressed() -> void:
main_panel.visible = false
play_button.visible = false
options_panel.visible = true
options_back.grab_focus()
func _on_credits_pressed() -> void:
main_panel.visible = false
play_button.visible = false
credits_panel.visible = true
credits_back.grab_focus()
@@ -240,6 +345,7 @@ func _close_subpanels() -> void:
options_panel.visible = false
credits_panel.visible = false
main_panel.visible = true
play_button.visible = true
play_button.grab_focus()
+217 -124
View File
@@ -22,6 +22,7 @@ var _sim: PhysicalBoneSimulator3D = null
var _anim_player: AnimationPlayer = null
var _wander_target: Vector3 = Vector3.ZERO
var _idle_timer: float = 0.0
var _taunt_swap: float = 0.0
var _idle_anim: StringName = _ANIM_IDLE
var _bull: CharacterBody3D = null
var _step_dir: Vector3 = Vector3.ZERO
@@ -34,6 +35,7 @@ var _swing_timer: float = 0.0
var _lunge_timer: float = 0.0
var _roll_timer: float = 0.0
var _roll_dir: Vector3 = Vector3.ZERO
var _is_spawning: bool = false
var _throw_timer: float = 0.0
var _throw_aim_cd: float = 0.0
var _throw_dir: Vector3 = Vector3.ZERO
@@ -50,8 +52,8 @@ var _drawing: bool = false
var _sheathing: bool = false
var _draw_timer: float = 0.0
var _draw_len: float = 0.0
var _resword_timer: float = 0.0
var _sword_blade_area: Area3D = null
var _spear_node: Node3D = null
var _blade_hit_cd: float = 0.0
var _death_player: AudioStreamPlayer = null
var _blood_burst: CPUParticles3D = null
@@ -59,6 +61,7 @@ var _blood_burst: CPUParticles3D = null
@onready var _mesh: Node3D = $matador2
@onready var _hit_area: Area3D = $HitArea
const _ANIM_RUN: StringName = &"Run"
const _ANIM_IDLE: StringName = &"Taunt"
const _ANIM_ATTACK: StringName = &"Attack"
@@ -74,6 +77,9 @@ const _ANIM_TAUNTS: Array[StringName] = [&"Taunt", &"Taunt_B", &"Taunt_C"]
const _DRAW_GRAB_AT: float = 0.55
const _SWORD_SCENE: PackedScene = preload("res://Assets/sword.glb")
const _SPEAR_SCENE: PackedScene = preload("res://Assets/spear.glb")
const _GRAVITY: float = 9.8 # arena gravity, for the spear's ballistic arc
const _STEER_LOOKAHEAD: float = 3.5 # longer = turns before hitting wall
const _STEER_INTERVAL: float = 0.12 # recompute direction at most ~8×/sec
@@ -88,15 +94,23 @@ const _TURN_FACE: float = 16.0 # face the bull while engaging
const _TURN_SHARP: float = 24.0 # snap onto a sidestep / roll direction
const RigidSkin = preload("res://rigid_skin.gd")
func _ready() -> void:
add_to_group(&"matador")
_skeleton = _find_skeleton(_mesh)
_anim_player = _find_anim_player(_mesh)
if _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:
for anim in [_ANIM_RUN, _ANIM_ATTACK, _ANIM_ROLL] + _ANIM_TAUNTS:
for anim in [_ANIM_RUN, _ANIM_ATTACK] + _ANIM_TAUNTS:
_ensure_loop(anim)
_flatten_roll_travel()
if not _anim_player.has_animation(_ANIM_RUN):
push_warning("Matador: expected animations not found. Available: %s" %
str(_anim_player.get_animation_list()))
@@ -104,7 +118,7 @@ func _ready() -> void:
_anim_player.play(_ANIM_IDLE)
_setup_sword()
DP.any_changed.connect(_on_dp_changed)
_throw_aim_cd = randf_range(0.3, 1.0)
_throw_aim_cd = randf_range(2.0, 5.0)
_hit_area.body_entered.connect(_on_body_entered)
_pick_wander_target()
_blood_burst = preload("res://blood_burst.gd").new()
@@ -113,11 +127,13 @@ func _ready() -> void:
_ole_player = AudioStreamPlayer.new()
_ole_player.stream = load("res://sounds/ole.ogg")
_ole_player.volume_db = 0.0
_ole_player.bus = &"Effects"
add_child(_ole_player)
if ResourceLoader.exists("res://sounds/matador_death.ogg"):
_death_player = AudioStreamPlayer.new()
_death_player.stream = load("res://sounds/matador_death.ogg")
_death_player.volume_db = 2.0
_death_player.bus = &"Effects"
add_child(_death_player)
@@ -138,6 +154,10 @@ func _acquire_bull() -> void:
func _physics_process(delta: float) -> void:
_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:
var o := global_position + Vector3.UP * 0.6
DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9))
@@ -148,12 +168,6 @@ func _physics_process(delta: float) -> void:
_dodge_cd = maxf(0.0, _dodge_cd - delta)
_blade_hit_cd = maxf(0.0, _blade_hit_cd - delta)
_throw_aim_cd = maxf(0.0, _throw_aim_cd - delta)
# Pull a replacement sword from the holster a beat after throwing the last one.
if _state != State.RAGDOLL and not is_instance_valid(_sword_node) \
and (_sword_rest_attach != null or _sword_hand_attach != null):
_resword_timer -= delta
if _resword_timer <= 0.0:
_spawn_sword()
if _state != State.RAGDOLL and _bull != null:
_update_ai_state(delta)
@@ -216,20 +230,30 @@ func _choose_intent() -> Intent:
var throw_ok := _throw_aim_cd <= 0.0 and is_instance_valid(_sword_node) \
and dist >= DP.f("mat_throw_min_dist") and dist <= DP.f("mat_throw_range")
# Only the nearest few matadors work the bull at once; the rest hang back and
# taunt (see _engagement_rank). This turns a crowd into a bullring — matadors
# spreading out and taking turns — instead of a swarm all piling on together.
var is_engager := _engagement_rank() < maxi(1, int(DP.f("mat_engage_slots")))
# Supporting matadors strut and circle constantly; the lead only struts between
# passes, once the bull is back out past horn reach.
var strut := 0.65 if not is_engager else 0.3 * beyond
var engage_mult := 1.0 if is_engager else 0.15
var score := {
# Only drift when the bull is out past throw range; up close this is ~0.
Intent.WANDER: DP.f("mat_w_showmanship") * 0.3 * beyond,
# Showmanship: circle the bull at a taunting distance and pose for the crowd.
Intent.WANDER: DP.f("mat_w_showmanship") * strut,
# Retreat only when the bull is close AND actually dangerous.
Intent.FLEE: DP.f("mat_w_flee") * close * (0.25 + 0.75 * danger),
# Press the attack; ease off a touch when a charge is barrelling in.
# Press the attack; ease off a touch when a charge is barrelling in. Supporting
# matadors barely press — they yield the pass to whoever's engaging.
Intent.ATTACK: DP.f("mat_w_aggression") * (0.45 + 0.55 * close) \
* (1.0 - 0.55 * danger) * dodge_ready,
* (1.0 - 0.55 * danger) * dodge_ready * engage_mult,
# Sidestep / plant against an incoming charge.
Intent.DODGE: DP.f("mat_w_caution") * danger * (1.0 if _dodge_cd <= 0.0 else 0.0),
# Hurl the blade whenever there's a clean lane at range — the ranged threat
# that punishes a bull for hanging back out of horn reach.
Intent.THROW: ((DP.f("mat_w_showmanship") * 0.55 + DP.f("mat_w_aggression") * 0.6) \
* (0.35 + 0.65 * (1.0 - close))) if throw_ok else -1.0,
# Hurl the blade — an occasional ranged flourish from an engaging matador when
# there's a clean lane, not a barrage (see mat_w_throw). Supporters never pelt.
Intent.THROW: (DP.f("mat_w_throw") * (0.35 + 0.65 * (1.0 - close))) \
if (throw_ok and is_engager) else -1.0,
}
score[_intent] = float(score[_intent]) + DP.f("mat_w_commit")
@@ -277,6 +301,40 @@ func _enter_intent(intent: Intent) -> void:
_start_throw()
# How many other live matadors are closer to the bull than this one. 0 = the
# closest (the natural lead engager); the utility brain gives the nearest
# mat_engage_slots the go-ahead to press while the rest spread out and taunt.
func _engagement_rank() -> int:
if _bull == null:
return 0
var my_d := global_position.distance_squared_to(_bull.global_position)
var rank := 0
for node: Node in get_tree().get_nodes_in_group(&"matador"):
if node == self:
continue
var other := node as Node3D
if other == null or not is_instance_valid(other) or not other.call(&"is_active"):
continue
if other.global_position.distance_squared_to(_bull.global_position) < my_d:
rank += 1
return rank
# Live and vying for the bull's attention — a ragdolled corpse doesn't hold a slot.
func is_active() -> bool:
return _state != State.RAGDOLL
# True when this matador is safely out of the action — not one of the engaging few,
# and with the bull far enough off to strike a long, unhurried taunt for the crowd.
func _out_of_harm() -> bool:
if _bull == null:
return true
if _engagement_rank() < maxi(1, int(DP.f("mat_engage_slots"))):
return false
return global_position.distance_to(_bull.global_position) >= DP.f("mat_taunt_ring") * 0.6
# Bull is moving fast and aimed within ~30° of the matador
func _is_charge_incoming() -> bool:
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
@@ -351,6 +409,14 @@ func _tick_wander(delta: float) -> void:
if _drawing or _sheathing or _idle_timer > 0.0:
_idle_timer = maxf(0.0, _idle_timer - delta)
_decelerate(10.0, delta)
# Taunt toward the bull so the posturing plays to it, not off into space.
if _bull != null and _idle_timer > 0.0:
_face_point(_bull.global_position, delta, _TURN_MOVE)
# Cycle gestures through a long hold so a posing matador stays lively.
_taunt_swap -= delta
if _taunt_swap <= 0.0 and not _drawing and not _sheathing:
_idle_anim = _pick_taunt()
_taunt_swap = randf_range(1.5, 3.0)
_play_anim(_idle_anim)
move_and_slide()
return
@@ -358,8 +424,12 @@ func _tick_wander(delta: float) -> void:
var to_target := Vector3(
_wander_target.x - global_position.x, 0.0, _wander_target.z - global_position.z)
if to_target.length() < 0.8:
_idle_timer = randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
# A matador out of harm's way settles into a long taunt for the crowd; one still
# in the mix only pauses briefly between passes.
_idle_timer = randf_range(DP.f("mat_taunt_hold") * 0.7, DP.f("mat_taunt_hold") * 1.3) \
if _out_of_harm() else randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
_idle_anim = _pick_taunt()
_taunt_swap = randf_range(1.5, 3.0)
_pick_wander_target()
else:
var dir := _steer_clear(to_target.normalized(), delta)
@@ -482,7 +552,7 @@ func _tick_attack(delta: float) -> void:
if _swing_timer > 0.0:
_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)
else:
_decelerate(22.0, delta)
@@ -509,6 +579,23 @@ func _enter_roll(hit_dir: Vector3) -> void:
_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:
_roll_timer -= delta
if _roll_timer <= 0.0:
@@ -528,7 +615,8 @@ func _start_throw() -> void:
_throw_timer = DP.f("mat_throw_windup")
velocity = Vector3.ZERO
_cancel_transitions()
_carry_sword(true) # blade must be in hand for the wind-up and release
_carry_sword(false) # the sword is melee-only — holster it for the throw
_spawn_spear_in_hand() # a fresh spear is drawn for the overhand throw
if _anim_player:
_anim_player.pause()
var to_bull := _bull.global_position - global_position
@@ -552,8 +640,8 @@ func _tick_throw(delta: float) -> void:
_pose_throw_arm(phase)
var t_rel := DP.f("mat_throw_release")
if prev < t_rel and phase >= t_rel and is_instance_valid(_sword_node):
_release_sword()
if prev < t_rel and phase >= t_rel and is_instance_valid(_spear_node):
_release_spear()
if _throw_timer <= 0.0:
_end_throw()
@@ -562,20 +650,18 @@ func _tick_throw(delta: float) -> void:
func _end_throw() -> void:
_state = State.FLEE
_clear_spear() # drop any spear still in hand if the wind-up was cut short
_dodge_cd = DP.f("mat_dodge_cooldown")
# Space throws out so the utility brain doesn't immediately vote another one,
# but keep them frequent enough to pressure a bull that hangs back at range.
_throw_aim_cd = randf_range(1.6, 3.2)
# Space throws well apart so a hurl stays a flourish, not a barrage.
_throw_aim_cd = randf_range(4.0, 7.0)
_intent = Intent.FLEE
# Blend straight into locomotion so the manually-posed throw arm eases out
# (a bare resume() would snap and leave the player on whatever it paused on).
# Blend to locomotion so the manually-posed throw arm eases out (resume() snaps).
if _anim_player:
_anim_player.play(_ANIM_RUN)
# Drive arm_R + forearm_R through a cock-back → release arc while the
# AnimationPlayer is paused. Swing is on bone-local X (see CLAUDE.md rig notes);
# angles are DP-tunable so the arc can be dialled in per rig.
# AnimationPlayer is paused. Swing is on bone-local X (see CLAUDE.md), DP-tunable.
func _pose_throw_arm(phase: float) -> void:
if not _skeleton:
return
@@ -605,82 +691,59 @@ func _set_bone_swing(bone: String, deg: float) -> void:
_skeleton.set_bone_pose_rotation(idx, rest * Quaternion(Vector3.RIGHT, deg_to_rad(deg)))
# Detach the sword from the hand and launch it as a spinning physics projectile
# that damages the bull on contact, then settles and despawns.
func _release_sword() -> void:
if not is_instance_valid(_sword_node):
# Draw a fresh spear into the throwing hand for the wind-up (no melee collider).
func _spawn_spear_in_hand() -> void:
if is_instance_valid(_spear_node) or _sword_hand_attach == null:
return
var sword := _sword_node
var blade := _sword_blade_area
var gx := sword.global_transform
_sword_node = null
_sword_blade_area = null
_sword_in_hand = false
_resword_timer = DP.f("mat_resword_delay")
# The RigidBody owns collisions now — silence the melee blade Area it carries.
if is_instance_valid(blade):
blade.monitoring = false
_spear_node = _SPEAR_SCENE.instantiate() as Node3D
_sword_hand_attach.add_child(_spear_node)
_spear_node.position = Vector3(
DP.f("spear_pos_x"), DP.f("spear_pos_y"), DP.f("spear_pos_z"))
_spear_node.rotation_degrees = Vector3(
DP.f("spear_rot_x"), DP.f("spear_rot_y"), DP.f("spear_rot_z"))
var rb := RigidBody3D.new()
rb.collision_layer = 0
rb.collision_mask = 1
rb.contact_monitor = true
rb.max_contacts_reported = 6
# Fly nearly straight to the aim point instead of lobbing — the old horizontal
# throw sailed clean over the bull's low body. A small gravity_scale keeps a
# touch of drop for feel without dropping short.
rb.gravity_scale = DP.f("mat_throw_gravity")
get_tree().current_scene.add_child(rb)
rb.global_transform = gx
sword.reparent(rb, true)
# Physics collider along the blade (GLB +Z), which equals rb-local +Z because
# the mesh kept its global transform and rb adopted it. Fattened so a fast throw
# reliably overlaps the bull's collision spheres instead of tunnelling past.
var cap := CapsuleShape3D.new()
cap.radius = 0.14
cap.height = 1.4
var cs := CollisionShape3D.new()
cs.shape = cap
cs.position = Vector3(0.0, 0.0, 0.7)
cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
rb.add_child(cs)
func _clear_spear() -> void:
if is_instance_valid(_spear_node):
_spear_node.queue_free()
_spear_node = null
# Aim at the bull's BODY (its collision spheres ride low, ~0.5 m below the
# origin), leading a moving target so it arrives where the bull will be. The aim
# keeps its vertical component so the blade drives into the body, not over it.
var speed := maxf(DP.f("mat_throw_speed"), 0.1)
var dir := (_throw_dir + Vector3.UP * 0.0)
# Hurl the held spear on a ballistic arc that lands on the bull's low body. The
# launch velocity is solved so gravity (mat_throw_gravity) carries it down onto the
# lead point; SpearProjectile aims the shaft along the arc and embeds it on a hit.
func _release_spear() -> void:
if not is_instance_valid(_spear_node):
return
var spear := _spear_node
var origin := spear.global_position
_spear_node = null
var hs := maxf(DP.f("mat_throw_speed"), 0.1) # horizontal speed
var g := _GRAVITY * maxf(DP.f("mat_throw_gravity"), 0.05)
# Aim low (the bull's collision spheres ride ~0.5 m below its origin) and lead it
# across the arc's flight time.
var target := origin + _throw_dir * 5.0
if is_instance_valid(_bull):
var target := _bull.global_position + Vector3(0.0, DP.f("mat_throw_aim_y"), 0.0)
var flat := target - gx.origin
target = _bull.global_position + Vector3(0.0, DP.f("mat_throw_aim_y"), 0.0)
var lead := target - origin
lead.y = 0.0
target += _bull.velocity * (lead.length() / hs)
var flat := target - origin
var rise := flat.y
flat.y = 0.0
var flight := flat.length() / speed
target += _bull.velocity * flight
var to_target := target - gx.origin
if to_target.length() > 0.1:
dir = to_target.normalized()
# A little aim scatter so throws are a threat to READ and dodge, not a hitscan —
# a bull that keeps moving can slip them, a stationary one gets pinned.
var dist := flat.length()
var hdir := flat.normalized() if dist > 0.01 else _throw_dir
# Aim scatter so the arc can be read and sidestepped, not a hitscan.
var spread := deg_to_rad(DP.f("mat_throw_spread"))
dir = dir.rotated(Vector3.UP, randf_range(-spread, spread)).normalized()
rb.linear_velocity = dir * speed
rb.angular_velocity = dir.cross(Vector3.UP).normalized() * -DP.f("mat_throw_spin")
var hit_done := [false]
rb.body_entered.connect(func(body: Node) -> void:
if hit_done[0]:
return
if body.is_in_group(&"player"):
hit_done[0] = true
body.call(&"take_sword_hit", "thrown")
)
var cleanup := get_tree().create_timer(6.0)
cleanup.timeout.connect(func() -> void:
if is_instance_valid(rb):
rb.queue_free()
)
hdir = hdir.rotated(Vector3.UP, randf_range(-spread, spread)).normalized()
# Ballistic solve: with horizontal speed hs over distance dist, the flight lasts t;
# pick the vertical launch speed that lands the arc on the aim point's height.
var t := dist / hs
var vy := (rise + 0.5 * g * t * t) / t
var vel := hdir * hs + Vector3.UP * vy
SpearProjectile.launch(get_tree().current_scene, spear, origin, vel,
maxf(DP.f("mat_throw_gravity"), 0.05))
func _tick_ragdoll(_delta: float) -> void:
@@ -756,6 +819,7 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float, up_boost: float = 0.0)
_state = State.RAGDOLL
if _sword_blade_area:
_sword_blade_area.monitoring = false
_clear_spear() # a spear mid-wind-up dies with the matador, not in the air
killed.emit()
var cleanup_timer := get_tree().create_timer(4.0)
cleanup_timer.timeout.connect(func() -> void:
@@ -866,6 +930,32 @@ func _ensure_loop(anim_name: StringName) -> void:
anim.loop_mode = Animation.LOOP_LINEAR
# Roll was authored *travelling* (COG drives ~4.5 m forward on Z), which snapped the
# mesh back when the clip ended. _tick_roll moves the dodge, so strip that COG travel.
func _flatten_roll_travel() -> void:
if _anim_player == null or not _anim_player.has_animation(_ANIM_ROLL):
return
var anim := _anim_player.get_animation(_ANIM_ROLL)
if anim.has_meta(&"_travel_flattened"):
return
anim.set_meta(&"_travel_flattened", true)
var cog := anim.find_track(NodePath("Armature/Skeleton3D:COG"), Animation.TYPE_POSITION_3D)
if cog < 0:
return
# Snapshot the COG travel before mutating (flattening COG first would zero it).
var travel: Dictionary = {}
for i in anim.get_track_count():
if anim.track_get_type(i) == Animation.TYPE_POSITION_3D:
for k in anim.track_get_key_count(i):
var t := anim.track_get_key_time(i, k)
travel[t] = anim.position_track_interpolate(cog, t).z
for i in anim.get_track_count():
if anim.track_get_type(i) == Animation.TYPE_POSITION_3D:
for k in anim.track_get_key_count(i):
var v: Vector3 = anim.track_get_key_value(i, k)
anim.track_set_key_value(i, k, v - Vector3(0.0, 0.0, travel[anim.track_get_key_time(i, k)]))
func _pick_taunt() -> StringName:
var choices: Array[StringName] = []
for anim in _ANIM_TAUNTS:
@@ -893,13 +983,25 @@ func _play_anim(anim_name: StringName) -> void:
func _pick_wander_target() -> void:
# 35% chance: pick a point orbiting close to the bull for a daring pass
if _bull != null and randf() < 0.35:
var angle := randf() * TAU
var r := randf_range(1.5, 4.0)
# Circle the bull for showmanship: mostly settle on the taunt ring (spreading the
# crowd out around it), with an occasional close pass for daring flair.
if _bull != null:
var bull_flat := _bull.global_position
bull_flat.y = 0.0
var r: float
if randf() < 0.2:
r = randf_range(1.5, 4.0) # close daring pass
else:
var ring := DP.f("mat_taunt_ring")
r = randf_range(ring * 0.7, ring * 1.3) # the showmanship ring
var angle := randf() * TAU
_wander_target = bull_flat + Vector3(cos(angle) * r, 0.0, sin(angle) * r)
# Keep the pose inside the fighting floor rather than backing onto the benches.
var arena_r := DP.f("arena_spawn_radius")
var flat := Vector2(_wander_target.x, _wander_target.z)
if flat.length() > arena_r:
flat = flat.normalized() * arena_r
_wander_target = Vector3(flat.x, 0.0, flat.y)
return
var radius := DP.f("mat_wander_radius")
var angle := randf() * TAU
@@ -909,13 +1011,10 @@ func _pick_wander_target() -> void:
# ── Sword attachment ──────────────────────────────────────────────────────────
# The rig (matador_v03) carries two purpose-built bones for the sword:
# weapon_bone — the grip pose in the right hand (drawn / fighting)
# weapon_rest_bone — the holstered pose at the hip (sheathed / wandering)
# The sword lives in the holster by default and is carried to the hand while the
# matador is in a combat state (see _carry_sword). Each bone has its own
# DP-tunable local offset/rotation (sword_* for the hand, sword_rest_* for the
# holster) so the two poses can be seated independently without re-exporting.
# The rig (matador_v03) carries two purpose-built bones: weapon_bone (right-hand
# grip) and weapon_rest_bone (hip holster). The sword rides in the holster and is
# carried to the hand in combat (see _carry_sword); each bone has its own DP-tunable
# local offset/rotation (sword_* / sword_rest_*) so the poses seat independently.
func _setup_sword() -> void:
if not _skeleton:
return
@@ -934,8 +1033,7 @@ func _setup_sword() -> void:
_spawn_sword()
# Instantiate a fresh sword into the holster, with its own blade collider. Called
# at spawn and again after a throw, so a matador can keep drawing replacements.
# Instantiate the matador's sword into the holster, with its own blade collider.
func _spawn_sword() -> void:
if is_instance_valid(_sword_node):
return
@@ -949,15 +1047,14 @@ func _spawn_sword() -> void:
_drawing = false
_apply_sword_grip()
# Blade collider spans the steel (hilt at local origin, blade running +Z out
# to ~1.4 m). Only monitors while the sword is in hand (see _carry_sword).
# Blade collider along +Z (hilt at origin), tipped from the capsule's default Y.
# Only monitors while the sword is in hand (see _carry_sword).
var blade_cap := CapsuleShape3D.new()
blade_cap.radius = 0.18
blade_cap.height = 1.30
var blade_cs := CollisionShape3D.new()
blade_cs.shape = blade_cap
blade_cs.position = Vector3(0.0, 0.0, 0.8)
# CapsuleShape3D runs along Y by default; tip it onto +Z to follow the blade.
blade_cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
_sword_blade_area = Area3D.new()
@@ -969,12 +1066,11 @@ func _spawn_sword() -> void:
_sword_blade_area.body_entered.connect(_on_blade_hit)
# The sword is carried by ACTION, not distance: drawn only while striking
# (ATTACK / THROW) and holstered while running, dodging, bracing or taunting.
# THROW snaps the blade out (its arm cock-back covers the motion); everything
# else animates with the Draw_weapon clip — forward to draw, reversed to sheathe.
# The sword is MELEE-only: drawn while fighting up close (ATTACK) or guarding
# (BRACE / SIDESTEP), holstered otherwise. Ranged throws use a spear instead
# (see _spawn_spear_in_hand), so THROW keeps the sword holstered.
func _wants_sword_drawn() -> bool:
return _state == State.ATTACK or _state == State.THROW \
return _state == State.ATTACK \
or _state == State.BRACE or _state == State.SIDESTEP
@@ -1019,8 +1115,7 @@ func _begin_sheathe() -> void:
_anim_player.play(_ANIM_DRAW, -1.0, -_draw_speed(), true)
# Progress a draw or sheathe; the blade reparents at the grab point (mirrored for
# the reverse sheathe), and the transition ends when the clip's runtime elapses.
# Progress a draw or sheathe; the blade reparents at the grab point (mirrored on the reverse).
func _advance_draw(delta: float) -> void:
if not _drawing and not _sheathing:
return
@@ -1049,9 +1144,7 @@ func _draw_speed() -> float:
return maxf(DP.f("mat_draw_speed"), 0.1)
# Move the sword between the hand grip and the hip holster. Reparenting follows
# whichever bone the BoneAttachment tracks; the local grip transform is reapplied
# so the seating is identical in both sockets.
# Move the sword between hand grip and hip holster, reapplying the local seating.
func _carry_sword(in_hand: bool) -> void:
if not is_instance_valid(_sword_node):
return
-69
View File
@@ -1,69 +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 _resolved: bool = false
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)
for pos: Vector3 in _spread_positions(_spawn_count, bull_angle + PI, arena_r):
_spawn_at(pos)
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, clustered on the far side (centred on `center_angle`)
# so every matador stays near the point diametrically opposite the bull.
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 := TAU * 0.33
for i: int in count:
var t := float(i) / float(count - 1) - 0.5 # -0.5 .. 0.5 across the arc
var angle := center_angle + t * arc
var radius := randf_range(arena_r * 0.7, arena_r)
positions.append(Vector3(cos(angle) * radius, 1.0, sin(angle) * radius))
return positions
func _spawn_at(pos: Vector3) -> void:
var m: Node3D = MATADOR.instantiate()
m.position = pos
get_parent().add_child(m)
m.killed.connect(_on_matador_killed)
_alive += 1
# Every matador down = the bull wins. No waves, no respawns — one clean arena.
func _on_matador_killed() -> void:
matador_killed.emit()
_alive -= 1
if _alive <= 0 and not _resolved:
_resolved = true
all_defeated.emit()
+25
View File
@@ -0,0 +1,25 @@
shader_type canvas_item;
// Blurred, darkened backdrop for the intro video. A COVER-scaled copy of the
// clip sits behind the sharp fitted clip so portrait/ultrawide screens fill
// edge-to-edge instead of showing dead bars.
uniform float blur_radius : hint_range(0.0, 8.0) = 3.0;
uniform float darken : hint_range(0.0, 1.0) = 0.35;
void fragment() {
vec2 px = TEXTURE_PIXEL_SIZE * blur_radius;
vec4 sum = vec4(0.0);
// 3x3 gaussian-ish tap, weights sum to 16.
sum += texture(TEXTURE, UV + vec2(-px.x, -px.y)) * 1.0;
sum += texture(TEXTURE, UV + vec2(0.0, -px.y)) * 2.0;
sum += texture(TEXTURE, UV + vec2(px.x, -px.y)) * 1.0;
sum += texture(TEXTURE, UV + vec2(-px.x, 0.0)) * 2.0;
sum += texture(TEXTURE, UV) * 4.0;
sum += texture(TEXTURE, UV + vec2(px.x, 0.0)) * 2.0;
sum += texture(TEXTURE, UV + vec2(-px.x, px.y)) * 1.0;
sum += texture(TEXTURE, UV + vec2(0.0, px.y)) * 2.0;
sum += texture(TEXTURE, UV + vec2(px.x, px.y)) * 1.0;
sum /= 16.0;
COLOR = vec4(sum.rgb * (1.0 - darken), sum.a);
}

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