Compare commits
23 Commits
0.1.0
..
008650a87e
| Author | SHA1 | Date | |
|---|---|---|---|
| 008650a87e | |||
| b9e3147519 | |||
| cdba320cbc | |||
| ee7b1617c1 | |||
| d749485126 | |||
| 45427b7070 | |||
| d514435dd8 | |||
| 3eb9fa50f3 | |||
| aee900c009 | |||
| 4cf714f516 | |||
| b78736a1f4 | |||
| 212263c858 | |||
| 8c4eeb0861 | |||
| 95def2d527 | |||
| 9a43a9ff8b | |||
| 7238cbf49e | |||
| 44c667f097 | |||
| 3cd347d420 | |||
| 6414b2e7cc | |||
| 56593c58cf | |||
| ce6f3d7075 | |||
| a74182255b | |||
| ebf5046461 |
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://dk6xuwwa0mfpe"
|
uid="uid://dk6xuwwa0mfpe"
|
||||||
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_sand.png"
|
source_file="res://Assets/Arena_sand.png"
|
||||||
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex", "res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://b4gkpa7jypcsm"
|
uid="uid://b4gkpa7jypcsm"
|
||||||
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_wall.png"
|
source_file="res://Assets/Arena_wall.png"
|
||||||
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex", "res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://yercpq3y2o0x"
|
uid="uid://yercpq3y2o0x"
|
||||||
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
|
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
|
||||||
|
path.etc2="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
@@ -15,7 +16,7 @@ generator_parameters={
|
|||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/Arena_wood.png"
|
source_file="res://Assets/Arena_wood.png"
|
||||||
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"]
|
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex", "res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.etc2.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cofv5dbavesis"
|
||||||
|
path="res://.godot/imported/Bear.glb-d982be4b426919de19c028d884069d30.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Bear.glb"
|
||||||
|
dest_files=["res://.godot/imported/Bear.glb-d982be4b426919de19c028d884069d30.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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
|
||||||
@@ -28,7 +28,7 @@ meshes/generate_lods=true
|
|||||||
meshes/create_shadow_meshes=true
|
meshes/create_shadow_meshes=true
|
||||||
meshes/light_baking=1
|
meshes/light_baking=1
|
||||||
meshes/lightmap_texel_size=0.2
|
meshes/lightmap_texel_size=0.2
|
||||||
meshes/force_disable_compression=false
|
meshes/force_disable_compression=true
|
||||||
skins/use_named_skins=true
|
skins/use_named_skins=true
|
||||||
animation/import=true
|
animation/import=true
|
||||||
animation/fps=30
|
animation/fps=30
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[gd_scene format=3 uid="uid://c2dx4khl7voyd"]
|
||||||
|
|
||||||
|
[node name="BearFx" type="Node3D" unique_id=1568304275]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
[gd_scene format=3 uid="uid://by4ifsgxlv0gj"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bm6rmt3xprhjs" path="res://Assets/bear_level_arena.glb" id="1_beararena"]
|
||||||
|
[ext_resource type="Script" path="res://matador_spawn.gd" id="2_spawn"]
|
||||||
|
[ext_resource type="PackedScene" path="res://Bear.tscn" id="3_bear"]
|
||||||
|
|
||||||
|
[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]
|
||||||
|
script = ExtResource("2_spawn")
|
||||||
|
enemy_scene = ExtResource("3_bear")
|
||||||
|
|
||||||
|
[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.
@@ -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
|
||||||
Binary file not shown.
@@ -28,7 +28,7 @@ meshes/generate_lods=true
|
|||||||
meshes/create_shadow_meshes=true
|
meshes/create_shadow_meshes=true
|
||||||
meshes/light_baking=1
|
meshes/light_baking=1
|
||||||
meshes/lightmap_texel_size=0.2
|
meshes/lightmap_texel_size=0.2
|
||||||
meshes/force_disable_compression=false
|
meshes/force_disable_compression=true
|
||||||
skins/use_named_skins=true
|
skins/use_named_skins=true
|
||||||
animation/import=true
|
animation/import=true
|
||||||
animation/fps=30
|
animation/fps=30
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -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.
@@ -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 |
@@ -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 |
@@ -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
|
||||||
Binary file not shown.
@@ -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
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[gd_scene load_steps=5 format=3 uid="uid://bear_scene_v1"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://bear.gd" id="1_beargd"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cofv5dbavesis" path="res://Assets/Bear.glb" id="2_bearglb"]
|
||||||
|
|
||||||
|
[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"]
|
||||||
|
script = ExtResource("1_beargd")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
|
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="."]
|
||||||
|
monitoring = true
|
||||||
|
monitorable = false
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.35, 0)
|
||||||
|
shape = SubResource("CapsuleShape3D_hit")
|
||||||
|
|
||||||
|
[node name="bear_model" parent="." instance=ExtResource("2_bearglb")]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,13 +12,23 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen
|
|||||||
|
|
||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `scene.tscn` | Main scene (arena + player) |
|
| `scene.tscn` | The reusable **shell** loaded for every level: player, camera, HUD, PS1 filter, and an empty `LevelRoot`. Its `game_shell.gd` instances the active level's module into `LevelRoot` at runtime |
|
||||||
|
| `levels/` | The level system: per-level module scenes plus the code that defines, loads and sequences them (see rows below) |
|
||||||
|
| `levels/game_shell.gd` | Shell controller — on load, frees any current level module and instances `Run.active_level().level_scene` into `LevelRoot`. Swapping the whole subtree (never hiding it) is what keeps one level's meshes/colliders/spawners from leaking into the next |
|
||||||
|
| `levels/arena_level.tscn` | The arena **level module**: floor + wall colliders, arena meshes, `MatadorSpawner` (with child `Node3D` spawn points placed by the designer), lighting, audience (`PublikNode`). Instanced into the shell's `LevelRoot` |
|
||||||
|
| `levels/bear_level.tscn` | The Bear's Den **level module**: bear-arena mesh, its own floor collider, lighting, and a `BearSpawner` (`bear_spawn.gd`) that places the bear at center. Instanced into the shell's `LevelRoot` |
|
||||||
| `Player.tscn` | Player scene root (`CharacterBody3D`) |
|
| `Player.tscn` | Player scene root (`CharacterBody3D`) |
|
||||||
| `player.gd` | Movement, charge, turning logic |
|
| `player.gd` | Movement, charge, turning logic |
|
||||||
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
||||||
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
||||||
| `matador.gd` | Matador AI (wander / ragdoll state machine) |
|
| `matador.gd` | Matador AI (wander / ragdoll state machine) |
|
||||||
| `matador_spawn.gd` | Spawns N matadors at random arena positions |
|
| `levels/matador_spawn.gd` | Arena spawner — one matador per designer-placed child `Node3D` spawn point (each enters with a jump-roll toward center); falls back to a programmatic spread if no spawn points are present. Also handles the split-on-death hydra logic |
|
||||||
|
| `levels/bear_spawn.gd` | Bear-level spawner — instantiates `enemy_scene` at the arena center; emits the same `all_defeated` / `matador_killed` signals as `matador_spawn.gd` |
|
||||||
|
| `Bear.tscn` / `bear.gd` | Dark-Souls-style bear boss (`CharacterBody3D`). Routine state machine (wind-up → active → recovery) picking telegraphed moves by range band: Double Swipe / Overhead Smash (close), Leap Slam (mid-far), plus Stalk (2-leg walk-in) / Prowl (4-leg lope) positioning. Melee steps in during the swing (`bear_lunge_speed`) + tracks the bull; the leap is frame-synced to JUMP_SMASH so the paws hit ground on the impact frame (`bear_leap_impact_frame`); slams kick up dust + shake. Combos chain, hyper-armour mid-swing + poise (`bear_stagger_cd`), phase 2 enrage under `bear_enrage_frac` HP. Never damages the bull by colliding — only swings/slams. Joins `&"matador"` so bull abilities already hit it. Tuning under the `Bear` DP section |
|
||||||
|
| `levels/run_state.gd` | Run progression autoload (`Run`) — Slay-the-Spire map + player position across fights |
|
||||||
|
| `levels/level_def.gd` | `LevelDef` resource — a level's name / map colour / `scene_path` (always the shell) / `level_scene` (the level module instanced into the shell) / optional `enemy_scene` (arena wave vs. a boss like the bear) |
|
||||||
|
| `levels/map_node.gd` | `MapNode` — one runtime node on the run map (position, level, links) |
|
||||||
|
| `levels/MapScreen.tscn` / `levels/map_screen.gd` | Between-fights map screen; win → pick a node → next level |
|
||||||
| `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) |
|
| `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) |
|
||||||
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
||||||
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
||||||
@@ -88,10 +98,12 @@ After FBX import, animations appear as `Armature|walk` and `Armature|idle` in An
|
|||||||
bash run_tests.sh
|
bash run_tests.sh
|
||||||
|
|
||||||
# Individual tests:
|
# Individual tests:
|
||||||
gdlint *.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
gdlint *.gd levels/*.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
||||||
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
||||||
godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s
|
godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s
|
||||||
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
|
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
|
||||||
|
godot --headless --script tests/level_switch_test.gd # no arena geometry leaks into the bear level, ~2 s
|
||||||
|
godot --headless --script tests/bear_boss_test.gd # bear boss moveset: routines, leap landing, hyper-armour, ~10 s
|
||||||
|
|
||||||
# Gameplay test with real rendering (inspect frame strip visually):
|
# Gameplay test with real rendering (inspect frame strip visually):
|
||||||
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://bxm7c6ldrdiq5"
|
uid="uid://bxm7c6ldrdiq5"
|
||||||
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"
|
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
|
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
|
||||||
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"]
|
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex", "res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ importer="texture"
|
|||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://3syhacswepyj"
|
uid="uid://3syhacswepyj"
|
||||||
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"
|
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
|
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
|
||||||
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"]
|
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex", "res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -51,7 +51,7 @@ dof_blur_near_transition = 5.0
|
|||||||
transform = Transform3D(-0.56715536, 0, -0.8236109, 0, 1, 0, 0.8236109, 0, -0.56715536, 5.2114463, -0.08998108, 1.8927763)
|
transform = Transform3D(-0.56715536, 0, -0.8236109, 0, 1, 0, 0.8236109, 0, -0.56715536, 5.2114463, -0.08998108, 1.8927763)
|
||||||
script = ExtResource("2_5jr0i")
|
script = ExtResource("2_5jr0i")
|
||||||
|
|
||||||
[node name="Skeleton3D" parent="Matador/Armature" parent_id_path=PackedInt32Array(166852612, 1269629432) index="0" unique_id=978366810]
|
[node name="Skeleton3D" parent="Matador/Armature" parent_id_path=PackedInt32Array(166852612, 1447304122) index="0" unique_id=357848305]
|
||||||
bones/0/position = Vector3(0.056660548, 0.6424767, -0.0167365)
|
bones/0/position = Vector3(0.056660548, 0.6424767, -0.0167365)
|
||||||
bones/0/rotation = Quaternion(0.013179442, -0.2660563, 0.073319815, 0.9610747)
|
bones/0/rotation = Quaternion(0.013179442, -0.2660563, 0.073319815, 0.9610747)
|
||||||
bones/1/rotation = Quaternion(0.06234612, 0.049236443, 0.08183311, 0.99347484)
|
bones/1/rotation = Quaternion(0.06234612, 0.049236443, 0.08183311, 0.99347484)
|
||||||
@@ -81,10 +81,10 @@ bones/23/rotation = Quaternion(0.9993525, -0.0021175682, 0.03502641, -0.00795782
|
|||||||
bones/24/rotation = Quaternion(0.08874816, -0.018719967, -0.04671622, 0.99478185)
|
bones/24/rotation = Quaternion(0.08874816, -0.018719967, -0.04671622, 0.99478185)
|
||||||
bones/25/rotation = Quaternion(0.09634846, -0.0268489, -0.04050248, 0.9941608)
|
bones/25/rotation = Quaternion(0.09634846, -0.0268489, -0.04050248, 0.9941608)
|
||||||
|
|
||||||
[node name="matador" parent="Matador/Armature/Skeleton3D" index="0" unique_id=256196413]
|
[node name="matador" parent="Matador/Armature/Skeleton3D" index="0" unique_id=593562242]
|
||||||
transform = Transform3D(0.8642424, 0, 0.50307554, 0, 1, 0, -0.50307554, 0, 0.8642424, 0, 0, 0)
|
transform = Transform3D(0.8642424, 0, 0.50307554, 0, 1, 0, -0.50307554, 0, 0.8642424, 0, 0, 0)
|
||||||
|
|
||||||
[node name="Muleta" parent="Matador/Armature/Skeleton3D" index="1" unique_id=988244930]
|
[node name="Muleta" parent="Matador/Armature/Skeleton3D" index="1" unique_id=978680467]
|
||||||
cast_shadow = 0
|
cast_shadow = 0
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="." unique_id=1531255459]
|
[node name="Camera3D" type="Camera3D" parent="." unique_id=1531255459]
|
||||||
|
|||||||
+82
-39
@@ -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="Script" uid="uid://46y10alai56q" path="res://main_menu.gd" id="1_menu"]
|
||||||
[ext_resource type="VideoStream" path="res://Assets/intro_bg.ogv" id="2_video"]
|
[ext_resource type="VideoStream" uid="uid://csxxlnq74fej4" path="res://Assets/intro_bg.ogv" id="2_video"]
|
||||||
|
[ext_resource type="Shader" 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"]
|
[sub_resource type="StyleBoxFlat" id="sb_normal"]
|
||||||
content_margin_left = 16.0
|
content_margin_left = 16.0
|
||||||
@@ -30,7 +36,7 @@ corner_radius_top_right = 8
|
|||||||
corner_radius_bottom_right = 8
|
corner_radius_bottom_right = 8
|
||||||
corner_radius_bottom_left = 8
|
corner_radius_bottom_left = 8
|
||||||
|
|
||||||
[node name="MainMenu" type="Control"]
|
[node name="MainMenu" type="Control" unique_id=371457725]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
@@ -39,29 +45,54 @@ grow_horizontal = 2
|
|||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("1_menu")
|
script = ExtResource("1_menu")
|
||||||
|
|
||||||
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="."]
|
[node name="VideoBlur" type="AspectRatioContainer" parent="." unique_id=374854263]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 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")
|
stream = ExtResource("2_video")
|
||||||
expand = true
|
expand = true
|
||||||
|
|
||||||
[node name="MainPanel" type="Control" parent="."]
|
[node name="VideoAspect" type="AspectRatioContainer" parent="." unique_id=374854264]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 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
|
layout_mode = 1
|
||||||
anchor_left = 0.4530
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.453
|
||||||
anchor_top = 0.6513
|
anchor_top = 0.6513
|
||||||
anchor_right = 0.4530
|
anchor_right = 0.453
|
||||||
anchor_bottom = 0.6513
|
anchor_bottom = 0.6513
|
||||||
offset_left = -120.0
|
offset_left = -120.0
|
||||||
offset_top = -55.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_constants/outline_size = 8
|
||||||
theme_override_font_sizes/font_size = 56
|
theme_override_font_sizes/font_size = 56
|
||||||
theme_override_styles/normal = SubResource("sb_normal")
|
theme_override_styles/normal = SubResource("sb_normal")
|
||||||
theme_override_styles/hover = SubResource("sb_hover")
|
|
||||||
theme_override_styles/pressed = SubResource("sb_hover")
|
theme_override_styles/pressed = SubResource("sb_hover")
|
||||||
|
theme_override_styles/hover = SubResource("sb_hover")
|
||||||
theme_override_styles/focus = SubResource("sb_hover")
|
theme_override_styles/focus = SubResource("sb_hover")
|
||||||
text = "PLAY"
|
text = "PLAY"
|
||||||
|
|
||||||
[node name="OptionsButton" type="Button" parent="MainPanel"]
|
[node name="MainPanel" type="Control" parent="." unique_id=1263320918]
|
||||||
layout_mode = 1
|
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_left = 0.11
|
||||||
anchor_top = 0.94
|
anchor_top = 0.94
|
||||||
anchor_right = 0.11
|
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_constants/outline_size = 6
|
||||||
theme_override_font_sizes/font_size = 30
|
theme_override_font_sizes/font_size = 30
|
||||||
theme_override_styles/normal = SubResource("sb_normal")
|
theme_override_styles/normal = SubResource("sb_normal")
|
||||||
theme_override_styles/hover = SubResource("sb_hover")
|
|
||||||
theme_override_styles/pressed = SubResource("sb_hover")
|
theme_override_styles/pressed = SubResource("sb_hover")
|
||||||
|
theme_override_styles/hover = SubResource("sb_hover")
|
||||||
theme_override_styles/focus = SubResource("sb_hover")
|
theme_override_styles/focus = SubResource("sb_hover")
|
||||||
text = "OPTIONS"
|
text = "OPTIONS"
|
||||||
|
|
||||||
[node name="CreditsButton" type="Button" parent="MainPanel"]
|
[node name="CreditsButton" type="Button" parent="MainPanel" unique_id=882339461]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
anchor_left = 0.489
|
anchor_left = 0.489
|
||||||
anchor_top = 0.951
|
anchor_top = 0.951
|
||||||
anchor_right = 0.489
|
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_constants/outline_size = 6
|
||||||
theme_override_font_sizes/font_size = 30
|
theme_override_font_sizes/font_size = 30
|
||||||
theme_override_styles/normal = SubResource("sb_normal")
|
theme_override_styles/normal = SubResource("sb_normal")
|
||||||
theme_override_styles/hover = SubResource("sb_hover")
|
|
||||||
theme_override_styles/pressed = SubResource("sb_hover")
|
theme_override_styles/pressed = SubResource("sb_hover")
|
||||||
|
theme_override_styles/hover = SubResource("sb_hover")
|
||||||
theme_override_styles/focus = SubResource("sb_hover")
|
theme_override_styles/focus = SubResource("sb_hover")
|
||||||
text = "CREDITS"
|
text = "CREDITS"
|
||||||
|
|
||||||
[node name="ExitButton" type="Button" parent="MainPanel"]
|
[node name="ExitButton" type="Button" parent="MainPanel" unique_id=1064586744]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
anchor_left = 0.92
|
anchor_left = 0.92
|
||||||
anchor_top = 0.951
|
anchor_top = 0.951
|
||||||
anchor_right = 0.92
|
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_constants/outline_size = 6
|
||||||
theme_override_font_sizes/font_size = 30
|
theme_override_font_sizes/font_size = 30
|
||||||
theme_override_styles/normal = SubResource("sb_normal")
|
theme_override_styles/normal = SubResource("sb_normal")
|
||||||
theme_override_styles/hover = SubResource("sb_hover")
|
|
||||||
theme_override_styles/pressed = SubResource("sb_hover")
|
theme_override_styles/pressed = SubResource("sb_hover")
|
||||||
|
theme_override_styles/hover = SubResource("sb_hover")
|
||||||
theme_override_styles/focus = SubResource("sb_hover")
|
theme_override_styles/focus = SubResource("sb_hover")
|
||||||
text = "EXIT"
|
text = "EXIT"
|
||||||
|
|
||||||
[node name="OptionsPanel" type="CenterContainer" parent="."]
|
[node name="OptionsPanel" type="CenterContainer" parent="." unique_id=593823446]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
@@ -154,17 +197,17 @@ anchor_bottom = 1.0
|
|||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
|
|
||||||
[node name="Backdrop" type="ColorRect" parent="OptionsPanel"]
|
[node name="Backdrop" type="ColorRect" parent="OptionsPanel" unique_id=1811982549]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
color = Color(0, 0, 0, 0.55)
|
|
||||||
mouse_filter = 2
|
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)
|
custom_minimum_size = Vector2(560, 480)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 16
|
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
|
layout_mode = 2
|
||||||
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
theme_override_constants/outline_size = 8
|
theme_override_constants/outline_size = 8
|
||||||
@@ -172,7 +215,7 @@ theme_override_font_sizes/font_size = 44
|
|||||||
text = "Controls"
|
text = "Controls"
|
||||||
horizontal_alignment = 1
|
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
|
layout_mode = 2
|
||||||
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
theme_override_constants/outline_size = 5
|
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."
|
text = "Click a key, then press a new key. Esc cancels."
|
||||||
horizontal_alignment = 1
|
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)
|
custom_minimum_size = Vector2(0, 340)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
horizontal_scroll_mode = 0
|
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
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
theme_override_constants/separation = 8
|
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)
|
custom_minimum_size = Vector2(0, 48)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_font_sizes/font_size = 24
|
theme_override_font_sizes/font_size = 24
|
||||||
text = "Back"
|
text = "Back"
|
||||||
|
|
||||||
[node name="CreditsPanel" type="CenterContainer" parent="."]
|
[node name="CreditsPanel" type="CenterContainer" parent="." unique_id=175743902]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
@@ -206,17 +249,17 @@ anchor_bottom = 1.0
|
|||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
|
|
||||||
[node name="Backdrop" type="ColorRect" parent="CreditsPanel"]
|
[node name="Backdrop" type="ColorRect" parent="CreditsPanel" unique_id=1950253735]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
color = Color(0, 0, 0, 0.55)
|
|
||||||
mouse_filter = 2
|
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)
|
custom_minimum_size = Vector2(520, 360)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 20
|
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
|
layout_mode = 2
|
||||||
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
theme_override_constants/outline_size = 8
|
theme_override_constants/outline_size = 8
|
||||||
@@ -224,36 +267,36 @@ theme_override_font_sizes/font_size = 44
|
|||||||
text = "Credits"
|
text = "Credits"
|
||||||
horizontal_alignment = 1
|
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
|
layout_mode = 2
|
||||||
size_flags_vertical = 3
|
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_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/line_separation = 6
|
||||||
|
theme_override_constants/outline_size = 5
|
||||||
theme_override_font_sizes/normal_font_size = 26
|
theme_override_font_sizes/normal_font_size = 26
|
||||||
theme_override_font_sizes/bold_font_size = 30
|
theme_override_font_sizes/bold_font_size = 30
|
||||||
bbcode_enabled = true
|
bbcode_enabled = true
|
||||||
fit_content = true
|
|
||||||
scroll_active = false
|
|
||||||
text = "[center][font_size=32][color=#d9a833]Bullosseum[/color][/font_size]
|
text = "[center][font_size=32][color=#d9a833]Bullosseum[/color][/font_size]
|
||||||
|
|
||||||
[b][color=#d9a833]Siim Raud[/color][/b] [url=https://instagram.com/siimpressionist]@siimpressionist[/url]
|
[b][color=#d9a833]Siim Raud[/color][/b] [url=https://instagram.com/siimpressionist]@siimpressionist[/url]
|
||||||
[font_size=20]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]
|
[b][color=#d9a833]Richard Aasa[/color][/b] [url=https://instagram.com/aasarichard]@aasarichard[/url]
|
||||||
[font_size=20]Programmer[/font_size]
|
[font_size=20]Programmer[/font_size]
|
||||||
|
|
||||||
[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]"
|
[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)
|
custom_minimum_size = Vector2(0, 48)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_font_sizes/font_size = 24
|
theme_override_font_sizes/font_size = 24
|
||||||
text = "Back"
|
text = "Back"
|
||||||
|
|
||||||
[node name="FadeRect" type="ColorRect" parent="."]
|
[node name="FadeRect" type="ColorRect" parent="." unique_id=15381366]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
|
|||||||
+27
@@ -35,4 +35,31 @@ script = ExtResource("2_camera_iso")
|
|||||||
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
|
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
|
||||||
|
|
||||||
|
[node name="Skeleton3D" parent="bull/Armature" parent_id_path=PackedInt32Array(1386495166, 547304299) index="0" unique_id=2096877109]
|
||||||
|
bones/0/position = Vector3(0, 0, 0.009963839)
|
||||||
|
bones/0/rotation = Quaternion(-0.733504, -0.0036974847, 0.0013620416, -0.67967373)
|
||||||
|
bones/0/scale = Vector3(1, 0.99999994, 0.99999994)
|
||||||
|
bones/1/position = Vector3(0.0035266252, -0.0043296535, 0.0040827994)
|
||||||
|
bones/1/rotation = Quaternion(0.020680686, 0.45675468, 0.8716418, -0.1766016)
|
||||||
|
bones/2/rotation = Quaternion(0.3289326, -0.03432299, -0.06047997, 0.94178957)
|
||||||
|
bones/4/position = Vector3(0.0045358227, -0.0021376952, -0.004238624)
|
||||||
|
bones/4/rotation = Quaternion(-0.2448651, -0.018126775, 0.96885973, -0.0319902)
|
||||||
|
bones/4/scale = Vector3(1.0000001, 1, 1.0000001)
|
||||||
|
bones/5/rotation = Quaternion(-0.7492961, -0.32333943, -0.24235487, 0.5246629)
|
||||||
|
bones/5/scale = Vector3(1, 1, 1)
|
||||||
|
bones/6/rotation = Quaternion(0.32210493, 0.15572408, 0.14126852, 0.9230611)
|
||||||
|
bones/6/scale = Vector3(0.9999998, 1, 1)
|
||||||
|
bones/17/position = Vector3(-2.3283064e-12, 0.0009373817, 0.007552617)
|
||||||
|
bones/17/rotation = Quaternion(-5.3357535e-08, 0.7071068, 0.7071067, -5.3357535e-08)
|
||||||
|
bones/19/position = Vector3(-0.0035266252, -0.0043296525, 0.0040827994)
|
||||||
|
bones/19/rotation = Quaternion(0.07793758, -0.2513099, 0.94914854, 0.1728764)
|
||||||
|
bones/20/rotation = Quaternion(-0.3791731, -0.08821739, 0.07153107, 0.91832936)
|
||||||
|
bones/22/position = Vector3(-0.0045358227, -0.0021376943, -0.004238624)
|
||||||
|
bones/22/rotation = Quaternion(0.20629132, 0.46411392, 0.84344316, 0.17506008)
|
||||||
|
bones/22/scale = Vector3(1.0000001, 1.0000001, 1.0000001)
|
||||||
|
bones/23/rotation = Quaternion(-0.7287124, 0.31838557, 0.209407, 0.5689971)
|
||||||
|
bones/23/scale = Vector3(1.0000001, 1.0000002, 1.0000002)
|
||||||
|
bones/24/rotation = Quaternion(0.71033937, -0.13944183, -0.049755, 0.6881123)
|
||||||
|
bones/24/scale = Vector3(0.9999999, 1, 1.0000001)
|
||||||
|
|
||||||
[editable path="bull"]
|
[editable path="bull"]
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -0,0 +1,882 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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"])
|
||||||
|
# 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]:
|
||||||
|
_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()
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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_idle)
|
||||||
|
# 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cn7fhjkro7637
|
||||||
@@ -42,6 +42,15 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
|
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
|
||||||
|
|
||||||
|
|
||||||
|
# Pinch-to-zoom hook for the touch UI. `ratio` is the two-finger spread this frame over
|
||||||
|
# last frame: >1 (fingers parting) narrows the FOV to zoom in, <1 widens it to pull back,
|
||||||
|
# mirroring the scroll-wheel zoom_in/zoom_out feel.
|
||||||
|
func adjust_zoom(ratio: float) -> void:
|
||||||
|
if ratio <= 0.0:
|
||||||
|
return
|
||||||
|
_user_fov = clampf(_user_fov / ratio, fov_min, fov_max)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
fov = lerpf(fov, _user_fov, delta * 8.0)
|
fov = lerpf(fov, _user_fov, delta * 8.0)
|
||||||
|
|
||||||
@@ -92,6 +101,8 @@ func _drag_focus(target: Node3D, delta: float) -> Vector3:
|
|||||||
|
|
||||||
|
|
||||||
func trigger_hit(strength: float) -> void:
|
func trigger_hit(strength: float) -> void:
|
||||||
|
if not Settings.screen_shake:
|
||||||
|
return
|
||||||
_shake_amount = strength * 0.5
|
_shake_amount = strength * 0.5
|
||||||
_shake_decay = _shake_amount / 0.4
|
_shake_decay = _shake_amount / 0.4
|
||||||
|
|
||||||
|
|||||||
+29
-2
@@ -18,6 +18,8 @@ const _COMMANDS := {
|
|||||||
"diff": "list params changed from default",
|
"diff": "list params changed from default",
|
||||||
"toggle": "flip a bool param",
|
"toggle": "flip a bool param",
|
||||||
"list": "print params (optional section)",
|
"list": "print params (optional section)",
|
||||||
|
"level": "select level and reload (level <id> — arena / arena_boss / bear)",
|
||||||
|
"restart": "reload the current level scene",
|
||||||
"clear": "clear the log",
|
"clear": "clear the log",
|
||||||
"help": "show this help",
|
"help": "show this help",
|
||||||
}
|
}
|
||||||
@@ -472,6 +474,12 @@ func _run_command(parts: PackedStringArray) -> bool:
|
|||||||
_cmd_diff()
|
_cmd_diff()
|
||||||
"list":
|
"list":
|
||||||
_cmd_list(parts)
|
_cmd_list(parts)
|
||||||
|
"level":
|
||||||
|
_cmd_level(parts)
|
||||||
|
"restart":
|
||||||
|
_log_ok("Restarting…")
|
||||||
|
get_tree().paused = false
|
||||||
|
get_tree().reload_current_scene()
|
||||||
"clear":
|
"clear":
|
||||||
_log.clear()
|
_log.clear()
|
||||||
"help":
|
"help":
|
||||||
@@ -581,9 +589,9 @@ func _set_param(key: String, meta: Dictionary, raw: String) -> void:
|
|||||||
var lo := meta["min"] as float
|
var lo := meta["min"] as float
|
||||||
var hi := meta["max"] as float
|
var hi := meta["max"] as float
|
||||||
if not raw.is_valid_float() or not is_finite(raw.to_float()):
|
if not raw.is_valid_float() or not is_finite(raw.to_float()):
|
||||||
_log_warn("%s wants a number %s … %s" % [key, _fmt_num(lo), _fmt_num(hi)])
|
_log_warn("%s wants a number" % key)
|
||||||
return
|
return
|
||||||
var v := snappedf(clampf(raw.to_float(), lo, hi), meta["step"] as float)
|
var v := raw.to_float()
|
||||||
DP.set_value(key, v)
|
DP.set_value(key, v)
|
||||||
_log_ok("[b]%s[/b] ← [color=#ffe080]%s[/color]" % [key, _fmt_num(v)])
|
_log_ok("[b]%s[/b] ← [color=#ffe080]%s[/color]" % [key, _fmt_num(v)])
|
||||||
elif meta["type"] == TYPE_BOOL:
|
elif meta["type"] == TYPE_BOOL:
|
||||||
@@ -600,6 +608,25 @@ func _set_param(key: String, meta: Dictionary, raw: String) -> void:
|
|||||||
_log_warn("Cannot set param of type %d" % meta["type"])
|
_log_warn("Cannot set param of type %d" % meta["type"])
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_level(parts: PackedStringArray) -> void:
|
||||||
|
if parts.size() == 1:
|
||||||
|
var cur := Run.active_level()
|
||||||
|
var cur_name := (cur.id as String) if cur != null else "none"
|
||||||
|
_log_ok("Current level: [b]%s[/b]" % cur_name)
|
||||||
|
var ids: Array[StringName] = Run.level_ids()
|
||||||
|
_log_raw("Available: " + " ".join(ids.map(func(id: StringName) -> String: return str(id))))
|
||||||
|
return
|
||||||
|
var id := StringName(parts[1].to_lower())
|
||||||
|
var level := Run.get_level(id)
|
||||||
|
if level == null:
|
||||||
|
_log_warn("Unknown level '%s'. Try: %s" % [parts[1], " ".join(Run.level_ids().map(func(i: StringName) -> String: return str(i)))])
|
||||||
|
return
|
||||||
|
Run.debug_level_override = level
|
||||||
|
_log_ok("Level set to [b]%s[/b] — reloading…" % str(id))
|
||||||
|
get_tree().paused = false
|
||||||
|
get_tree().change_scene_to_file(level.scene_path)
|
||||||
|
|
||||||
|
|
||||||
func _cmd_list(parts: PackedStringArray) -> void:
|
func _cmd_list(parts: PackedStringArray) -> void:
|
||||||
var filter := parts[1].to_lower() if parts.size() > 1 else ""
|
var filter := parts[1].to_lower() if parts.size() > 1 else ""
|
||||||
var all := DP.get_all()
|
var all := DP.get_all()
|
||||||
|
|||||||
+63
-1
@@ -15,8 +15,8 @@ const REBINDABLE_ACTIONS: PackedStringArray = [
|
|||||||
&"move_left",
|
&"move_left",
|
||||||
&"move_right",
|
&"move_right",
|
||||||
&"ability_kick",
|
&"ability_kick",
|
||||||
&"ability_slam",
|
|
||||||
&"ability_dash",
|
&"ability_dash",
|
||||||
|
&"ability_slam",
|
||||||
&"ability_roll",
|
&"ability_roll",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -25,6 +25,68 @@ func _ready() -> void:
|
|||||||
load_saved()
|
load_saved()
|
||||||
|
|
||||||
|
|
||||||
|
## 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.
|
## Returns the first keyboard event bound to an action, or null.
|
||||||
func get_key_event(action: StringName) -> InputEventKey:
|
func get_key_event(action: StringName) -> InputEventKey:
|
||||||
for event: InputEvent in InputMap.action_get_events(action):
|
for event: InputEvent in InputMap.action_get_events(action):
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dys23pu1kw5fl
|
||||||
+173
-28
@@ -17,14 +17,13 @@ var _values: Dictionary = {}
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_register_all()
|
_register_all()
|
||||||
load_saved()
|
|
||||||
|
|
||||||
|
|
||||||
func _register_all() -> void:
|
func _register_all() -> void:
|
||||||
# ── Movement ──────────────────────────────────────────────────────────────
|
# ── Movement ──────────────────────────────────────────────────────────────
|
||||||
# WASD/arrows drive the bull: move_accel ramps flat velocity toward max_speed in
|
# 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.
|
# 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_accel", 120.0, 5.0, 600.0)
|
||||||
_reg_f("Movement", "move_friction", 90.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)
|
_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", "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_bounce_restitution", 0.6, 0.0, 1.5, 0.05)
|
||||||
_reg_f("Movement", "wall_min_bounce_speed", 6.0, 0.0, 30.0)
|
_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 ─────────────────────────────────────────────────
|
# ── Dust speed thresholds ─────────────────────────────────────────────────
|
||||||
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
|
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
|
||||||
_reg_f("Dust", "dust_charge_spd", 7.0, 1.0, 20.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_explosiveness", 0.35, 0.0, 1.0)
|
||||||
_reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0)
|
_reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0)
|
||||||
_reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0)
|
_reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0)
|
||||||
_reg_f("Dust", "walk_scale_min", 0.35, 0.05, 3.0)
|
_reg_f("Dust", "walk_scale_min", 0.20, 0.05, 3.0)
|
||||||
_reg_f("Dust", "walk_scale_max", 0.65, 0.05, 3.0)
|
_reg_f("Dust", "walk_scale_max", 0.40, 0.05, 3.0)
|
||||||
_reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0)
|
_reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0)
|
||||||
_reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0)
|
_reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0)
|
||||||
_reg_f("Dust", "charge_scale_min", 1.0, 0.05, 6.0)
|
_reg_f("Dust", "charge_scale_min", 0.35, 0.05, 6.0)
|
||||||
_reg_f("Dust", "charge_scale_max", 2.2, 0.05, 6.0)
|
_reg_f("Dust", "charge_scale_max", 0.70, 0.05, 6.0)
|
||||||
_reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0)
|
_reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0)
|
||||||
_reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0)
|
_reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0)
|
||||||
# ── Tail ──────────────────────────────────────────────────────────────────
|
# ── Tail ──────────────────────────────────────────────────────────────────
|
||||||
@@ -94,14 +98,18 @@ func _register_all() -> void:
|
|||||||
_reg_f("Camera", "cam_drag_speed", 3.5, 0.5, 20.0)
|
_reg_f("Camera", "cam_drag_speed", 3.5, 0.5, 20.0)
|
||||||
_reg_f("Camera", "cam_follow_lerp", 10.0, 1.0, 40.0)
|
_reg_f("Camera", "cam_follow_lerp", 10.0, 1.0, 40.0)
|
||||||
# ── Matador ───────────────────────────────────────────────────────────────
|
# ── Matador ───────────────────────────────────────────────────────────────
|
||||||
_reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0)
|
# Wave system: matadors enter the arena in escalating waves (1 → 2 → … → N where N
|
||||||
|
# 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_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_walk_speed", 5.5, 0.5, 10.0)
|
||||||
_reg_f("Matador", "mat_wander_radius", 22.0, 5.0, 50.0)
|
_reg_f("Matador", "mat_wander_radius", 22.0, 5.0, 50.0)
|
||||||
# Radius of the inner fighting floor: bull + matadors spawn on this ring (so
|
# Radius of the inner fighting floor: bull + matadors spawn on this ring (so
|
||||||
# they stay off the benches). Tune in the console to hug the bench edge.
|
# 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", "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_min", 1.2, 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_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_hit_threshold", 4.0, 1.0, 30.0)
|
||||||
_reg_f("Matador", "mat_ragdoll_impulse", 6.0, 0.5, 20.0)
|
_reg_f("Matador", "mat_ragdoll_impulse", 6.0, 0.5, 20.0)
|
||||||
# Ragdoll joint limits — lower swing/twist = stiffer corpse that keeps its
|
# Ragdoll joint limits — lower swing/twist = stiffer corpse that keeps its
|
||||||
@@ -153,12 +161,99 @@ func _register_all() -> void:
|
|||||||
# bullfighter, caution for a slippery one. mat_w_commit is a stickiness bonus for
|
# 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.
|
# 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_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_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_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.
|
# Small so it smooths dithering without locking the matador into idling.
|
||||||
_reg_f("Matador", "mat_w_commit", 0.15, 0.0, 2.0, 0.05)
|
_reg_f("Matador", "mat_w_commit", 0.15, 0.0, 2.0, 0.05)
|
||||||
|
# ── Bear (boss) ─────────────────────────────────────────────────────────────
|
||||||
|
# A Dark-Souls-style boss with a learnable moveset. It soaks bear_max_hp clean hits
|
||||||
|
# (ram or ability), staggering on each, and only topples when worn down. From NEUTRAL
|
||||||
|
# it weighs its options by the range band to the bull and commits to one telegraphed
|
||||||
|
# routine (wind-up → active → recovery); each routine plays to the end (the recovery
|
||||||
|
# is the player's punish window). Below bear_enrage_frac HP it enters phase 2: faster
|
||||||
|
# tells and recovery (× bear_enrage_scale) and longer combo chains.
|
||||||
|
_reg_f("Bear", "bear_max_hp", 10.0, 1.0, 40.0, 1.0)
|
||||||
|
# HP fraction at which phase 2 ("Fury") triggers, and how much it multiplies every
|
||||||
|
# wind-up / recovery timer (lower = snappier, more dangerous).
|
||||||
|
_reg_f("Bear", "bear_enrage_frac", 0.4, 0.0, 1.0, 0.05)
|
||||||
|
_reg_f("Bear", "bear_enrage_scale", 0.7, 0.3, 1.0, 0.05)
|
||||||
|
# Range bands (m) that pick the moveset: ≤close it strikes (swipe / smash), ≤mid it
|
||||||
|
# walks in to close the gap (stalk) or leaps, beyond it lopes in (prowl) or leaps.
|
||||||
|
# close is deliberately generous — the bear commits to melee "from a bit further" and
|
||||||
|
# steps in during the swing (see bear_lunge_speed) rather than needing to be on top.
|
||||||
|
_reg_f("Bear", "bear_close_range", 5.0, 1.0, 12.0, 0.1)
|
||||||
|
_reg_f("Bear", "bear_mid_range", 9.0, 3.0, 20.0, 0.5)
|
||||||
|
# Beat spent in NEUTRAL sizing up the bull between routines — kept short so the bear
|
||||||
|
# flows straight from one action into the next instead of pausing (raise for a more
|
||||||
|
# hesitant, readable boss; near-0 for relentless pressure). Shrinks again in phase 2.
|
||||||
|
_reg_f("Bear", "bear_think_time", 0.06, 0.0, 1.0, 0.01)
|
||||||
|
# Locomotion: the all-fours prowl (close a big gap), the menacing 2-leg walk-in
|
||||||
|
# (stalk), the facing rate while positioning/winding up, and the softer tracking rate
|
||||||
|
# a swing keeps on the bull mid-attack so it's harder to just walk out of.
|
||||||
|
_reg_f("Bear", "bear_run_speed", 11.0, 1.0, 20.0, 0.1)
|
||||||
|
_reg_f("Bear", "bear_stalk_speed", 5.5, 0.5, 12.0, 0.1)
|
||||||
|
_reg_f("Bear", "bear_turn_rate", 7.0, 1.0, 20.0, 0.5)
|
||||||
|
_reg_f("Bear", "bear_track_active", 3.5, 0.0, 12.0, 0.1)
|
||||||
|
# Forward step-in during a swing so a strike lands even when the bull is a touch out of
|
||||||
|
# reach (also carries the tracking); 0 = a purely stationary swing.
|
||||||
|
_reg_f("Bear", "bear_lunge_speed", 7.0, 0.0, 16.0, 0.5)
|
||||||
|
# Playback multiplier for the swipe/smash clips — >1 makes the punches snappier.
|
||||||
|
_reg_f("Bear", "bear_attack_anim_speed", 1.35, 0.5, 3.0, 0.05)
|
||||||
|
# Half-arc (deg) the bear must be facing the bull within for a directional swing to
|
||||||
|
# connect; slams use their own radius instead of an arc.
|
||||||
|
_reg_f("Bear", "bear_hit_arc", 70.0, 5.0, 180.0, 5.0)
|
||||||
|
# Double Swipe (ONE_TWO_HIT) — bread-and-butter close combo, two quick hits.
|
||||||
|
_reg_f("Bear", "bear_swipe_windup", 0.3, 0.05, 2.0, 0.05)
|
||||||
|
_reg_f("Bear", "bear_swipe_recover", 0.35, 0.05, 3.0, 0.05)
|
||||||
|
_reg_f("Bear", "bear_swipe_reach", 4.2, 1.0, 8.0, 0.1)
|
||||||
|
# Overhead Smash (SMASH_HIT) — slower tell, biggest punish window, AoE on impact.
|
||||||
|
_reg_f("Bear", "bear_smash_windup", 0.6, 0.1, 3.0, 0.05)
|
||||||
|
_reg_f("Bear", "bear_smash_recover", 0.75, 0.1, 4.0, 0.05)
|
||||||
|
_reg_f("Bear", "bear_smash_radius", 4.2, 1.0, 8.0, 0.1)
|
||||||
|
# Leap Slam (JUMP_SMASH, 47 frames @30fps) — timed to the clip by FRAME: it plays the
|
||||||
|
# crouch grounded, springs at the takeoff frame, and the arc is solved so the paws hit
|
||||||
|
# ground exactly on the impact frame (25), leaving the tail frames to settle on the
|
||||||
|
# ground — no finishing the anim in mid-air. The arc uses its own gravity so peak height
|
||||||
|
# and airtime are independent: bear_leap_height is the peak reached in that fixed airtime.
|
||||||
|
_reg_f("Bear", "bear_leap_takeoff_frame", 6.0, 0.0, 46.0, 1.0)
|
||||||
|
_reg_f("Bear", "bear_leap_impact_frame", 25.0, 1.0, 47.0, 1.0)
|
||||||
|
_reg_f("Bear", "bear_leap_height", 2.6, 0.5, 8.0, 0.1)
|
||||||
|
_reg_f("Bear", "bear_leap_recover", 0.35, 0.0, 4.0, 0.05)
|
||||||
|
_reg_f("Bear", "bear_leap_speed", 14.0, 4.0, 30.0, 0.5)
|
||||||
|
_reg_f("Bear", "bear_slam_radius", 4.0, 1.0, 8.0, 0.1)
|
||||||
|
# Stalk (WALK_ON2LEGS) — how long it will keep walking the bull down before re-deciding.
|
||||||
|
_reg_f("Bear", "bear_stalk_time", 2.5, 0.0, 6.0, 0.1)
|
||||||
|
# Odds a close combo chains a follow-up smash after the swipe (phase 2 always chains).
|
||||||
|
_reg_f("Bear", "bear_combo_chance", 0.7, 0.0, 1.0, 0.05)
|
||||||
|
# Minimum bull speed (ram) that counts as a hit on the bear — a gentle nudge is
|
||||||
|
# shrugged off, only a real charge or an ability wounds it. (The bear itself never
|
||||||
|
# damages the bull by colliding — only its swings and slams hurt.)
|
||||||
|
_reg_f("Bear", "bear_hit_threshold", 6.0, 1.0, 30.0, 0.5)
|
||||||
|
# How long (s) the bear is rocked back and can't act after taking a hit.
|
||||||
|
_reg_f("Bear", "bear_stagger_time", 0.5, 0.0, 2.0, 0.05)
|
||||||
|
# Poise: after a stagger the bear shrugs off staggers for this long (hits still wound
|
||||||
|
# it), so it can't be perma-stunlocked — it gets a window to commit an attack that then
|
||||||
|
# rides its hyper-armour. 0 = no poise (stun-locks under sustained fire).
|
||||||
|
_reg_f("Bear", "bear_stagger_cd", 1.0, 0.0, 4.0, 0.05)
|
||||||
# ── Sword ─────────────────────────────────────────────────────────────────
|
# ── Sword ─────────────────────────────────────────────────────────────────
|
||||||
# Local seating of the sword in the right hand (drawn / fighting).
|
# Local seating of the sword in the right hand (drawn / fighting).
|
||||||
# Defaults are identity — the bone is oriented in Blender to seat the sword
|
# Defaults are identity — the bone is oriented in Blender to seat the sword
|
||||||
@@ -178,18 +273,28 @@ func _register_all() -> void:
|
|||||||
_reg_f("Sword", "sword_rest_rot_x", -120.0, -180.0, 180.0, 1.0)
|
_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_y", 0.0, -180.0, 180.0, 1.0)
|
||||||
_reg_f("Sword", "sword_rest_rot_z", 0.0, -180.0, 180.0, 1.0)
|
_reg_f("Sword", "sword_rest_rot_z", 0.0, -180.0, 180.0, 1.0)
|
||||||
# ── Sword throw (matador ranged attack) ──────────────────────────────────
|
# ── Spear throw (matador ranged attack) ──────────────────────────────────
|
||||||
# Delay before a fresh sword appears in the holster after one is thrown.
|
# 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_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_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_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_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_release", 0.55, 0.1, 0.95, 0.01)
|
||||||
_reg_f("Sword", "mat_throw_speed", 24.0, 5.0, 60.0)
|
# Horizontal speed of the thrown spear; the vertical launch is solved from this
|
||||||
_reg_f("Sword", "mat_throw_spin", 14.0, 0.0, 40.0)
|
# and the gravity below so the arc lands on the bull. Lower = a higher, slower lob.
|
||||||
# Gravity scale on the thrown blade (low = flies straight to the aim point) and
|
_reg_f("Sword", "mat_throw_speed", 11.0, 5.0, 60.0)
|
||||||
# the vertical aim offset from the bull origin down onto its low collision body.
|
# Gravity scale on the spear (higher = a more pronounced arc) and the vertical aim
|
||||||
_reg_f("Sword", "mat_throw_gravity", 0.25, 0.0, 1.0, 0.05)
|
# 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)
|
_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.
|
# 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)
|
_reg_f("Sword", "mat_throw_spread", 11.0, 0.0, 30.0, 0.5)
|
||||||
@@ -198,7 +303,7 @@ func _register_all() -> void:
|
|||||||
_reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0)
|
_reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0)
|
||||||
# ── Abilities ─────────────────────────────────────────────────────────────
|
# ── Abilities ─────────────────────────────────────────────────────────────
|
||||||
_reg_f("Abilities", "kick_cooldown", 1.5, 0.2, 10.0, 0.1)
|
_reg_f("Abilities", "kick_cooldown", 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_range", 6.0, 0.5, 8.0, 0.1)
|
||||||
_reg_f("Abilities", "kick_strength", 12.0, 1.0, 30.0)
|
_reg_f("Abilities", "kick_strength", 12.0, 1.0, 30.0)
|
||||||
_reg_f("Abilities", "slam_cooldown", 4.0, 0.5, 15.0, 0.1)
|
_reg_f("Abilities", "slam_cooldown", 4.0, 0.5, 15.0, 0.1)
|
||||||
_reg_f("Abilities", "slam_jump_velocity", 7.0, 2.0, 20.0)
|
_reg_f("Abilities", "slam_jump_velocity", 7.0, 2.0, 20.0)
|
||||||
@@ -209,19 +314,24 @@ func _register_all() -> void:
|
|||||||
_reg_f("Abilities", "slam_launch_up", 7.0, 0.0, 15.0, 0.5)
|
_reg_f("Abilities", "slam_launch_up", 7.0, 0.0, 15.0, 0.5)
|
||||||
_reg_f("Abilities", "slam_range", 4.0, 1.0, 10.0, 0.1)
|
_reg_f("Abilities", "slam_range", 4.0, 1.0, 10.0, 0.1)
|
||||||
_reg_f("Abilities", "slam_strength", 14.0, 1.0, 30.0)
|
_reg_f("Abilities", "slam_strength", 14.0, 1.0, 30.0)
|
||||||
_reg_f("Abilities", "dash_cooldown", 2.0, 0.5, 10.0, 0.1)
|
_reg_f("Abilities", "dash_cooldown", 0.5, 0.1, 10.0, 0.1)
|
||||||
_reg_f("Abilities", "dash_speed", 66.0, 5.0, 200.0)
|
_reg_f("Abilities", "dash_speed", 66.0, 5.0, 200.0)
|
||||||
# ── 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
|
# 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
|
# roll_base_speed up to roll_max_speed over roll_rampup_time. It HOMES on the
|
||||||
# (momentum steering via roll_turn). On slamming into a matador the ball POPS:
|
# closest matador; on slamming one the ball POPS it (launched skyward via
|
||||||
# the target is launched skyward (roll_knockup) and the roll ends.
|
# 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", 3.0, 0.5, 15.0, 0.1)
|
_reg_f("Roll", "roll_cooldown", 3.0, 0.5, 15.0, 0.1)
|
||||||
_reg_f("Roll", "roll_duration", 2.5, 0.5, 6.0, 0.1) # max roll time
|
_reg_f("Roll", "roll_duration", 2.5, 0.5, 6.0, 0.1) # max roll time
|
||||||
|
_reg_f("Roll", "roll_chain_count", 2.0, 0.0, 8.0, 1.0) # extra matadors chained after the first
|
||||||
_reg_f("Roll", "roll_base_speed", 28.0, 5.0, 120.0) # speed at the start of the roll
|
_reg_f("Roll", "roll_base_speed", 28.0, 5.0, 120.0) # speed at the start of the roll
|
||||||
_reg_f("Roll", "roll_max_speed", 95.0, 10.0, 200.0) # top speed once fully ramped
|
_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_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_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_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
|
_reg_f("Roll", "roll_hit_strength", 22.0, 1.0, 40.0) # horizontal launch on the pop
|
||||||
@@ -250,6 +360,12 @@ func _register_all() -> void:
|
|||||||
_reg_b("Debug", "show_wireframe", false)
|
_reg_b("Debug", "show_wireframe", false)
|
||||||
_reg_b("Debug", "show_grid", false)
|
_reg_b("Debug", "show_grid", false)
|
||||||
_reg_b("Debug", "show_axes", 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,
|
func _reg_f(section: String, key: String, default: float,
|
||||||
@@ -300,11 +416,40 @@ func get_all() -> Dictionary:
|
|||||||
|
|
||||||
|
|
||||||
func save() -> void:
|
func save() -> void:
|
||||||
var cfg := ConfigFile.new()
|
var path := "res://debug_params.gd"
|
||||||
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
push_error("DP.save: cannot open " + path)
|
||||||
|
return
|
||||||
|
var src := file.get_as_text()
|
||||||
|
file.close()
|
||||||
|
|
||||||
for key: String in _params:
|
for key: String in _params:
|
||||||
var p: Dictionary = _params[key]
|
var p: Dictionary = _params[key]
|
||||||
cfg.set_value(p["section"], key, p["value"])
|
if p["value"] == p["default"]:
|
||||||
cfg.save(SAVE_PATH)
|
continue
|
||||||
|
var re := RegEx.new()
|
||||||
|
if p["type"] == TYPE_FLOAT:
|
||||||
|
re.compile('(_reg_f\\("[^"]+",\\s*"%s",\\s*)([-\\d.e]+)' % key)
|
||||||
|
src = re.sub(src, "$1" + _fmt_float(p["value"] as float))
|
||||||
|
else:
|
||||||
|
re.compile('(_reg_b\\("[^"]+",\\s*"%s",\\s*)(true|false)' % key)
|
||||||
|
src = re.sub(src, "$1" + ("true" if bool(p["value"]) else "false"))
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
func _fmt_float(v: float) -> String:
|
||||||
|
var s := "%.6g" % v
|
||||||
|
if not ("." in s or "e" in s):
|
||||||
|
s += ".0"
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
func load_saved() -> void:
|
func load_saved() -> void:
|
||||||
|
|||||||
+44
-3
@@ -1,26 +1,67 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Run from the project root regardless of where the script is invoked.
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
SERVER_USER="richard"
|
SERVER_USER="richard"
|
||||||
SERVER_HOST="192.168.1.168"
|
SERVER_HOST="192.168.1.168"
|
||||||
SERVER_PATH="/opt/bulloseum/data"
|
SERVER_PATH="/opt/bulloseum/data"
|
||||||
|
|
||||||
BUILD_DIR="build/web"
|
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"
|
mkdir -p "$BUILD_DIR"
|
||||||
godot --headless --export-release "Web" "$BUILD_DIR/index.html"
|
godot --headless --export-release "Web" "$BUILD_DIR/index.html"
|
||||||
|
|
||||||
gzip -kf "$BUILD_DIR/index.wasm"
|
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" \
|
scp "$BUILD_DIR/index.html" \
|
||||||
"$BUILD_DIR/index.js" \
|
"$BUILD_DIR/index.js" \
|
||||||
"$BUILD_DIR/index.wasm.gz" \
|
"$BUILD_DIR/index.${STAMP}.wasm.gz" \
|
||||||
"$BUILD_DIR/index.pck" \
|
"$BUILD_DIR/index.${STAMP}.pck" \
|
||||||
"$BUILD_DIR/index.audio.worklet.js" \
|
"$BUILD_DIR/index.audio.worklet.js" \
|
||||||
"$BUILD_DIR/index.audio.position.worklet.js" \
|
"$BUILD_DIR/index.audio.position.worklet.js" \
|
||||||
"$BUILD_DIR/index.icon.png" \
|
"$BUILD_DIR/index.icon.png" \
|
||||||
"$BUILD_DIR/index.apple-touch-icon.png" \
|
"$BUILD_DIR/index.apple-touch-icon.png" \
|
||||||
"$BUILD_DIR/index.png" \
|
"$BUILD_DIR/index.png" \
|
||||||
|
"$BUILD_DIR/version.txt" \
|
||||||
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
|
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
|
||||||
|
|
||||||
echo "deployed to $SERVER_HOST:$SERVER_PATH"
|
echo "deployed ${VERSION} (${STAMP}) to $SERVER_HOST:$SERVER_PATH"
|
||||||
|
|||||||
+88
-2
@@ -1,18 +1,30 @@
|
|||||||
|
[runnable_presets]
|
||||||
|
|
||||||
|
Web="Web"
|
||||||
|
"Windows Desktop"="Windows Desktop"
|
||||||
|
|
||||||
[preset.0]
|
[preset.0]
|
||||||
|
|
||||||
name="Web"
|
name="Web"
|
||||||
platform="Web"
|
platform="Web"
|
||||||
runnable=true
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/web/index.html"
|
export_path="build/web/index.html"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
encrypt_pck=false
|
encrypt_pck=false
|
||||||
encrypt_directory=false
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
[preset.0.options]
|
[preset.0.options]
|
||||||
|
|
||||||
@@ -21,7 +33,7 @@ custom_template/release=""
|
|||||||
variant/extensions_support=false
|
variant/extensions_support=false
|
||||||
variant/thread_support=true
|
variant/thread_support=true
|
||||||
vram_texture_compression/for_desktop=true
|
vram_texture_compression/for_desktop=true
|
||||||
vram_texture_compression/for_mobile=false
|
vram_texture_compression/for_mobile=true
|
||||||
html/export_icon=true
|
html/export_icon=true
|
||||||
html/custom_html_shell=""
|
html/custom_html_shell=""
|
||||||
html/head_include=""
|
html/head_include=""
|
||||||
@@ -29,6 +41,7 @@ html/canvas_resize_policy=2
|
|||||||
html/focus_canvas_on_start=true
|
html/focus_canvas_on_start=true
|
||||||
html/experimental_virtual_keyboard=false
|
html/experimental_virtual_keyboard=false
|
||||||
progressive_web_app/enabled=false
|
progressive_web_app/enabled=false
|
||||||
|
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||||
progressive_web_app/offline_page=""
|
progressive_web_app/offline_page=""
|
||||||
progressive_web_app/display=1
|
progressive_web_app/display=1
|
||||||
progressive_web_app/orientation=0
|
progressive_web_app/orientation=0
|
||||||
@@ -36,3 +49,76 @@ progressive_web_app/icon_144x144=""
|
|||||||
progressive_web_app/icon_180x180=""
|
progressive_web_app/icon_180x180=""
|
||||||
progressive_web_app/icon_512x512=""
|
progressive_web_app/icon_512x512=""
|
||||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||||
|
threads/emscripten_pool_size=8
|
||||||
|
threads/godot_pool_size=4
|
||||||
|
|
||||||
|
[preset.1]
|
||||||
|
|
||||||
|
name="Windows Desktop"
|
||||||
|
platform="Windows Desktop"
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="../Export/Bullosseum_020.exe"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.1.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
debug/export_console_wrapper=1
|
||||||
|
binary_format/embed_pck=false
|
||||||
|
texture_format/s3tc_bptc=true
|
||||||
|
texture_format/etc2_astc=false
|
||||||
|
shader_baker/enabled=false
|
||||||
|
binary_format/architecture="x86_64"
|
||||||
|
codesign/enable=false
|
||||||
|
codesign/timestamp=true
|
||||||
|
codesign/timestamp_server_url=""
|
||||||
|
codesign/digest_algorithm=1
|
||||||
|
codesign/description=""
|
||||||
|
codesign/custom_options=PackedStringArray()
|
||||||
|
application/modify_resources=true
|
||||||
|
application/icon=""
|
||||||
|
application/console_wrapper_icon=""
|
||||||
|
application/icon_interpolation=4
|
||||||
|
application/file_version=""
|
||||||
|
application/product_version=""
|
||||||
|
application/company_name=""
|
||||||
|
application/product_name=""
|
||||||
|
application/file_description=""
|
||||||
|
application/copyright=""
|
||||||
|
application/trademarks=""
|
||||||
|
application/export_angle=0
|
||||||
|
application/export_d3d12=0
|
||||||
|
application/d3d12_agility_sdk_multiarch=true
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
|
ssh_remote_deploy/port="22"
|
||||||
|
ssh_remote_deploy/extra_args_ssh=""
|
||||||
|
ssh_remote_deploy/extra_args_scp=""
|
||||||
|
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||||
|
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||||
|
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||||
|
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||||
|
Start-ScheduledTask -TaskName godot_remote_debug
|
||||||
|
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||||
|
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||||
|
Remove-Item -Recurse -Force '{temp_dir}'"
|
||||||
|
|||||||
@@ -5,25 +5,25 @@ const _CREAM := Color(0.93, 0.88, 0.72)
|
|||||||
const _MUTED := Color(0.66, 0.60, 0.46)
|
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.
|
# 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] = [
|
const _ABILITY_ACCENT: Array[Color] = [
|
||||||
Color(1.00, 0.62, 0.16), # Kick — ember orange
|
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.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
|
Color(1.00, 0.82, 0.32), # Roll — gold
|
||||||
]
|
]
|
||||||
|
|
||||||
const _ABILITY_ICONS: Array[String] = [
|
const _ABILITY_ICONS: Array[String] = [
|
||||||
"res://HUD/KICK.png",
|
"res://HUD/KICK.png",
|
||||||
"res://HUD/SLAM.png",
|
|
||||||
"res://HUD/DASH.png",
|
"res://HUD/DASH.png",
|
||||||
|
"res://HUD/SLAM.png",
|
||||||
"res://HUD/ROLL.png",
|
"res://HUD/ROLL.png",
|
||||||
]
|
]
|
||||||
const _ABILITY_ACTIONS: Array[StringName] = [
|
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.
|
# 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_visible: bool = false
|
||||||
var _controls_panel: PanelContainer
|
var _controls_panel: PanelContainer
|
||||||
@@ -34,18 +34,45 @@ var _player: Node = null
|
|||||||
var _died_connected: bool = false
|
var _died_connected: bool = false
|
||||||
var _cd_overlays: Array[ColorRect] = []
|
var _cd_overlays: Array[ColorRect] = []
|
||||||
var _cd_labels: Array[Label] = []
|
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_bar: HBoxContainer
|
||||||
var _ability_panels: Array[PanelContainer] = []
|
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
|
# Win/lose screen. _game_over latches so the result is shown once; _result_win
|
||||||
# records which way it went (read by the gameplay test).
|
# records which way it went (read by the gameplay test).
|
||||||
const _WIN_VIDEO := "res://videos/bull_win.ogv" # the bull triumphs
|
const _WIN_VIDEO := "res://videos/bull_win.ogv" # the bull triumphs
|
||||||
const _LOSS_VIDEO := "res://videos/matador_win.ogv" # the matador 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 _game_over: bool = false
|
||||||
var _result_win: bool = false
|
var _result_win: bool = false
|
||||||
var _over_root: Control
|
var _over_root: Control
|
||||||
var _over_video: VideoStreamPlayer
|
var _over_video: VideoStreamPlayer
|
||||||
|
var _over_label: Label
|
||||||
|
var _over_continue: Button
|
||||||
|
var _over_again: Button
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -53,12 +80,89 @@ func _ready() -> void:
|
|||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
_build_controls_panel()
|
_build_controls_panel()
|
||||||
_build_tab_hint()
|
_build_tab_hint()
|
||||||
|
_build_bottom_hud()
|
||||||
|
_build_bull_hud_bg()
|
||||||
_build_ability_bar()
|
_build_ability_bar()
|
||||||
|
_build_health()
|
||||||
_build_version_label()
|
_build_version_label()
|
||||||
|
_build_damage_vignette()
|
||||||
_build_game_over()
|
_build_game_over()
|
||||||
|
_build_touch_controls()
|
||||||
|
_update_control_hints()
|
||||||
_find_spawner.call_deferred()
|
_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.
|
# Unobtrusive early-build tag in the bottom-right corner.
|
||||||
func _build_version_label() -> void:
|
func _build_version_label() -> void:
|
||||||
var lbl := Label.new()
|
var lbl := Label.new()
|
||||||
@@ -75,6 +179,7 @@ func _build_version_label() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
|
_update_control_hints()
|
||||||
if _player == null:
|
if _player == null:
|
||||||
var players := get_tree().get_nodes_in_group(&"player")
|
var players := get_tree().get_nodes_in_group(&"player")
|
||||||
if not players.is_empty():
|
if not players.is_empty():
|
||||||
@@ -83,6 +188,9 @@ func _process(_delta: float) -> void:
|
|||||||
if not _died_connected:
|
if not _died_connected:
|
||||||
_died_connected = true
|
_died_connected = true
|
||||||
_player.died.connect(_on_player_died)
|
_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():
|
for i: int in _cd_overlays.size():
|
||||||
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
|
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
|
||||||
var ov := _cd_overlays[i]
|
var ov := _cd_overlays[i]
|
||||||
@@ -108,7 +216,10 @@ func _find_spawner() -> void:
|
|||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.pressed and not event.echo:
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
||||||
_restart()
|
if _result_win:
|
||||||
|
_continue_to_map()
|
||||||
|
else:
|
||||||
|
_restart()
|
||||||
elif event.physical_keycode == KEY_TAB and not _game_over:
|
elif event.physical_keycode == KEY_TAB and not _game_over:
|
||||||
_toggle_controls()
|
_toggle_controls()
|
||||||
elif event.physical_keycode == KEY_ESCAPE:
|
elif event.physical_keycode == KEY_ESCAPE:
|
||||||
@@ -127,11 +238,64 @@ func _restart() -> void:
|
|||||||
get_tree().reload_current_scene()
|
get_tree().reload_current_scene()
|
||||||
|
|
||||||
|
|
||||||
|
# A cleared arena advances the run: commit the win and open the map to pick the next node.
|
||||||
|
func _continue_to_map() -> void:
|
||||||
|
get_tree().paused = false
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
Run.complete_current_level()
|
||||||
|
get_tree().change_scene_to_file("res://levels/MapScreen.tscn")
|
||||||
|
|
||||||
|
|
||||||
func _toggle_controls() -> void:
|
func _toggle_controls() -> void:
|
||||||
_controls_visible = not _controls_visible
|
_controls_visible = not _controls_visible
|
||||||
_controls_panel.visible = _controls_visible
|
|
||||||
_tab_hint.visible = not _controls_visible
|
|
||||||
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
|
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
|
||||||
|
_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
|
||||||
|
|
||||||
|
|
||||||
|
# ── 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 ──────────────────────────────────────────────────────────────
|
# ── Win / lose ──────────────────────────────────────────────────────────────
|
||||||
@@ -157,8 +321,14 @@ func _show_game_over(win: bool, _cause: String = "") -> void:
|
|||||||
if _tab_hint:
|
if _tab_hint:
|
||||||
_tab_hint.visible = false
|
_tab_hint.visible = false
|
||||||
|
|
||||||
# Swap in the matching outcome clip and loop it behind the buttons.
|
await get_tree().create_timer(_WIN_DELAY if win else _LOSS_DELAY, false, false, true).timeout
|
||||||
var path := _WIN_VIDEO if win else _LOSS_VIDEO
|
|
||||||
|
_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):
|
if ResourceLoader.exists(path):
|
||||||
_over_video.stream = load(path)
|
_over_video.stream = load(path)
|
||||||
_over_video.play()
|
_over_video.play()
|
||||||
@@ -179,13 +349,40 @@ func _build_game_over() -> void:
|
|||||||
add_child(_over_root)
|
add_child(_over_root)
|
||||||
|
|
||||||
# Full-screen outcome clip (bull win / matador win), looping.
|
# 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 = VideoStreamPlayer.new()
|
||||||
_over_video.set_anchors_preset(Control.PRESET_FULL_RECT)
|
_over_video.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
_over_video.expand = true
|
_over_video.expand = true
|
||||||
_over_video.process_mode = Node.PROCESS_MODE_ALWAYS
|
_over_video.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
_over_video.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
_over_video.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
_over_video.finished.connect(_over_video.play) # loop the outcome clip
|
_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.
|
# Buttons overlaid at the bottom of the clip.
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
@@ -200,14 +397,111 @@ func _build_game_over() -> void:
|
|||||||
row.offset_bottom = -48.0
|
row.offset_bottom = -48.0
|
||||||
_over_root.add_child(row)
|
_over_root.add_child(row)
|
||||||
|
|
||||||
var again := _make_button("Go Again (Enter)")
|
# Win → Continue to the run map; loss → Go Again from the top. Shown per-result in
|
||||||
again.pressed.connect(_restart)
|
# _show_game_over; both share the Enter shortcut.
|
||||||
row.add_child(again)
|
_over_continue = _make_button("Continue (Enter)")
|
||||||
|
_over_continue.pressed.connect(_continue_to_map)
|
||||||
|
row.add_child(_over_continue)
|
||||||
|
|
||||||
|
_over_again = _make_button("Go Again (Enter)")
|
||||||
|
_over_again.pressed.connect(_restart)
|
||||||
|
row.add_child(_over_again)
|
||||||
|
|
||||||
var menu := _make_button("Main Menu")
|
var menu := _make_button("Main Menu")
|
||||||
menu.pressed.connect(_return_to_menu)
|
menu.pressed.connect(_return_to_menu)
|
||||||
row.add_child(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
|
const _SLOT_SEPARATION: int = 10
|
||||||
|
|
||||||
@@ -215,14 +509,9 @@ const _SLOT_SEPARATION: int = 10
|
|||||||
func _build_ability_bar() -> void:
|
func _build_ability_bar() -> void:
|
||||||
_ability_bar = HBoxContainer.new()
|
_ability_bar = HBoxContainer.new()
|
||||||
_ability_bar.add_theme_constant_override("separation", _SLOT_SEPARATION)
|
_ability_bar.add_theme_constant_override("separation", _SLOT_SEPARATION)
|
||||||
_ability_bar.anchor_left = 0.5
|
_ability_bar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
_ability_bar.anchor_right = 0.5
|
_ability_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
_ability_bar.anchor_top = 1.0
|
_bottom_col.add_child(_ability_bar)
|
||||||
_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)
|
|
||||||
for i: int in 4:
|
for i: int in 4:
|
||||||
_ability_bar.add_child(_build_ability_slot(i))
|
_ability_bar.add_child(_build_ability_slot(i))
|
||||||
_layout_ability_bar()
|
_layout_ability_bar()
|
||||||
@@ -230,20 +519,17 @@ func _build_ability_bar() -> void:
|
|||||||
|
|
||||||
|
|
||||||
# Size the ability slots as a fraction of the viewport (clamped) so the bar scales
|
# 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:
|
func _layout_ability_bar() -> void:
|
||||||
var vp := get_viewport().get_visible_rect().size
|
var vp := get_viewport().get_visible_rect().size
|
||||||
var slot := clampf(minf(vp.x * 0.055, vp.y * 0.11), 56.0, 104.0)
|
var slot := clampf(minf(vp.x * 0.055, vp.y * 0.11), 56.0, 104.0)
|
||||||
for panel: PanelContainer in _ability_panels:
|
for panel: PanelContainer in _ability_panels:
|
||||||
panel.custom_minimum_size = Vector2(slot, slot)
|
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:
|
func _build_ability_slot(idx: int) -> Control:
|
||||||
var vbox := VBoxContainer.new()
|
var vbox := VBoxContainer.new()
|
||||||
vbox.add_theme_constant_override("separation", 4)
|
vbox.add_theme_constant_override("separation", 3)
|
||||||
|
|
||||||
var panel := PanelContainer.new()
|
var panel := PanelContainer.new()
|
||||||
panel.custom_minimum_size = Vector2(72.0, 72.0)
|
panel.custom_minimum_size = Vector2(72.0, 72.0)
|
||||||
@@ -298,19 +584,48 @@ func _build_ability_slot(idx: int) -> Control:
|
|||||||
stack.add_child(cd_lbl)
|
stack.add_child(cd_lbl)
|
||||||
_cd_labels.append(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()
|
var key_lbl := Label.new()
|
||||||
key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
|
key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
|
||||||
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
key_lbl.add_theme_font_override("font", UiFonts.body())
|
key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
key_lbl.add_theme_color_override("font_color", _ABILITY_ACCENT[idx].lerp(_CREAM, 0.35))
|
key_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
|
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_constant_override("outline_size", 3)
|
||||||
key_lbl.add_theme_font_size_override("font_size", 13)
|
key_lbl.add_theme_font_size_override("font_size", 14)
|
||||||
vbox.add_child(key_lbl)
|
keycap.add_child(key_lbl)
|
||||||
|
|
||||||
return vbox
|
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:
|
func _build_tab_hint() -> void:
|
||||||
_tab_hint = Label.new()
|
_tab_hint = Label.new()
|
||||||
_tab_hint.text = "Press 'Tab' to see controls"
|
_tab_hint.text = "Press 'Tab' to see controls"
|
||||||
@@ -353,8 +668,8 @@ func _build_controls_panel() -> void:
|
|||||||
]
|
]
|
||||||
_row(vbox, move_keys, "Move", _CREAM)
|
_row(vbox, move_keys, "Move", _CREAM)
|
||||||
_row(vbox, Controls.get_key_label(&"ability_kick"), "Kick", _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_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, Controls.get_key_label(&"ability_roll"), "Roll (Powerball: ram the matador!)", _CREAM)
|
||||||
_row(vbox, "Scroll", "Zoom", _CREAM)
|
_row(vbox, "Scroll", "Zoom", _CREAM)
|
||||||
_row(vbox, "Tab", "Toggle controls", _MUTED)
|
_row(vbox, "Tab", "Toggle controls", _MUTED)
|
||||||
|
|||||||
@@ -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")
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,104 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
const _GOLD := Color(1.00, 0.78, 0.22)
|
||||||
|
const _CREAM := Color(0.93, 0.88, 0.72)
|
||||||
|
|
||||||
|
const _BAR_W: float = 260.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 %d / %d" % [maxi(_current_hp, 0), _max_hp]
|
||||||
|
|
||||||
|
|
||||||
|
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", 13)
|
||||||
|
_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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dfk7dcvki7rqu
|
||||||
@@ -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" path="res://levels/bear_spawn.gd" id="2_spawn"]
|
||||||
|
[ext_resource type="PackedScene" path="res://Bear.tscn" id="3_bear"]
|
||||||
|
[ext_resource type="Script" 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(2.875, 0, 0, 0, 2.875, 0, 0, 0, 2.875, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="floor" parent="bear_level_arena/floor" parent_id_path=PackedInt32Array(1529242945, 41795043) index="0" unique_id=1653720920]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="wall" parent="bear_level_arena/floor" parent_id_path=PackedInt32Array(1529242945, 41795043) index="1" unique_id=2041884474]
|
||||||
|
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"]
|
||||||
@@ -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.ZERO
|
||||||
|
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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bbtlwihtlc1wp
|
||||||
@@ -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())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ptsfs3wq5bje
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c6sef0tfl26sr
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://emq8i2bmkekn
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dt3ffakbwlkmc
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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
|
||||||
|
var count := mini(_wave, _spawn_points.size())
|
||||||
|
var points: Array = _spawn_points.duplicate()
|
||||||
|
points.shuffle()
|
||||||
|
for i: int in count:
|
||||||
|
var sp := points[i] as Node3D
|
||||||
|
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 >= _spawn_points.size():
|
||||||
|
_resolved = true
|
||||||
|
all_defeated.emit()
|
||||||
|
else:
|
||||||
|
await get_tree().create_timer(DP.f("mat_wave_pause")).timeout
|
||||||
|
_start_next_wave()
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
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")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## All known level ids — shown by `level` in the console.
|
||||||
|
func level_ids() -> Array[StringName]:
|
||||||
|
return [&"arena", &"arena_boss", &"bear"]
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
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
|
||||||
|
pending_row = node.row
|
||||||
|
pending_col = node.col
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://u4fartm52bk3
|
||||||
+109
-3
@@ -57,23 +57,26 @@ const ACTION_LABELS: Dictionary = {
|
|||||||
&"move_left": "Move Left",
|
&"move_left": "Move Left",
|
||||||
&"move_right": "Move Right",
|
&"move_right": "Move Right",
|
||||||
&"ability_kick": "Kick",
|
&"ability_kick": "Kick",
|
||||||
&"ability_slam": "Slam",
|
|
||||||
&"ability_dash": "Dash",
|
&"ability_dash": "Dash",
|
||||||
|
&"ability_slam": "Slam",
|
||||||
&"ability_roll": "Roll",
|
&"ability_roll": "Roll",
|
||||||
}
|
}
|
||||||
|
|
||||||
@onready var main_panel: Control = $MainPanel
|
@onready var main_panel: Control = $MainPanel
|
||||||
@onready var options_panel: Control = $OptionsPanel
|
@onready var options_panel: Control = $OptionsPanel
|
||||||
@onready var credits_panel: Control = $CreditsPanel
|
@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 rebind_list: VBoxContainer = $OptionsPanel/VBox/Scroll/RebindList
|
||||||
@onready var fade_rect: ColorRect = $FadeRect
|
@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 options_button: Button = $MainPanel/OptionsButton
|
||||||
@onready var credits_button: Button = $MainPanel/CreditsButton
|
@onready var credits_button: Button = $MainPanel/CreditsButton
|
||||||
@onready var exit_button: Button = $MainPanel/ExitButton
|
@onready var exit_button: Button = $MainPanel/ExitButton
|
||||||
@onready var options_back: Button = $OptionsPanel/VBox/BackButton
|
@onready var options_back: Button = $OptionsPanel/VBox/BackButton
|
||||||
@onready var credits_back: Button = $CreditsPanel/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_action: StringName = &""
|
||||||
var _listening_button: Button = null
|
var _listening_button: Button = null
|
||||||
@@ -87,6 +90,9 @@ func _ready() -> void:
|
|||||||
if video.stream != null:
|
if video.stream != null:
|
||||||
video.play()
|
video.play()
|
||||||
video.finished.connect(video.play) # loop the background clip
|
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)
|
play_button.pressed.connect(_on_play_pressed)
|
||||||
options_button.pressed.connect(_on_options_pressed)
|
options_button.pressed.connect(_on_options_pressed)
|
||||||
@@ -96,6 +102,7 @@ func _ready() -> void:
|
|||||||
credits_back.pressed.connect(_close_subpanels)
|
credits_back.pressed.connect(_close_subpanels)
|
||||||
_apply_theme()
|
_apply_theme()
|
||||||
_build_version_label()
|
_build_version_label()
|
||||||
|
_build_options_toggles()
|
||||||
_build_rebind_rows()
|
_build_rebind_rows()
|
||||||
play_button.grab_focus()
|
play_button.grab_focus()
|
||||||
|
|
||||||
@@ -183,6 +190,101 @@ func _process(_delta: float) -> void:
|
|||||||
play_button.anchor_bottom = cape.y
|
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:
|
func _build_rebind_rows() -> void:
|
||||||
for child in rebind_list.get_children():
|
for child in rebind_list.get_children():
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
@@ -213,6 +315,7 @@ func _build_rebind_rows() -> void:
|
|||||||
|
|
||||||
func _on_play_pressed() -> void:
|
func _on_play_pressed() -> void:
|
||||||
_set_buttons_disabled(true)
|
_set_buttons_disabled(true)
|
||||||
|
Run.start_new_run() # fresh map for this run; the arena is the intro fight
|
||||||
var tween := create_tween()
|
var tween := create_tween()
|
||||||
tween.tween_property(fade_rect, "color:a", 1.0, FADE_TIME)
|
tween.tween_property(fade_rect, "color:a", 1.0, FADE_TIME)
|
||||||
tween.tween_callback(func() -> void:
|
tween.tween_callback(func() -> void:
|
||||||
@@ -221,12 +324,14 @@ func _on_play_pressed() -> void:
|
|||||||
|
|
||||||
func _on_options_pressed() -> void:
|
func _on_options_pressed() -> void:
|
||||||
main_panel.visible = false
|
main_panel.visible = false
|
||||||
|
play_button.visible = false
|
||||||
options_panel.visible = true
|
options_panel.visible = true
|
||||||
options_back.grab_focus()
|
options_back.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
func _on_credits_pressed() -> void:
|
func _on_credits_pressed() -> void:
|
||||||
main_panel.visible = false
|
main_panel.visible = false
|
||||||
|
play_button.visible = false
|
||||||
credits_panel.visible = true
|
credits_panel.visible = true
|
||||||
credits_back.grab_focus()
|
credits_back.grab_focus()
|
||||||
|
|
||||||
@@ -240,6 +345,7 @@ func _close_subpanels() -> void:
|
|||||||
options_panel.visible = false
|
options_panel.visible = false
|
||||||
credits_panel.visible = false
|
credits_panel.visible = false
|
||||||
main_panel.visible = true
|
main_panel.visible = true
|
||||||
|
play_button.visible = true
|
||||||
play_button.grab_focus()
|
play_button.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+209
-124
@@ -22,6 +22,7 @@ var _sim: PhysicalBoneSimulator3D = null
|
|||||||
var _anim_player: AnimationPlayer = null
|
var _anim_player: AnimationPlayer = null
|
||||||
var _wander_target: Vector3 = Vector3.ZERO
|
var _wander_target: Vector3 = Vector3.ZERO
|
||||||
var _idle_timer: float = 0.0
|
var _idle_timer: float = 0.0
|
||||||
|
var _taunt_swap: float = 0.0
|
||||||
var _idle_anim: StringName = _ANIM_IDLE
|
var _idle_anim: StringName = _ANIM_IDLE
|
||||||
var _bull: CharacterBody3D = null
|
var _bull: CharacterBody3D = null
|
||||||
var _step_dir: Vector3 = Vector3.ZERO
|
var _step_dir: Vector3 = Vector3.ZERO
|
||||||
@@ -34,6 +35,7 @@ var _swing_timer: float = 0.0
|
|||||||
var _lunge_timer: float = 0.0
|
var _lunge_timer: float = 0.0
|
||||||
var _roll_timer: float = 0.0
|
var _roll_timer: float = 0.0
|
||||||
var _roll_dir: Vector3 = Vector3.ZERO
|
var _roll_dir: Vector3 = Vector3.ZERO
|
||||||
|
var _is_spawning: bool = false
|
||||||
var _throw_timer: float = 0.0
|
var _throw_timer: float = 0.0
|
||||||
var _throw_aim_cd: float = 0.0
|
var _throw_aim_cd: float = 0.0
|
||||||
var _throw_dir: Vector3 = Vector3.ZERO
|
var _throw_dir: Vector3 = Vector3.ZERO
|
||||||
@@ -50,8 +52,8 @@ var _drawing: bool = false
|
|||||||
var _sheathing: bool = false
|
var _sheathing: bool = false
|
||||||
var _draw_timer: float = 0.0
|
var _draw_timer: float = 0.0
|
||||||
var _draw_len: float = 0.0
|
var _draw_len: float = 0.0
|
||||||
var _resword_timer: float = 0.0
|
|
||||||
var _sword_blade_area: Area3D = null
|
var _sword_blade_area: Area3D = null
|
||||||
|
var _spear_node: Node3D = null
|
||||||
var _blade_hit_cd: float = 0.0
|
var _blade_hit_cd: float = 0.0
|
||||||
var _death_player: AudioStreamPlayer = null
|
var _death_player: AudioStreamPlayer = null
|
||||||
var _blood_burst: CPUParticles3D = null
|
var _blood_burst: CPUParticles3D = null
|
||||||
@@ -74,6 +76,9 @@ const _ANIM_TAUNTS: Array[StringName] = [&"Taunt", &"Taunt_B", &"Taunt_C"]
|
|||||||
const _DRAW_GRAB_AT: float = 0.55
|
const _DRAW_GRAB_AT: float = 0.55
|
||||||
|
|
||||||
const _SWORD_SCENE: PackedScene = preload("res://Assets/sword.glb")
|
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_LOOKAHEAD: float = 3.5 # longer = turns before hitting wall
|
||||||
const _STEER_INTERVAL: float = 0.12 # recompute direction at most ~8×/sec
|
const _STEER_INTERVAL: float = 0.12 # recompute direction at most ~8×/sec
|
||||||
@@ -95,8 +100,9 @@ func _ready() -> void:
|
|||||||
if _skeleton:
|
if _skeleton:
|
||||||
_sim = MatadorRagdoll.build(_skeleton)
|
_sim = MatadorRagdoll.build(_skeleton)
|
||||||
if _anim_player:
|
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)
|
_ensure_loop(anim)
|
||||||
|
_flatten_roll_travel()
|
||||||
if not _anim_player.has_animation(_ANIM_RUN):
|
if not _anim_player.has_animation(_ANIM_RUN):
|
||||||
push_warning("Matador: expected animations not found. Available: %s" %
|
push_warning("Matador: expected animations not found. Available: %s" %
|
||||||
str(_anim_player.get_animation_list()))
|
str(_anim_player.get_animation_list()))
|
||||||
@@ -104,7 +110,7 @@ func _ready() -> void:
|
|||||||
_anim_player.play(_ANIM_IDLE)
|
_anim_player.play(_ANIM_IDLE)
|
||||||
_setup_sword()
|
_setup_sword()
|
||||||
DP.any_changed.connect(_on_dp_changed)
|
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)
|
_hit_area.body_entered.connect(_on_body_entered)
|
||||||
_pick_wander_target()
|
_pick_wander_target()
|
||||||
_blood_burst = preload("res://blood_burst.gd").new()
|
_blood_burst = preload("res://blood_burst.gd").new()
|
||||||
@@ -113,11 +119,13 @@ func _ready() -> void:
|
|||||||
_ole_player = AudioStreamPlayer.new()
|
_ole_player = AudioStreamPlayer.new()
|
||||||
_ole_player.stream = load("res://sounds/ole.ogg")
|
_ole_player.stream = load("res://sounds/ole.ogg")
|
||||||
_ole_player.volume_db = 0.0
|
_ole_player.volume_db = 0.0
|
||||||
|
_ole_player.bus = &"Effects"
|
||||||
add_child(_ole_player)
|
add_child(_ole_player)
|
||||||
if ResourceLoader.exists("res://sounds/matador_death.ogg"):
|
if ResourceLoader.exists("res://sounds/matador_death.ogg"):
|
||||||
_death_player = AudioStreamPlayer.new()
|
_death_player = AudioStreamPlayer.new()
|
||||||
_death_player.stream = load("res://sounds/matador_death.ogg")
|
_death_player.stream = load("res://sounds/matador_death.ogg")
|
||||||
_death_player.volume_db = 2.0
|
_death_player.volume_db = 2.0
|
||||||
|
_death_player.bus = &"Effects"
|
||||||
add_child(_death_player)
|
add_child(_death_player)
|
||||||
|
|
||||||
|
|
||||||
@@ -138,6 +146,10 @@ func _acquire_bull() -> void:
|
|||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
_acquire_bull()
|
_acquire_bull()
|
||||||
|
|
||||||
|
if _is_spawning and is_on_floor():
|
||||||
|
_is_spawning = false
|
||||||
|
collision_layer = 1
|
||||||
|
|
||||||
if DP.b("show_raycasts") and _steer_dir != Vector3.ZERO:
|
if DP.b("show_raycasts") and _steer_dir != Vector3.ZERO:
|
||||||
var o := global_position + Vector3.UP * 0.6
|
var o := global_position + Vector3.UP * 0.6
|
||||||
DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9))
|
DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9))
|
||||||
@@ -148,12 +160,6 @@ func _physics_process(delta: float) -> void:
|
|||||||
_dodge_cd = maxf(0.0, _dodge_cd - delta)
|
_dodge_cd = maxf(0.0, _dodge_cd - delta)
|
||||||
_blade_hit_cd = maxf(0.0, _blade_hit_cd - delta)
|
_blade_hit_cd = maxf(0.0, _blade_hit_cd - delta)
|
||||||
_throw_aim_cd = maxf(0.0, _throw_aim_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:
|
if _state != State.RAGDOLL and _bull != null:
|
||||||
_update_ai_state(delta)
|
_update_ai_state(delta)
|
||||||
|
|
||||||
@@ -216,20 +222,30 @@ func _choose_intent() -> Intent:
|
|||||||
var throw_ok := _throw_aim_cd <= 0.0 and is_instance_valid(_sword_node) \
|
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")
|
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 := {
|
var score := {
|
||||||
# Only drift when the bull is out past throw range; up close this is ~0.
|
# Showmanship: circle the bull at a taunting distance and pose for the crowd.
|
||||||
Intent.WANDER: DP.f("mat_w_showmanship") * 0.3 * beyond,
|
Intent.WANDER: DP.f("mat_w_showmanship") * strut,
|
||||||
# Retreat only when the bull is close AND actually dangerous.
|
# Retreat only when the bull is close AND actually dangerous.
|
||||||
Intent.FLEE: DP.f("mat_w_flee") * close * (0.25 + 0.75 * danger),
|
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) \
|
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.
|
# Sidestep / plant against an incoming charge.
|
||||||
Intent.DODGE: DP.f("mat_w_caution") * danger * (1.0 if _dodge_cd <= 0.0 else 0.0),
|
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
|
# Hurl the blade — an occasional ranged flourish from an engaging matador when
|
||||||
# that punishes a bull for hanging back out of horn reach.
|
# there's a clean lane, not a barrage (see mat_w_throw). Supporters never pelt.
|
||||||
Intent.THROW: ((DP.f("mat_w_showmanship") * 0.55 + DP.f("mat_w_aggression") * 0.6) \
|
Intent.THROW: (DP.f("mat_w_throw") * (0.35 + 0.65 * (1.0 - close))) \
|
||||||
* (0.35 + 0.65 * (1.0 - close))) if throw_ok else -1.0,
|
if (throw_ok and is_engager) else -1.0,
|
||||||
}
|
}
|
||||||
score[_intent] = float(score[_intent]) + DP.f("mat_w_commit")
|
score[_intent] = float(score[_intent]) + DP.f("mat_w_commit")
|
||||||
|
|
||||||
@@ -277,6 +293,40 @@ func _enter_intent(intent: Intent) -> void:
|
|||||||
_start_throw()
|
_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
|
# Bull is moving fast and aimed within ~30° of the matador
|
||||||
func _is_charge_incoming() -> bool:
|
func _is_charge_incoming() -> bool:
|
||||||
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||||
@@ -351,6 +401,14 @@ func _tick_wander(delta: float) -> void:
|
|||||||
if _drawing or _sheathing or _idle_timer > 0.0:
|
if _drawing or _sheathing or _idle_timer > 0.0:
|
||||||
_idle_timer = maxf(0.0, _idle_timer - delta)
|
_idle_timer = maxf(0.0, _idle_timer - delta)
|
||||||
_decelerate(10.0, 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)
|
_play_anim(_idle_anim)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
return
|
return
|
||||||
@@ -358,8 +416,12 @@ func _tick_wander(delta: float) -> void:
|
|||||||
var to_target := Vector3(
|
var to_target := Vector3(
|
||||||
_wander_target.x - global_position.x, 0.0, _wander_target.z - global_position.z)
|
_wander_target.x - global_position.x, 0.0, _wander_target.z - global_position.z)
|
||||||
if to_target.length() < 0.8:
|
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()
|
_idle_anim = _pick_taunt()
|
||||||
|
_taunt_swap = randf_range(1.5, 3.0)
|
||||||
_pick_wander_target()
|
_pick_wander_target()
|
||||||
else:
|
else:
|
||||||
var dir := _steer_clear(to_target.normalized(), delta)
|
var dir := _steer_clear(to_target.normalized(), delta)
|
||||||
@@ -509,6 +571,23 @@ func _enter_roll(hit_dir: Vector3) -> void:
|
|||||||
_anim_player.seek(0.0, true)
|
_anim_player.seek(0.0, true)
|
||||||
|
|
||||||
|
|
||||||
|
# Called by the spawner (deferred) to make the matador leap-roll toward a target
|
||||||
|
# point on entry — the cinematic arena entrance. After the roll it transitions to
|
||||||
|
# normal AI via the standard ROLL → FLEE path.
|
||||||
|
func enter_spawn_roll(toward: Vector3) -> void:
|
||||||
|
var dir := toward - global_position
|
||||||
|
dir.y = 0.0
|
||||||
|
_roll_dir = dir.normalized() if dir.length() > 0.1 else -_mesh.global_transform.basis.z
|
||||||
|
_state = State.ROLL
|
||||||
|
_roll_timer = _anim_length(_ANIM_ROLL, DP.f("mat_roll_duration"))
|
||||||
|
_is_spawning = true
|
||||||
|
collision_layer = 0
|
||||||
|
velocity.y = 4.0
|
||||||
|
if _anim_player and _anim_player.has_animation(_ANIM_ROLL):
|
||||||
|
_anim_player.play(_ANIM_ROLL)
|
||||||
|
_anim_player.seek(0.0, true)
|
||||||
|
|
||||||
|
|
||||||
func _tick_roll(delta: float) -> void:
|
func _tick_roll(delta: float) -> void:
|
||||||
_roll_timer -= delta
|
_roll_timer -= delta
|
||||||
if _roll_timer <= 0.0:
|
if _roll_timer <= 0.0:
|
||||||
@@ -528,7 +607,8 @@ func _start_throw() -> void:
|
|||||||
_throw_timer = DP.f("mat_throw_windup")
|
_throw_timer = DP.f("mat_throw_windup")
|
||||||
velocity = Vector3.ZERO
|
velocity = Vector3.ZERO
|
||||||
_cancel_transitions()
|
_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:
|
if _anim_player:
|
||||||
_anim_player.pause()
|
_anim_player.pause()
|
||||||
var to_bull := _bull.global_position - global_position
|
var to_bull := _bull.global_position - global_position
|
||||||
@@ -552,8 +632,8 @@ func _tick_throw(delta: float) -> void:
|
|||||||
_pose_throw_arm(phase)
|
_pose_throw_arm(phase)
|
||||||
|
|
||||||
var t_rel := DP.f("mat_throw_release")
|
var t_rel := DP.f("mat_throw_release")
|
||||||
if prev < t_rel and phase >= t_rel and is_instance_valid(_sword_node):
|
if prev < t_rel and phase >= t_rel and is_instance_valid(_spear_node):
|
||||||
_release_sword()
|
_release_spear()
|
||||||
|
|
||||||
if _throw_timer <= 0.0:
|
if _throw_timer <= 0.0:
|
||||||
_end_throw()
|
_end_throw()
|
||||||
@@ -562,20 +642,18 @@ func _tick_throw(delta: float) -> void:
|
|||||||
|
|
||||||
func _end_throw() -> void:
|
func _end_throw() -> void:
|
||||||
_state = State.FLEE
|
_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")
|
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||||
# Space throws out so the utility brain doesn't immediately vote another one,
|
# Space throws well apart so a hurl stays a flourish, not a barrage.
|
||||||
# but keep them frequent enough to pressure a bull that hangs back at range.
|
_throw_aim_cd = randf_range(4.0, 7.0)
|
||||||
_throw_aim_cd = randf_range(1.6, 3.2)
|
|
||||||
_intent = Intent.FLEE
|
_intent = Intent.FLEE
|
||||||
# Blend straight into locomotion so the manually-posed throw arm eases out
|
# Blend to locomotion so the manually-posed throw arm eases out (resume() snaps).
|
||||||
# (a bare resume() would snap and leave the player on whatever it paused on).
|
|
||||||
if _anim_player:
|
if _anim_player:
|
||||||
_anim_player.play(_ANIM_RUN)
|
_anim_player.play(_ANIM_RUN)
|
||||||
|
|
||||||
|
|
||||||
# Drive arm_R + forearm_R through a cock-back → release arc while the
|
# 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);
|
# AnimationPlayer is paused. Swing is on bone-local X (see CLAUDE.md), DP-tunable.
|
||||||
# angles are DP-tunable so the arc can be dialled in per rig.
|
|
||||||
func _pose_throw_arm(phase: float) -> void:
|
func _pose_throw_arm(phase: float) -> void:
|
||||||
if not _skeleton:
|
if not _skeleton:
|
||||||
return
|
return
|
||||||
@@ -605,82 +683,59 @@ func _set_bone_swing(bone: String, deg: float) -> void:
|
|||||||
_skeleton.set_bone_pose_rotation(idx, rest * Quaternion(Vector3.RIGHT, deg_to_rad(deg)))
|
_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
|
# Draw a fresh spear into the throwing hand for the wind-up (no melee collider).
|
||||||
# that damages the bull on contact, then settles and despawns.
|
func _spawn_spear_in_hand() -> void:
|
||||||
func _release_sword() -> void:
|
if is_instance_valid(_spear_node) or _sword_hand_attach == null:
|
||||||
if not is_instance_valid(_sword_node):
|
|
||||||
return
|
return
|
||||||
var sword := _sword_node
|
_spear_node = _SPEAR_SCENE.instantiate() as Node3D
|
||||||
var blade := _sword_blade_area
|
_sword_hand_attach.add_child(_spear_node)
|
||||||
var gx := sword.global_transform
|
_spear_node.position = Vector3(
|
||||||
_sword_node = null
|
DP.f("spear_pos_x"), DP.f("spear_pos_y"), DP.f("spear_pos_z"))
|
||||||
_sword_blade_area = null
|
_spear_node.rotation_degrees = Vector3(
|
||||||
_sword_in_hand = false
|
DP.f("spear_rot_x"), DP.f("spear_rot_y"), DP.f("spear_rot_z"))
|
||||||
_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
|
|
||||||
|
|
||||||
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
|
func _clear_spear() -> void:
|
||||||
# the mesh kept its global transform and rb adopted it. Fattened so a fast throw
|
if is_instance_valid(_spear_node):
|
||||||
# reliably overlaps the bull's collision spheres instead of tunnelling past.
|
_spear_node.queue_free()
|
||||||
var cap := CapsuleShape3D.new()
|
_spear_node = null
|
||||||
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)
|
|
||||||
|
|
||||||
# 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
|
# Hurl the held spear on a ballistic arc that lands on the bull's low body. The
|
||||||
# keeps its vertical component so the blade drives into the body, not over it.
|
# launch velocity is solved so gravity (mat_throw_gravity) carries it down onto the
|
||||||
var speed := maxf(DP.f("mat_throw_speed"), 0.1)
|
# lead point; SpearProjectile aims the shaft along the arc and embeds it on a hit.
|
||||||
var dir := (_throw_dir + Vector3.UP * 0.0)
|
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):
|
if is_instance_valid(_bull):
|
||||||
var target := _bull.global_position + Vector3(0.0, DP.f("mat_throw_aim_y"), 0.0)
|
target = _bull.global_position + Vector3(0.0, DP.f("mat_throw_aim_y"), 0.0)
|
||||||
var flat := target - gx.origin
|
var lead := target - origin
|
||||||
flat.y = 0.0
|
lead.y = 0.0
|
||||||
var flight := flat.length() / speed
|
target += _bull.velocity * (lead.length() / hs)
|
||||||
target += _bull.velocity * flight
|
var flat := target - origin
|
||||||
var to_target := target - gx.origin
|
var rise := flat.y
|
||||||
if to_target.length() > 0.1:
|
flat.y = 0.0
|
||||||
dir = to_target.normalized()
|
var dist := flat.length()
|
||||||
# A little aim scatter so throws are a threat to READ and dodge, not a hitscan —
|
var hdir := flat.normalized() if dist > 0.01 else _throw_dir
|
||||||
# a bull that keeps moving can slip them, a stationary one gets pinned.
|
# Aim scatter so the arc can be read and sidestepped, not a hitscan.
|
||||||
var spread := deg_to_rad(DP.f("mat_throw_spread"))
|
var spread := deg_to_rad(DP.f("mat_throw_spread"))
|
||||||
dir = dir.rotated(Vector3.UP, randf_range(-spread, spread)).normalized()
|
hdir = hdir.rotated(Vector3.UP, randf_range(-spread, spread)).normalized()
|
||||||
rb.linear_velocity = dir * speed
|
# Ballistic solve: with horizontal speed hs over distance dist, the flight lasts t;
|
||||||
rb.angular_velocity = dir.cross(Vector3.UP).normalized() * -DP.f("mat_throw_spin")
|
# pick the vertical launch speed that lands the arc on the aim point's height.
|
||||||
|
var t := dist / hs
|
||||||
var hit_done := [false]
|
var vy := (rise + 0.5 * g * t * t) / t
|
||||||
rb.body_entered.connect(func(body: Node) -> void:
|
var vel := hdir * hs + Vector3.UP * vy
|
||||||
if hit_done[0]:
|
SpearProjectile.launch(get_tree().current_scene, spear, origin, vel,
|
||||||
return
|
maxf(DP.f("mat_throw_gravity"), 0.05))
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _tick_ragdoll(_delta: float) -> void:
|
func _tick_ragdoll(_delta: float) -> void:
|
||||||
@@ -756,6 +811,7 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float, up_boost: float = 0.0)
|
|||||||
_state = State.RAGDOLL
|
_state = State.RAGDOLL
|
||||||
if _sword_blade_area:
|
if _sword_blade_area:
|
||||||
_sword_blade_area.monitoring = false
|
_sword_blade_area.monitoring = false
|
||||||
|
_clear_spear() # a spear mid-wind-up dies with the matador, not in the air
|
||||||
killed.emit()
|
killed.emit()
|
||||||
var cleanup_timer := get_tree().create_timer(4.0)
|
var cleanup_timer := get_tree().create_timer(4.0)
|
||||||
cleanup_timer.timeout.connect(func() -> void:
|
cleanup_timer.timeout.connect(func() -> void:
|
||||||
@@ -866,6 +922,32 @@ func _ensure_loop(anim_name: StringName) -> void:
|
|||||||
anim.loop_mode = Animation.LOOP_LINEAR
|
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:
|
func _pick_taunt() -> StringName:
|
||||||
var choices: Array[StringName] = []
|
var choices: Array[StringName] = []
|
||||||
for anim in _ANIM_TAUNTS:
|
for anim in _ANIM_TAUNTS:
|
||||||
@@ -893,13 +975,25 @@ func _play_anim(anim_name: StringName) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _pick_wander_target() -> void:
|
func _pick_wander_target() -> void:
|
||||||
# 35% chance: pick a point orbiting close to the bull for a daring pass
|
# Circle the bull for showmanship: mostly settle on the taunt ring (spreading the
|
||||||
if _bull != null and randf() < 0.35:
|
# crowd out around it), with an occasional close pass for daring flair.
|
||||||
var angle := randf() * TAU
|
if _bull != null:
|
||||||
var r := randf_range(1.5, 4.0)
|
|
||||||
var bull_flat := _bull.global_position
|
var bull_flat := _bull.global_position
|
||||||
bull_flat.y = 0.0
|
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)
|
_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
|
return
|
||||||
var radius := DP.f("mat_wander_radius")
|
var radius := DP.f("mat_wander_radius")
|
||||||
var angle := randf() * TAU
|
var angle := randf() * TAU
|
||||||
@@ -909,13 +1003,10 @@ func _pick_wander_target() -> void:
|
|||||||
|
|
||||||
# ── Sword attachment ──────────────────────────────────────────────────────────
|
# ── Sword attachment ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# The rig (matador_v03) carries two purpose-built bones for the sword:
|
# The rig (matador_v03) carries two purpose-built bones: weapon_bone (right-hand
|
||||||
# weapon_bone — the grip pose in the right hand (drawn / fighting)
|
# grip) and weapon_rest_bone (hip holster). The sword rides in the holster and is
|
||||||
# weapon_rest_bone — the holstered pose at the hip (sheathed / wandering)
|
# carried to the hand in combat (see _carry_sword); each bone has its own DP-tunable
|
||||||
# The sword lives in the holster by default and is carried to the hand while the
|
# local offset/rotation (sword_* / sword_rest_*) so the poses seat independently.
|
||||||
# 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.
|
|
||||||
func _setup_sword() -> void:
|
func _setup_sword() -> void:
|
||||||
if not _skeleton:
|
if not _skeleton:
|
||||||
return
|
return
|
||||||
@@ -934,8 +1025,7 @@ func _setup_sword() -> void:
|
|||||||
_spawn_sword()
|
_spawn_sword()
|
||||||
|
|
||||||
|
|
||||||
# Instantiate a fresh sword into the holster, with its own blade collider. Called
|
# Instantiate the matador's sword into the holster, with its own blade collider.
|
||||||
# at spawn and again after a throw, so a matador can keep drawing replacements.
|
|
||||||
func _spawn_sword() -> void:
|
func _spawn_sword() -> void:
|
||||||
if is_instance_valid(_sword_node):
|
if is_instance_valid(_sword_node):
|
||||||
return
|
return
|
||||||
@@ -949,15 +1039,14 @@ func _spawn_sword() -> void:
|
|||||||
_drawing = false
|
_drawing = false
|
||||||
_apply_sword_grip()
|
_apply_sword_grip()
|
||||||
|
|
||||||
# Blade collider spans the steel (hilt at local origin, blade running +Z out
|
# Blade collider along +Z (hilt at origin), tipped from the capsule's default Y.
|
||||||
# to ~1.4 m). Only monitors while the sword is in hand (see _carry_sword).
|
# Only monitors while the sword is in hand (see _carry_sword).
|
||||||
var blade_cap := CapsuleShape3D.new()
|
var blade_cap := CapsuleShape3D.new()
|
||||||
blade_cap.radius = 0.18
|
blade_cap.radius = 0.18
|
||||||
blade_cap.height = 1.30
|
blade_cap.height = 1.30
|
||||||
var blade_cs := CollisionShape3D.new()
|
var blade_cs := CollisionShape3D.new()
|
||||||
blade_cs.shape = blade_cap
|
blade_cs.shape = blade_cap
|
||||||
blade_cs.position = Vector3(0.0, 0.0, 0.8)
|
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)
|
blade_cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
|
||||||
|
|
||||||
_sword_blade_area = Area3D.new()
|
_sword_blade_area = Area3D.new()
|
||||||
@@ -969,12 +1058,11 @@ func _spawn_sword() -> void:
|
|||||||
_sword_blade_area.body_entered.connect(_on_blade_hit)
|
_sword_blade_area.body_entered.connect(_on_blade_hit)
|
||||||
|
|
||||||
|
|
||||||
# The sword is carried by ACTION, not distance: drawn only while striking
|
# The sword is MELEE-only: drawn while fighting up close (ATTACK) or guarding
|
||||||
# (ATTACK / THROW) and holstered while running, dodging, bracing or taunting.
|
# (BRACE / SIDESTEP), holstered otherwise. Ranged throws use a spear instead
|
||||||
# THROW snaps the blade out (its arm cock-back covers the motion); everything
|
# (see _spawn_spear_in_hand), so THROW keeps the sword holstered.
|
||||||
# else animates with the Draw_weapon clip — forward to draw, reversed to sheathe.
|
|
||||||
func _wants_sword_drawn() -> bool:
|
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
|
or _state == State.BRACE or _state == State.SIDESTEP
|
||||||
|
|
||||||
|
|
||||||
@@ -1019,8 +1107,7 @@ func _begin_sheathe() -> void:
|
|||||||
_anim_player.play(_ANIM_DRAW, -1.0, -_draw_speed(), true)
|
_anim_player.play(_ANIM_DRAW, -1.0, -_draw_speed(), true)
|
||||||
|
|
||||||
|
|
||||||
# Progress a draw or sheathe; the blade reparents at the grab point (mirrored for
|
# Progress a draw or sheathe; the blade reparents at the grab point (mirrored on the reverse).
|
||||||
# the reverse sheathe), and the transition ends when the clip's runtime elapses.
|
|
||||||
func _advance_draw(delta: float) -> void:
|
func _advance_draw(delta: float) -> void:
|
||||||
if not _drawing and not _sheathing:
|
if not _drawing and not _sheathing:
|
||||||
return
|
return
|
||||||
@@ -1049,9 +1136,7 @@ func _draw_speed() -> float:
|
|||||||
return maxf(DP.f("mat_draw_speed"), 0.1)
|
return maxf(DP.f("mat_draw_speed"), 0.1)
|
||||||
|
|
||||||
|
|
||||||
# Move the sword between the hand grip and the hip holster. Reparenting follows
|
# Move the sword between hand grip and hip holster, reapplying the local seating.
|
||||||
# whichever bone the BoneAttachment tracks; the local grip transform is reapplied
|
|
||||||
# so the seating is identical in both sockets.
|
|
||||||
func _carry_sword(in_hand: bool) -> void:
|
func _carry_sword(in_hand: bool) -> void:
|
||||||
if not is_instance_valid(_sword_node):
|
if not is_instance_valid(_sword_node):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bmdtg6qlh7wri
|
||||||
@@ -14,7 +14,9 @@ const _ANIM_IDLE: StringName = &"Armature|IDLE"
|
|||||||
const _ANIM_KICK: StringName = &"Armature|KICK"
|
const _ANIM_KICK: StringName = &"Armature|KICK"
|
||||||
const _ANIM_SLAM: StringName = &"Armature|SLAM"
|
const _ANIM_SLAM: StringName = &"Armature|SLAM"
|
||||||
const _ANIM_DASH: StringName = &"Armature|DASH"
|
const _ANIM_DASH: StringName = &"Armature|DASH"
|
||||||
|
const _ANIM_WALK: StringName = &"Armature|WALK"
|
||||||
const _ANIM_ROLL: StringName = &"Armature|ROLL"
|
const _ANIM_ROLL: StringName = &"Armature|ROLL"
|
||||||
|
const _ANIM_DEATH: StringName = &"Armature|DEATH"
|
||||||
|
|
||||||
@onready var camera_pivot: Node3D = $Camera3D
|
@onready var camera_pivot: Node3D = $Camera3D
|
||||||
@onready var cube_guy: Node3D = $bull
|
@onready var cube_guy: Node3D = $bull
|
||||||
@@ -27,16 +29,23 @@ var _pre_slide_vel_y: float = 0.0
|
|||||||
|
|
||||||
var _slam_ring_mesh: ArrayMesh = null
|
var _slam_ring_mesh: ArrayMesh = null
|
||||||
|
|
||||||
# Ability system — kick=0, slam=1, dash=2, roll=3
|
# Ability system — kick=0, dash=1, slam=2, roll=3
|
||||||
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
||||||
var _ability_active: bool = false
|
var _ability_active: bool = false
|
||||||
var _ability_timer: float = 0.0
|
var _ability_timer: float = 0.0
|
||||||
var _active_ability: int = -1
|
var _active_ability: int = -1
|
||||||
var _dash_dir: Vector3 = Vector3.ZERO
|
var _dash_dir: Vector3 = Vector3.ZERO
|
||||||
|
var _dash_speed_cur: float = 0.0
|
||||||
var _roll_dir: Vector3 = Vector3.ZERO
|
var _roll_dir: Vector3 = Vector3.ZERO
|
||||||
var _roll_ramp: float = 0.0
|
var _roll_ramp: float = 0.0
|
||||||
var _roll_spin: float = 0.0
|
var _roll_spin: float = 0.0
|
||||||
|
var _roll_target: Node3D = null
|
||||||
|
var _roll_chains_left: int = 0
|
||||||
|
var _roll_hit: Array[Node] = []
|
||||||
var _bull_anim: AnimationPlayer = null
|
var _bull_anim: AnimationPlayer = null
|
||||||
|
# Locomotion animation state: true while the looping DASH clip is driving WASD
|
||||||
|
# movement, false while idling. Flipping it cross-fades between the two clips.
|
||||||
|
var _locomotion_moving: bool = false
|
||||||
|
|
||||||
var _slam_pending: bool = false
|
var _slam_pending: bool = false
|
||||||
var _charge_trail_emitter: CPUParticles3D = null
|
var _charge_trail_emitter: CPUParticles3D = null
|
||||||
@@ -49,6 +58,17 @@ var _cube_scale: Vector3 = Vector3.ONE
|
|||||||
signal died(cause: String)
|
signal died(cause: String)
|
||||||
var _dead: bool = false
|
var _dead: bool = false
|
||||||
|
|
||||||
|
# HP system — the bull soaks several clean hits instead of dying to the first.
|
||||||
|
# Each landed blade drops one pip and starts an i-frame window (so one pass can't
|
||||||
|
# empty the bar); at 0 the run ends via `died`. The HUD listens on `health_changed`.
|
||||||
|
signal health_changed(current: int, max_hp: int)
|
||||||
|
# Fired on every landed hit (after i-frames pass) so the HUD can flash the damage
|
||||||
|
# vignette; `cause` mirrors take_sword_hit ("gored" / "thrown").
|
||||||
|
signal hit_taken(cause: String)
|
||||||
|
var _max_hp: int = 10
|
||||||
|
var _hp: int = 10
|
||||||
|
var _hit_iframes: float = 0.0
|
||||||
|
|
||||||
var _huff_player: AudioStreamPlayer = null
|
var _huff_player: AudioStreamPlayer = null
|
||||||
var _crowd_player: AudioStreamPlayer = null
|
var _crowd_player: AudioStreamPlayer = null
|
||||||
var _huff_timer: float = 0.0
|
var _huff_timer: float = 0.0
|
||||||
@@ -61,6 +81,8 @@ var _charge_dust_ramp: Gradient = null
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group(&"player")
|
add_to_group(&"player")
|
||||||
|
_max_hp = maxi(1, int(DP.f("bull_max_hp")))
|
||||||
|
_hp = _max_hp
|
||||||
_cube_scale = cube_guy.scale
|
_cube_scale = cube_guy.scale
|
||||||
_setup_hoof_dust()
|
_setup_hoof_dust()
|
||||||
_setup_legs()
|
_setup_legs()
|
||||||
@@ -80,16 +102,16 @@ func _setup_legs() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _setup_hoof_dust() -> void:
|
func _setup_hoof_dust() -> void:
|
||||||
var sphere := _particle_sphere(0.10, 4, 2, _unshaded_material())
|
var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material())
|
||||||
|
|
||||||
_walk_dust_ramp = Gradient.new()
|
_walk_dust_ramp = Gradient.new()
|
||||||
_walk_dust_ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
|
_walk_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
|
||||||
_walk_dust_ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0))
|
_walk_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
|
||||||
|
|
||||||
# Light blue dust while charging
|
# White-beige dust while running
|
||||||
_charge_dust_ramp = Gradient.new()
|
_charge_dust_ramp = Gradient.new()
|
||||||
_charge_dust_ramp.set_color(0, Color(0.20, 0.40, 0.85, 1.0))
|
_charge_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0))
|
||||||
_charge_dust_ramp.set_color(1, Color(0.20, 0.40, 0.85, 0.0))
|
_charge_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0))
|
||||||
|
|
||||||
var scale_curve := Curve.new()
|
var scale_curve := Curve.new()
|
||||||
scale_curve.add_point(Vector2(0.0, 0.2))
|
scale_curve.add_point(Vector2(0.0, 0.2))
|
||||||
@@ -193,6 +215,7 @@ func _setup_audio() -> void:
|
|||||||
_huff_player = AudioStreamPlayer.new()
|
_huff_player = AudioStreamPlayer.new()
|
||||||
_huff_player.stream = load("res://sounds/bull_huff.ogg")
|
_huff_player.stream = load("res://sounds/bull_huff.ogg")
|
||||||
_huff_player.volume_db = 0.0
|
_huff_player.volume_db = 0.0
|
||||||
|
_huff_player.bus = &"Effects"
|
||||||
add_child(_huff_player)
|
add_child(_huff_player)
|
||||||
if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"):
|
if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"):
|
||||||
_crowd_player = AudioStreamPlayer.new()
|
_crowd_player = AudioStreamPlayer.new()
|
||||||
@@ -202,6 +225,7 @@ func _setup_audio() -> void:
|
|||||||
_crowd_player.stream = s
|
_crowd_player.stream = s
|
||||||
_crowd_player.volume_db = -14.0
|
_crowd_player.volume_db = -14.0
|
||||||
_crowd_player.autoplay = true
|
_crowd_player.autoplay = true
|
||||||
|
_crowd_player.bus = &"Ambient"
|
||||||
add_child(_crowd_player)
|
add_child(_crowd_player)
|
||||||
|
|
||||||
|
|
||||||
@@ -231,8 +255,8 @@ func _set_dust_state(new_state: DustState) -> void:
|
|||||||
p.initial_velocity_min = DP.f("charge_vel_min")
|
p.initial_velocity_min = DP.f("charge_vel_min")
|
||||||
p.initial_velocity_max = DP.f("charge_vel_max")
|
p.initial_velocity_max = DP.f("charge_vel_max")
|
||||||
p.emitting = true
|
p.emitting = true
|
||||||
if _charge_trail_emitter:
|
# Blue energy trail belongs to the roll/dash ability only, not to fast WASD
|
||||||
_charge_trail_emitter.emitting = _dust_state == DustState.CHARGE
|
# running — driven explicitly in the roll loop / _end_roll, not by dust state.
|
||||||
|
|
||||||
|
|
||||||
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||||
@@ -258,8 +282,9 @@ func _find_anim_player(node: Node) -> AnimationPlayer:
|
|||||||
func _setup_bull_idle() -> void:
|
func _setup_bull_idle() -> void:
|
||||||
if _bull_anim == null:
|
if _bull_anim == null:
|
||||||
return
|
return
|
||||||
# IDLE and ROLL both loop as continuous states; the ability clips are one-shot.
|
# IDLE, DASH and ROLL loop as continuous states (DASH doubles as the WASD
|
||||||
for clip: StringName in [_ANIM_IDLE, _ANIM_ROLL]:
|
# locomotion cycle); the remaining ability clips are one-shot.
|
||||||
|
for clip: StringName in [_ANIM_IDLE, _ANIM_DASH, _ANIM_ROLL]:
|
||||||
if _bull_anim.has_animation(clip):
|
if _bull_anim.has_animation(clip):
|
||||||
_bull_anim.get_animation(clip).loop_mode = Animation.LOOP_LINEAR
|
_bull_anim.get_animation(clip).loop_mode = Animation.LOOP_LINEAR
|
||||||
_play_idle()
|
_play_idle()
|
||||||
@@ -268,6 +293,7 @@ func _setup_bull_idle() -> void:
|
|||||||
# Return to the looping idle (and undo any ability speed-scale). Cross-fades so a
|
# Return to the looping idle (and undo any ability speed-scale). Cross-fades so a
|
||||||
# finishing ability clip eases back to idle instead of snapping.
|
# finishing ability clip eases back to idle instead of snapping.
|
||||||
func _play_idle() -> void:
|
func _play_idle() -> void:
|
||||||
|
_locomotion_moving = false
|
||||||
if _bull_anim == null:
|
if _bull_anim == null:
|
||||||
return
|
return
|
||||||
_bull_anim.speed_scale = 1.0
|
_bull_anim.speed_scale = 1.0
|
||||||
@@ -275,25 +301,58 @@ func _play_idle() -> void:
|
|||||||
_bull_anim.play(_ANIM_IDLE, 0.2)
|
_bull_anim.play(_ANIM_IDLE, 0.2)
|
||||||
|
|
||||||
|
|
||||||
|
# Drive the WASD locomotion clip: the looping DASH doubles as the run cycle while a
|
||||||
|
# direction is held, easing back to idle when the bull coasts to a stop. Cross-fades
|
||||||
|
# on each transition so idle↔run (and steering into it after an ability) blend rather
|
||||||
|
# than snap. Abilities own the animation while active, so this yields to them.
|
||||||
|
func _update_locomotion_anim(moving: bool) -> void:
|
||||||
|
if _ability_active or _bull_anim == null or moving == _locomotion_moving:
|
||||||
|
return
|
||||||
|
_locomotion_moving = moving
|
||||||
|
if moving:
|
||||||
|
if _bull_anim.has_animation(_ANIM_WALK):
|
||||||
|
_bull_anim.speed_scale = 2.0
|
||||||
|
_bull_anim.play(_ANIM_WALK, 1)
|
||||||
|
var anim = _bull_anim.get_animation(_ANIM_WALK)
|
||||||
|
anim.loop_mode = Animation.LOOP_LINEAR
|
||||||
|
else:
|
||||||
|
_play_idle()
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if _dead:
|
||||||
|
return
|
||||||
# Abilities can be chained: a new one interrupts whatever is currently
|
# Abilities can be chained: a new one interrupts whatever is currently
|
||||||
# active, gated only by each ability's own cooldown.
|
# active, gated only by each ability's own cooldown.
|
||||||
if event.is_action_pressed(&"ability_kick") and ability_cd[0] <= 0.0:
|
if event.is_action_pressed(&"ability_kick"):
|
||||||
_activate_kick()
|
try_activate_ability(0)
|
||||||
elif event.is_action_pressed(&"ability_slam") and ability_cd[1] <= 0.0:
|
elif event.is_action_pressed(&"ability_dash"):
|
||||||
_activate_slam()
|
try_activate_ability(1)
|
||||||
elif event.is_action_pressed(&"ability_dash") and ability_cd[2] <= 0.0:
|
elif event.is_action_pressed(&"ability_slam"):
|
||||||
_activate_dash()
|
try_activate_ability(2)
|
||||||
elif event.is_action_pressed(&"ability_roll") and ability_cd[3] <= 0.0:
|
elif event.is_action_pressed(&"ability_roll"):
|
||||||
_activate_roll()
|
try_activate_ability(3)
|
||||||
|
|
||||||
|
|
||||||
|
# Public ability entry point shared by keyboard/gamepad input and the on-screen
|
||||||
|
# touch buttons. Slot order matches ability_cd — kick=0, dash=1, slam=2, roll=3.
|
||||||
|
# No-ops if the bull is dead or the slot is still cooling down.
|
||||||
|
func try_activate_ability(slot: int) -> void:
|
||||||
|
if _dead or slot < 0 or slot >= ability_cd.size() or ability_cd[slot] > 0.0:
|
||||||
|
return
|
||||||
|
match slot:
|
||||||
|
0: _activate_kick()
|
||||||
|
1: _activate_dash()
|
||||||
|
2: _activate_slam()
|
||||||
|
3: _activate_roll()
|
||||||
|
|
||||||
|
|
||||||
func ability_cooldown_fraction(slot: int) -> float:
|
func ability_cooldown_fraction(slot: int) -> float:
|
||||||
var maxcd := 0.0
|
var maxcd := 0.0
|
||||||
match slot:
|
match slot:
|
||||||
0: maxcd = DP.f("kick_cooldown")
|
0: maxcd = DP.f("kick_cooldown")
|
||||||
1: maxcd = DP.f("slam_cooldown")
|
1: maxcd = DP.f("dash_cooldown")
|
||||||
2: maxcd = DP.f("dash_cooldown")
|
2: maxcd = DP.f("slam_cooldown")
|
||||||
3: maxcd = DP.f("roll_cooldown")
|
3: maxcd = DP.f("roll_cooldown")
|
||||||
return ability_cd[slot] / maxcd if maxcd > 0.0 else 0.0
|
return ability_cd[slot] / maxcd if maxcd > 0.0 else 0.0
|
||||||
|
|
||||||
@@ -306,9 +365,9 @@ func _activate_kick() -> void:
|
|||||||
ability_cd[0] = DP.f("kick_cooldown")
|
ability_cd[0] = DP.f("kick_cooldown")
|
||||||
_ability_active = true
|
_ability_active = true
|
||||||
_active_ability = 0
|
_active_ability = 0
|
||||||
_ability_timer = 0.6
|
_ability_timer = 0.3
|
||||||
if _bull_anim:
|
if _bull_anim:
|
||||||
_bull_anim.speed_scale = 1.0
|
_bull_anim.speed_scale = 2.0
|
||||||
if _bull_anim and _bull_anim.has_animation(_ANIM_KICK):
|
if _bull_anim and _bull_anim.has_animation(_ANIM_KICK):
|
||||||
_bull_anim.play(_ANIM_KICK)
|
_bull_anim.play(_ANIM_KICK)
|
||||||
_ability_timer = _bull_anim.get_animation(_ANIM_KICK).length
|
_ability_timer = _bull_anim.get_animation(_ANIM_KICK).length
|
||||||
@@ -321,11 +380,12 @@ func _activate_kick() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _activate_slam() -> void:
|
func _activate_slam() -> void:
|
||||||
ability_cd[1] = DP.f("slam_cooldown")
|
ability_cd[2] = DP.f("slam_cooldown")
|
||||||
_ability_active = true
|
_ability_active = true
|
||||||
_active_ability = 1
|
_active_ability = 2
|
||||||
_legs.set(&"tail_ragdoll", true)
|
_legs.set(&"tail_ragdoll", true)
|
||||||
_slam_pending = true
|
_slam_pending = true
|
||||||
|
SpearProjectile.shed_all.call_deferred(self) # the leap shakes off any stuck spears
|
||||||
var jump_vel := DP.f("slam_jump_velocity")
|
var jump_vel := DP.f("slam_jump_velocity")
|
||||||
velocity.y = jump_vel
|
velocity.y = jump_vel
|
||||||
# Airtime with an accelerated descent: rise (v/g) + fall (v / (g·√mult)).
|
# Airtime with an accelerated descent: rise (v/g) + fall (v / (g·√mult)).
|
||||||
@@ -339,9 +399,9 @@ func _activate_slam() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _activate_dash() -> void:
|
func _activate_dash() -> void:
|
||||||
ability_cd[2] = DP.f("dash_cooldown")
|
ability_cd[1] = DP.f("dash_cooldown")
|
||||||
_ability_active = true
|
_ability_active = true
|
||||||
_active_ability = 2
|
_active_ability = 1
|
||||||
_ability_timer = 0.45
|
_ability_timer = 0.45
|
||||||
if _bull_anim:
|
if _bull_anim:
|
||||||
_bull_anim.speed_scale = 1.0
|
_bull_anim.speed_scale = 1.0
|
||||||
@@ -350,8 +410,9 @@ func _activate_dash() -> void:
|
|||||||
_ability_timer = _bull_anim.get_animation(_ANIM_DASH).length
|
_ability_timer = _bull_anim.get_animation(_ANIM_DASH).length
|
||||||
var raw := cube_guy.global_transform.basis.z
|
var raw := cube_guy.global_transform.basis.z
|
||||||
_dash_dir = Vector3(raw.x, 0.0, raw.z).normalized()
|
_dash_dir = Vector3(raw.x, 0.0, raw.z).normalized()
|
||||||
velocity.x = _dash_dir.x * DP.f("dash_speed")
|
_dash_speed_cur = DP.f("dash_speed")
|
||||||
velocity.z = _dash_dir.z * DP.f("dash_speed")
|
velocity.x = _dash_dir.x * _dash_speed_cur
|
||||||
|
velocity.z = _dash_dir.z * _dash_speed_cur
|
||||||
_spawn_dash_burst()
|
_spawn_dash_burst()
|
||||||
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
|
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
|
||||||
if _huff_player:
|
if _huff_player:
|
||||||
@@ -366,10 +427,16 @@ func _activate_roll() -> void:
|
|||||||
_ability_timer = DP.f("roll_duration")
|
_ability_timer = DP.f("roll_duration")
|
||||||
_roll_ramp = 0.0
|
_roll_ramp = 0.0
|
||||||
_roll_spin = 0.0
|
_roll_spin = 0.0
|
||||||
# Curl into the ball along the current heading (or facing when standing still)
|
_roll_chains_left = int(DP.f("roll_chain_count"))
|
||||||
# and launch at the base speed — it ramps up from here the longer you roll.
|
_roll_hit.clear()
|
||||||
|
# Lock onto the closest matador and curl into the ball aimed at it; if the arena
|
||||||
|
# is empty, launch along the current heading (or facing when standing still).
|
||||||
|
_roll_target = _roll_pick_target()
|
||||||
var flat := Vector3(velocity.x, 0.0, velocity.z)
|
var flat := Vector3(velocity.x, 0.0, velocity.z)
|
||||||
if flat.length() > 1.0:
|
if _roll_target != null:
|
||||||
|
var to_t := _roll_target.global_position - global_position
|
||||||
|
_roll_dir = Vector3(to_t.x, 0.0, to_t.z).normalized()
|
||||||
|
elif flat.length() > 1.0:
|
||||||
_roll_dir = flat.normalized()
|
_roll_dir = flat.normalized()
|
||||||
else:
|
else:
|
||||||
var f := cube_guy.global_transform.basis.z
|
var f := cube_guy.global_transform.basis.z
|
||||||
@@ -392,14 +459,20 @@ func _activate_roll() -> void:
|
|||||||
_huff_player.play()
|
_huff_player.play()
|
||||||
|
|
||||||
|
|
||||||
# One rolling frame (Rammus Powerball): steer with momentum, accelerate the longer
|
# One rolling frame: home on the current target matador, accelerate the longer the
|
||||||
# the roll lasts, move (walls just slide), then pop the ball on the first matador we
|
# roll lasts, move (walls just slide), then pop the ball on contact and chain to the
|
||||||
# ram. Fully replaces the normal locomotion path while active.
|
# next closest. Fully replaces the normal locomotion path while active. `steer` is
|
||||||
func _tick_roll(delta: float, steer: Vector3) -> void:
|
# ignored — the roll auto-targets — but kept so the caller stays uniform.
|
||||||
var steer_flat := Vector3(steer.x, 0.0, steer.z)
|
func _tick_roll(delta: float, _steer: Vector3) -> void:
|
||||||
if steer_flat.length() > 0.1:
|
# Reacquire if the target ragdolled or was freed before we reached it.
|
||||||
var t := clampf(DP.f("roll_turn") * delta, 0.0, 1.0)
|
if not _roll_target_valid():
|
||||||
_roll_dir = _roll_dir.slerp(steer_flat.normalized(), t).normalized()
|
_roll_target = _roll_pick_target()
|
||||||
|
if _roll_target != null:
|
||||||
|
var to_t := _roll_target.global_position - global_position
|
||||||
|
var flat := Vector3(to_t.x, 0.0, to_t.z)
|
||||||
|
if flat.length() > 0.1:
|
||||||
|
var t := clampf(DP.f("roll_turn") * delta, 0.0, 1.0)
|
||||||
|
_roll_dir = _roll_dir.slerp(flat.normalized(), t).normalized()
|
||||||
|
|
||||||
# Speed builds over time: base → max across roll_rampup_time, so a long roll
|
# Speed builds over time: base → max across roll_rampup_time, so a long roll
|
||||||
# winds up into a runaway boulder while a quick tap barely gets going.
|
# winds up into a runaway boulder while a quick tap barely gets going.
|
||||||
@@ -423,6 +496,7 @@ func _tick_roll(delta: float, steer: Vector3) -> void:
|
|||||||
|
|
||||||
_set_dust_state(DustState.CHARGE)
|
_set_dust_state(DustState.CHARGE)
|
||||||
if _charge_trail_emitter:
|
if _charge_trail_emitter:
|
||||||
|
_charge_trail_emitter.emitting = true
|
||||||
_charge_trail_emitter.direction = (-_roll_dir + Vector3(0.0, 0.25, 0.0)).normalized()
|
_charge_trail_emitter.direction = (-_roll_dir + Vector3(0.0, 0.25, 0.0)).normalized()
|
||||||
|
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
@@ -431,25 +505,64 @@ func _tick_roll(delta: float, steer: Vector3) -> void:
|
|||||||
|
|
||||||
|
|
||||||
# Pop the ball on ramming a matador: launch every matador in the hit radius up and
|
# Pop the ball on ramming a matador: launch every matador in the hit radius up and
|
||||||
# outward, then end the roll (Rammus stops when Powerball connects). Returns true if
|
# outward, then either CHAIN to the next closest (if chains remain and a target is
|
||||||
# it popped so the caller can stop touching roll state this frame.
|
# left) or end the roll. Returns true if it popped so the caller can stop touching
|
||||||
|
# roll state this frame.
|
||||||
func _roll_try_pop(speed: float) -> bool:
|
func _roll_try_pop(speed: float) -> bool:
|
||||||
var radius := DP.f("roll_hit_radius")
|
var radius := DP.f("roll_hit_radius")
|
||||||
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||||
|
if _roll_hit.has(mat) or not _mat_active(mat):
|
||||||
|
continue
|
||||||
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
|
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
|
||||||
to_mat.y = 0.0
|
to_mat.y = 0.0
|
||||||
if to_mat.length() > radius:
|
if to_mat.length() > radius:
|
||||||
continue
|
continue
|
||||||
|
# Record everyone this pop reaches so the chain never re-picks a downed body.
|
||||||
|
for m: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||||
|
var d: Vector3 = (m as Node3D).global_position - global_position
|
||||||
|
d.y = 0.0
|
||||||
|
if d.length() <= radius and not _roll_hit.has(m):
|
||||||
|
_roll_hit.append(m)
|
||||||
_hit_matadors_radius(radius, DP.f("roll_hit_strength"), DP.f("roll_knockup"))
|
_hit_matadors_radius(radius, DP.f("roll_hit_strength"), DP.f("roll_knockup"))
|
||||||
camera_pivot.call(&"trigger_hit", clampf(speed / 90.0, 0.35, 1.0))
|
camera_pivot.call(&"trigger_hit", clampf(speed / 90.0, 0.35, 1.0))
|
||||||
if _huff_player:
|
if _huff_player:
|
||||||
_huff_player.pitch_scale = randf_range(1.05, 1.35)
|
_huff_player.pitch_scale = randf_range(1.05, 1.35)
|
||||||
_huff_player.play()
|
_huff_player.play()
|
||||||
|
if _roll_chains_left > 0:
|
||||||
|
var next := _roll_pick_target()
|
||||||
|
if next != null:
|
||||||
|
_roll_chains_left -= 1
|
||||||
|
_roll_target = next
|
||||||
|
return true # keep rolling — the ball carries on to the next target
|
||||||
_end_roll()
|
_end_roll()
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
# Closest active matador we haven't already popped this roll, or null if none left.
|
||||||
|
func _roll_pick_target() -> Node3D:
|
||||||
|
var best: Node3D = null
|
||||||
|
var best_d := INF
|
||||||
|
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||||
|
if _roll_hit.has(mat) or not _mat_active(mat):
|
||||||
|
continue
|
||||||
|
var d: float = (mat as Node3D).global_position.distance_squared_to(global_position)
|
||||||
|
if d < best_d:
|
||||||
|
best_d = d
|
||||||
|
best = mat as Node3D
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
func _roll_target_valid() -> bool:
|
||||||
|
return is_instance_valid(_roll_target) and _mat_active(_roll_target)
|
||||||
|
|
||||||
|
|
||||||
|
# A matador still in play — valid, not ragdolled/consumed. Ragdolled bodies report
|
||||||
|
# is_active() == false, so a popped target is skipped on the next chain search.
|
||||||
|
func _mat_active(mat: Node) -> bool:
|
||||||
|
return is_instance_valid(mat) and mat.has_method(&"is_active") and mat.call(&"is_active")
|
||||||
|
|
||||||
|
|
||||||
# End the Powerball early: brake hard (the ball "pops" and unrolls) and drop back to
|
# End the Powerball early: brake hard (the ball "pops" and unrolls) and drop back to
|
||||||
# the normal locomotion path next frame.
|
# the normal locomotion path next frame.
|
||||||
func _end_roll() -> void:
|
func _end_roll() -> void:
|
||||||
@@ -457,11 +570,16 @@ func _end_roll() -> void:
|
|||||||
_active_ability = -1
|
_active_ability = -1
|
||||||
_ability_timer = 0.0
|
_ability_timer = 0.0
|
||||||
_roll_ramp = 0.0
|
_roll_ramp = 0.0
|
||||||
|
_roll_target = null
|
||||||
|
_roll_chains_left = 0
|
||||||
|
_roll_hit.clear()
|
||||||
_legs.set(&"charge_pitch_target", 0.0)
|
_legs.set(&"charge_pitch_target", 0.0)
|
||||||
_legs.set(&"tail_ragdoll", false)
|
_legs.set(&"tail_ragdoll", false)
|
||||||
_legs.set(&"external_rotation", false)
|
_legs.set(&"external_rotation", false)
|
||||||
if _roll_spark_emitter:
|
if _roll_spark_emitter:
|
||||||
_roll_spark_emitter.emitting = false
|
_roll_spark_emitter.emitting = false
|
||||||
|
if _charge_trail_emitter:
|
||||||
|
_charge_trail_emitter.emitting = false
|
||||||
velocity.x *= 0.15
|
velocity.x *= 0.15
|
||||||
velocity.z *= 0.15
|
velocity.z *= 0.15
|
||||||
_play_idle()
|
_play_idle()
|
||||||
@@ -805,6 +923,19 @@ func _make_slam_ring_mesh() -> ArrayMesh:
|
|||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
|
# ── Dead bull ─────────────────────────────────────────────────────────────
|
||||||
|
# The run is over the instant HP hits zero, but the lose screen holds off a
|
||||||
|
# beat before it appears (see HUD._show_game_over). Freeze the carcass through
|
||||||
|
# that delay — no input, no abilities, just coast to a stop under gravity — so a
|
||||||
|
# dead bull can't keep charging around while the screen is pending.
|
||||||
|
if _dead:
|
||||||
|
velocity.x = move_toward(velocity.x, 0.0, 40.0 * delta)
|
||||||
|
velocity.z = move_toward(velocity.z, 0.0, 40.0 * delta)
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity += get_gravity() * delta
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
|
||||||
# ── Gravity ───────────────────────────────────────────────────────────────
|
# ── Gravity ───────────────────────────────────────────────────────────────
|
||||||
if not is_on_floor():
|
if not is_on_floor():
|
||||||
var gravity := get_gravity()
|
var gravity := get_gravity()
|
||||||
@@ -824,6 +955,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
var on_floor := is_on_floor()
|
var on_floor := is_on_floor()
|
||||||
|
|
||||||
# ── Ability cooldowns ────────────────────────────────────────────────────────
|
# ── Ability cooldowns ────────────────────────────────────────────────────────
|
||||||
|
_hit_iframes = maxf(0.0, _hit_iframes - delta)
|
||||||
for i: int in 4:
|
for i: int in 4:
|
||||||
ability_cd[i] = maxf(0.0, ability_cd[i] - delta)
|
ability_cd[i] = maxf(0.0, ability_cd[i] - delta)
|
||||||
if _ability_active:
|
if _ability_active:
|
||||||
@@ -856,7 +988,13 @@ func _physics_process(delta: float) -> void:
|
|||||||
# input is released.
|
# input is released.
|
||||||
var flat_vel := Vector3(velocity.x, 0.0, velocity.z)
|
var flat_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||||
var max_speed := DP.f("max_speed")
|
var max_speed := DP.f("max_speed")
|
||||||
if direction.length_squared() > 0.01:
|
if _active_ability == 1 and _ability_active:
|
||||||
|
# Dash: leave the barrel fast like a bullet, then bleed exponentially back
|
||||||
|
# down to cruise speed (fastest drop right at the start), so it settles into
|
||||||
|
# normal running by the time the clip ends instead of holding top speed.
|
||||||
|
_dash_speed_cur = lerpf(_dash_speed_cur, max_speed, 1.0 - exp(-DP.f("dash_decel") * delta))
|
||||||
|
flat_vel = _dash_dir * _dash_speed_cur
|
||||||
|
elif direction.length_squared() > 0.01:
|
||||||
var cap := maxf(max_speed, flat_vel.length())
|
var cap := maxf(max_speed, flat_vel.length())
|
||||||
flat_vel = flat_vel.move_toward(direction * cap, DP.f("move_accel") * delta)
|
flat_vel = flat_vel.move_toward(direction * cap, DP.f("move_accel") * delta)
|
||||||
else:
|
else:
|
||||||
@@ -871,6 +1009,9 @@ func _physics_process(delta: float) -> void:
|
|||||||
cube_guy.rotation.y = lerp_angle(
|
cube_guy.rotation.y = lerp_angle(
|
||||||
cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))
|
cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))
|
||||||
|
|
||||||
|
# Looping DASH clip while genuinely moving on the ground, idle otherwise.
|
||||||
|
_update_locomotion_anim(on_floor and flat_speed > 1.0)
|
||||||
|
|
||||||
_pre_slide_vel_y = velocity.y
|
_pre_slide_vel_y = velocity.y
|
||||||
var pre_wall_vel := Vector3(velocity.x, 0.0, velocity.z)
|
var pre_wall_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
@@ -925,11 +1066,79 @@ func _physics_process(delta: float) -> void:
|
|||||||
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
# A single clean hit — thrown or swung blade — ends the run. `cause` ("gored" /
|
func get_hp() -> int:
|
||||||
# "thrown") tags how it happened for the death notice. Latches so the lose screen is
|
return _hp
|
||||||
# raised exactly once even if several blades land on the same frame.
|
|
||||||
func take_sword_hit(cause: String = "") -> void:
|
|
||||||
if _dead:
|
func get_max_hp() -> int:
|
||||||
|
return _max_hp
|
||||||
|
|
||||||
|
|
||||||
|
# A clean hit — thrown or swung blade — costs the bull one HP pip; the run ends only
|
||||||
|
# when the last pip is gone. `cause` ("gored" / "thrown") tags how it happened for the
|
||||||
|
# death notice. I-frames absorb a flurry so several blades in one pass drop one pip,
|
||||||
|
# not the whole bar; `_dead` latches so the lose screen is raised exactly once.
|
||||||
|
func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void:
|
||||||
|
if _dead or _hit_iframes > 0.0:
|
||||||
return
|
return
|
||||||
_dead = true
|
_hp -= 1
|
||||||
died.emit(cause)
|
_hit_iframes = DP.f("bull_hit_iframes")
|
||||||
|
health_changed.emit(_hp, _max_hp)
|
||||||
|
hit_taken.emit(cause)
|
||||||
|
camera_pivot.call(&"trigger_hit", 0.7)
|
||||||
|
# A spear gores a spot — blood spurts from the entry wound (where it struck the
|
||||||
|
# bull). Melee gores pass no position and skip the spurt; gore can be turned off.
|
||||||
|
if cause == "thrown" and Settings.gore:
|
||||||
|
_spawn_blood(hit_pos if hit_pos != Vector3.ZERO else global_position)
|
||||||
|
if _huff_player:
|
||||||
|
_huff_player.pitch_scale = randf_range(0.7, 0.9)
|
||||||
|
_huff_player.play()
|
||||||
|
if _hp <= 0:
|
||||||
|
if _bull_anim.current_animation != _ANIM_DEATH:
|
||||||
|
_bull_anim.speed_scale = 0.8
|
||||||
|
_bull_anim.play(_ANIM_DEATH)
|
||||||
|
_dead = true
|
||||||
|
died.emit(cause)
|
||||||
|
|
||||||
|
|
||||||
|
# Blood spurt from a spear wound: a short-lived spray of dark-red gobs flung out of the
|
||||||
|
# entry point, biased outward from the bull's centre (so it reads as bursting from the
|
||||||
|
# hide) with a touch of lift before gravity drags it down.
|
||||||
|
func _spawn_blood(hit_pos: Vector3) -> void:
|
||||||
|
var out := hit_pos - global_position
|
||||||
|
out.y = 0.0
|
||||||
|
out = out.normalized() if out.length() > 0.05 else Vector3(0.0, 0.0, 1.0)
|
||||||
|
var dir := (out + Vector3.UP * 0.8).normalized()
|
||||||
|
|
||||||
|
var sphere := _particle_sphere(0.05, 4, 2, _unshaded_material())
|
||||||
|
|
||||||
|
var ramp := Gradient.new()
|
||||||
|
ramp.set_color(0, Color(0.55, 0.02, 0.02, 1.0))
|
||||||
|
ramp.set_color(1, Color(0.28, 0.0, 0.0, 0.0))
|
||||||
|
|
||||||
|
var scale_curve := Curve.new()
|
||||||
|
scale_curve.add_point(Vector2(0.0, 1.0))
|
||||||
|
scale_curve.add_point(Vector2(0.6, 0.7))
|
||||||
|
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||||
|
|
||||||
|
var p := CPUParticles3D.new()
|
||||||
|
p.one_shot = true
|
||||||
|
p.explosiveness = 0.9
|
||||||
|
p.amount = 34
|
||||||
|
p.lifetime = 0.7
|
||||||
|
p.randomness = 0.5
|
||||||
|
p.local_coords = false
|
||||||
|
p.direction = dir
|
||||||
|
p.spread = 42.0
|
||||||
|
p.gravity = Vector3(0.0, -14.0, 0.0)
|
||||||
|
p.initial_velocity_min = 3.5
|
||||||
|
p.initial_velocity_max = 8.0
|
||||||
|
p.scale_amount_min = 0.5
|
||||||
|
p.scale_amount_max = 1.6
|
||||||
|
p.mesh = sphere
|
||||||
|
p.color_ramp = ramp
|
||||||
|
p.scale_amount_curve = scale_curve
|
||||||
|
get_parent().add_child(p)
|
||||||
|
p.global_position = hit_pos
|
||||||
|
p.restart()
|
||||||
|
_free_after(p, 1.5)
|
||||||
|
|||||||
+21
-7
@@ -11,10 +11,12 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Bullosseum"
|
config/name="Bullosseum"
|
||||||
config/version="0.1.0"
|
config/version="0.2.0"
|
||||||
run/main_scene="res://MainMenu.tscn"
|
run/main_scene="res://MainMenu.tscn"
|
||||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||||
config/icon="res://icon.svg"
|
boot_splash/bg_color=Color(0, 0, 0, 1)
|
||||||
|
boot_splash/image="res://Assets/loading_screen.png"
|
||||||
|
config/icon="uid://dct1d88g015el"
|
||||||
config/run/main_scene="res://MainMenu.tscn"
|
config/run/main_scene="res://MainMenu.tscn"
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
@@ -23,6 +25,12 @@ DP="*res://debug_params.gd"
|
|||||||
Console="*res://console.gd"
|
Console="*res://console.gd"
|
||||||
Controls="*res://controls_manager.gd"
|
Controls="*res://controls_manager.gd"
|
||||||
DebugDraw="*res://debug_draw.gd"
|
DebugDraw="*res://debug_draw.gd"
|
||||||
|
Settings="*res://settings.gd"
|
||||||
|
Run="*res://levels/run_state.gd"
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/mode=3
|
||||||
|
|
||||||
[editor_plugins]
|
[editor_plugins]
|
||||||
|
|
||||||
@@ -85,13 +93,13 @@ ability_kick={
|
|||||||
}
|
}
|
||||||
ability_slam={
|
ability_slam={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":104,"location":0,"echo":false,"script":null)
|
||||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null)
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
ability_dash={
|
ability_dash={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":104,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
|
||||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null)
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -102,11 +110,17 @@ ability_roll={
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[input_devices]
|
||||||
|
|
||||||
|
pointing/emulate_touch_from_mouse=true
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
3d/physics_engine="Jolt Physics"
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|
||||||
[rendering]
|
[rendering]
|
||||||
|
|
||||||
rendering_device/driver.windows="d3d12"
|
rendering_device/driver.windows="opengl3"
|
||||||
anti_aliasing/quality/use_taa=true
|
renderer/rendering_method="gl_compatibility"
|
||||||
|
textures/vram_compression/import_s3tc_bptc=true
|
||||||
|
textures/vram_compression/import_etc2_astc=true
|
||||||
|
|||||||
+5
-1
@@ -8,7 +8,7 @@ SKIP_GAMEPLAY=0
|
|||||||
for arg in "$@"; do [[ "$arg" == "--no-gameplay" ]] && SKIP_GAMEPLAY=1; done
|
for arg in "$@"; do [[ "$arg" == "--no-gameplay" ]] && SKIP_GAMEPLAY=1; done
|
||||||
|
|
||||||
echo "=== gdlint ==="
|
echo "=== gdlint ==="
|
||||||
gdlint *.gd tests/*.gd
|
gdlint *.gd levels/*.gd tests/*.gd
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Logic tests ==="
|
echo "=== Logic tests ==="
|
||||||
@@ -18,6 +18,10 @@ echo ""
|
|||||||
echo "=== Console focus tests ==="
|
echo "=== Console focus tests ==="
|
||||||
"$GODOT" --headless --script tests/console_focus_test.gd
|
"$GODOT" --headless --script tests/console_focus_test.gd
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Level switch tests (no cross-level geometry/collider leaks) ==="
|
||||||
|
"$GODOT" --headless --script tests/level_switch_test.gd
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Performance tests (frame-budget regression guard) ==="
|
echo "=== Performance tests (frame-budget regression guard) ==="
|
||||||
"$GODOT" --headless --script tests/performance_test.gd
|
"$GODOT" --headless --script tests/performance_test.gd
|
||||||
|
|||||||
+61
-416
@@ -1,18 +1,10 @@
|
|||||||
[gd_scene format=3 uid="uid://b7afbx36ltb3f"]
|
[gd_scene load_steps=8 format=3 uid="uid://b7afbx36ltb3f"]
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://crtm0xtiql3r8" path="res://Player.tscn" id="1_ulcgi"]
|
[ext_resource type="PackedScene" uid="uid://crtm0xtiql3r8" path="res://Player.tscn" id="1_ulcgi"]
|
||||||
[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://matador_spawn.gd" id="3_spawn"]
|
|
||||||
[ext_resource type="Shader" uid="uid://d3xyyy5bh0ma" path="res://ps1_filter.gdshader" id="4_ps1shader"]
|
[ext_resource type="Shader" uid="uid://d3xyyy5bh0ma" path="res://ps1_filter.gdshader" id="4_ps1shader"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cmxqocsdbgufr" path="res://Animations/mees_plaksutab.glb" id="5_m7jmp"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://1y07gn65ktsi" path="res://Animations/ANIM_publik_mees.glb" id="7_xlvrw"]
|
|
||||||
[ext_resource type="Script" uid="uid://dr6phggarm73e" path="res://Animations/anim_publik_mees.gd" id="8_73fnb"]
|
|
||||||
[ext_resource type="Script" uid="uid://di0jafakftp3n" path="res://publikum_controller.gd" id="9_pubctrl"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://dxe72ig8gfbcg" path="res://Assets/arena model2026.glb" id="10_5juve"]
|
|
||||||
[ext_resource type="Script" uid="uid://butd6b3tdwxrv" path="res://hud.gd" id="10_hud"]
|
[ext_resource type="Script" uid="uid://butd6b3tdwxrv" path="res://hud.gd" id="10_hud"]
|
||||||
[ext_resource type="Script" path="res://crowd_billboards.gd" id="11_crowd"]
|
[ext_resource type="Script" uid="uid://ce5n4qu88mhyx" path="res://ps1_filter.gd" id="12_ps1ctl"]
|
||||||
[ext_resource type="Script" path="res://ps1_filter.gd" id="12_ps1ctl"]
|
[ext_resource type="Script" path="res://levels/game_shell.gd" id="13_shell"]
|
||||||
|
|
||||||
[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_73fnb"]
|
[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_73fnb"]
|
||||||
dof_blur_far_enabled = true
|
dof_blur_far_enabled = true
|
||||||
@@ -23,8 +15,6 @@ dof_blur_near_distance = 8192.0
|
|||||||
dof_blur_near_transition = 8192.0
|
dof_blur_near_transition = 8192.0
|
||||||
dof_blur_amount = 1.0
|
dof_blur_amount = 1.0
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ulcgi"]
|
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_ps1"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_ps1"]
|
||||||
shader = ExtResource("4_ps1shader")
|
shader = ExtResource("4_ps1shader")
|
||||||
shader_parameter/pixel_scale = 4.0
|
shader_parameter/pixel_scale = 4.0
|
||||||
@@ -34,429 +24,84 @@ shader_parameter/scanline_strength = 0.2
|
|||||||
shader_parameter/roll_speed = 3.0
|
shader_parameter/roll_speed = 3.0
|
||||||
shader_parameter/roll_strength = 1.0
|
shader_parameter/roll_strength = 1.0
|
||||||
|
|
||||||
[sub_resource type="Environment" id="Environment_73fnb"]
|
[node name="scene" type="Node3D"]
|
||||||
|
script = ExtResource("13_shell")
|
||||||
|
|
||||||
[node name="scene" type="Node3D" unique_id=22320312]
|
[node name="Player" parent="." instance=ExtResource("1_ulcgi")]
|
||||||
|
|
||||||
[node name="Player" parent="." unique_id=165510044 instance=ExtResource("1_ulcgi")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.2416196, 5.1711025, -4.841382)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.2416196, 5.1711025, -4.841382)
|
||||||
|
|
||||||
[node name="Camera3D" parent="Player" index="3" unique_id=1120161087]
|
[node name="Camera3D" parent="Player" index="3"]
|
||||||
attributes = SubResource("CameraAttributesPractical_73fnb")
|
attributes = SubResource("CameraAttributesPractical_73fnb")
|
||||||
fov = 50.0
|
fov = 50.0
|
||||||
near = 2.636
|
near = 2.636
|
||||||
far = 0.01
|
far = 0.01
|
||||||
|
|
||||||
[node name="Node3D" type="Node3D" parent="." unique_id=1081100876]
|
[node name="Skeleton3D" parent="Player/bull/Armature" index="0"]
|
||||||
|
bones/0/position = Vector3(0, 0, 0.0074015963)
|
||||||
|
bones/0/rotation = Quaternion(-0.36922705, 0.6030517, -0.6030517, -0.36922705)
|
||||||
|
bones/0/scale = Vector3(1, 1, 1)
|
||||||
|
bones/1/position = Vector3(0.003526625, -0.0043296535, 0.004082799)
|
||||||
|
bones/1/rotation = Quaternion(-0.00038647576, 0.06909673, 0.9868542, -0.14609681)
|
||||||
|
bones/1/scale = Vector3(1.0000021, 1.0000001, 1.0000006)
|
||||||
|
bones/2/position = Vector3(-5.853537e-11, 0.002669535, -1.489375e-10)
|
||||||
|
bones/2/rotation = Quaternion(-0.06929122, 3.2415448e-07, -0.11267061, 0.99121344)
|
||||||
|
bones/2/scale = Vector3(1, 1.0000001, 0.9999999)
|
||||||
|
bones/4/position = Vector3(0.004535823, -0.0021376952, -0.0042386237)
|
||||||
|
bones/4/rotation = Quaternion(-0.24948508, 0.19590344, 0.9416978, -0.11217982)
|
||||||
|
bones/4/scale = Vector3(0.99999994, 0.99999994, 1.0000002)
|
||||||
|
bones/5/position = Vector3(1.264555e-10, 0.0028599252, -2.4303243e-10)
|
||||||
|
bones/5/rotation = Quaternion(-0.58943665, -0.36607245, -0.2256448, 0.683842)
|
||||||
|
bones/6/position = Vector3(-7.092306e-11, 0.003274492, 4.6413304e-10)
|
||||||
|
bones/6/rotation = Quaternion(0.52910197, 0.15997829, 0.12024502, 0.82462066)
|
||||||
|
bones/6/scale = Vector3(1, 1.0000001, 1.0000001)
|
||||||
|
bones/8/position = Vector3(2.259811e-19, 0.0010177285, -0.008495503)
|
||||||
|
bones/8/rotation = Quaternion(-0.70710677, 0, 0, 0.70710677)
|
||||||
|
bones/8/scale = Vector3(1, 1, 1)
|
||||||
|
bones/9/position = Vector3(4.814825e-35, 0.0017711426, 0)
|
||||||
|
bones/10/position = Vector3(4.814825e-35, 0.0018082206, 0)
|
||||||
|
bones/11/position = Vector3(0, 0.0019911951, 0)
|
||||||
|
bones/11/scale = Vector3(1, 1, 1)
|
||||||
|
bones/12/position = Vector3(0, 0.002023485, 0)
|
||||||
|
bones/12/scale = Vector3(1, 1, 1)
|
||||||
|
bones/13/position = Vector3(0, 0.0016682985, 0)
|
||||||
|
bones/13/scale = Vector3(1, 1, 1)
|
||||||
|
bones/14/position = Vector3(0, 0.0014960878, 0)
|
||||||
|
bones/14/scale = Vector3(1, 1, 1)
|
||||||
|
bones/15/position = Vector3(0, 0.0010996431, 0)
|
||||||
|
bones/17/position = Vector3(0, 0.000937381, 0.0075526168)
|
||||||
|
bones/17/rotation = Quaternion(-5.3385083e-08, 0.70710677, 0.70710677, -5.3385083e-08)
|
||||||
|
bones/19/position = Vector3(-0.003526625, -0.0043296535, 0.004082799)
|
||||||
|
bones/19/rotation = Quaternion(0.00038647576, 0.06909673, 0.9868542, 0.14609681)
|
||||||
|
bones/19/scale = Vector3(1.0000021, 1.0000001, 1.0000006)
|
||||||
|
bones/20/position = Vector3(5.853537e-11, 0.002669535, -1.489375e-10)
|
||||||
|
bones/20/rotation = Quaternion(-0.06929122, -3.2415448e-07, 0.11267061, 0.99121344)
|
||||||
|
bones/20/scale = Vector3(1, 1.0000001, 0.9999999)
|
||||||
|
bones/22/position = Vector3(-0.004535823, -0.0021376952, -0.0042386237)
|
||||||
|
bones/22/rotation = Quaternion(0.24948508, 0.19590344, 0.9416978, 0.11217982)
|
||||||
|
bones/22/scale = Vector3(0.99999994, 0.99999994, 1.0000002)
|
||||||
|
bones/23/position = Vector3(-1.264555e-10, 0.0028599252, -2.4303243e-10)
|
||||||
|
bones/23/rotation = Quaternion(-0.58943665, 0.36607245, 0.2256448, 0.683842)
|
||||||
|
bones/23/scale = Vector3(1, 1, 1)
|
||||||
|
bones/24/position = Vector3(7.092306e-11, 0.003274492, 4.6413304e-10)
|
||||||
|
bones/24/rotation = Quaternion(0.52910197, -0.15997829, -0.12024502, 0.82462066)
|
||||||
|
bones/24/scale = Vector3(1, 1.0000001, 1.0000001)
|
||||||
|
|
||||||
[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=2072095624]
|
[node name="HUD" type="CanvasLayer" parent="."]
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=1444198204]
|
|
||||||
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=1341659233 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="Arena2" parent="Node3D/arena_placeholder_v01" unique_id=524718167 instance=ExtResource("3_fy5k1")]
|
|
||||||
|
|
||||||
[node name="arena model2026" parent="Node3D/arena_placeholder_v01" unique_id=179544183 instance=ExtResource("10_5juve")]
|
|
||||||
transform = Transform3D(0.99999994, 0, 0, 0, 0.99999994, 0, 0, 0, 0.99999994, -0.901, -0.6, 0)
|
|
||||||
visible = false
|
|
||||||
|
|
||||||
[node name="MatadorSpawner" type="Node3D" parent="." unique_id=-1293967295]
|
|
||||||
script = ExtResource("3_spawn")
|
|
||||||
|
|
||||||
[node name="HUD" type="CanvasLayer" parent="." unique_id=1100000001]
|
|
||||||
script = ExtResource("10_hud")
|
script = ExtResource("10_hud")
|
||||||
|
|
||||||
[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=1032421313]
|
[node name="CanvasLayer" type="CanvasLayer" parent="."]
|
||||||
layer = 10
|
layer = 10
|
||||||
|
|
||||||
[node name="PS1Filter" type="ColorRect" parent="CanvasLayer" unique_id=1449116873]
|
[node name="PS1Filter" type="ColorRect" parent="CanvasLayer"]
|
||||||
material = SubResource("ShaderMaterial_ps1")
|
material = SubResource("ShaderMaterial_ps1")
|
||||||
script = ExtResource("12_ps1ctl")
|
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
|
script = ExtResource("12_ps1ctl")
|
||||||
|
|
||||||
[node name="Lights" type="Node3D" parent="." unique_id=737767662]
|
[node name="LevelRoot" type="Node3D" parent="."]
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=818712731]
|
|
||||||
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=918477294]
|
|
||||||
transform = Transform3D(0.904133, 0.30962405, -0.2944087, 0.3084627, 0.0037425563, 0.9512291, 0.2956252, -0.95085174, -0.0921237, -3.629381, 8.186143, -2.0124264)
|
|
||||||
visible = false
|
|
||||||
light_energy = 14.585
|
|
||||||
distance_fade_enabled = true
|
|
||||||
spot_range = 32.90586
|
|
||||||
spot_angle = 73.93
|
|
||||||
|
|
||||||
[node name="SpotLight3D" type="SpotLight3D" parent="Lights" unique_id=422808558]
|
|
||||||
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=1706919950]
|
|
||||||
transform = Transform3D(0.45811793, 0.34843865, -0.81775206, 0.5316611, 0.62986195, 0.56622505, 0.7123655, -0.69416475, 0.10329977, 0, 8.980043, 0)
|
|
||||||
light_energy = 2.988
|
|
||||||
shadow_enabled = true
|
|
||||||
shadow_blur = 10.0
|
|
||||||
|
|
||||||
[node name="PublikNode" type="Node3D" parent="." unique_id=1020007445]
|
|
||||||
visible = false
|
|
||||||
|
|
||||||
[node name="publikum2" type="Node3D" parent="PublikNode" unique_id=813507078]
|
|
||||||
transform = Transform3D(-0.91522944, 0, -0.4029331, 0, 1, 0, 0.4029331, 0, -0.91522944, 4.195425, 3.6553268, 13.885913)
|
|
||||||
visible = false
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum2" unique_id=1052467743 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum2" unique_id=1015461330 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum2" unique_id=1089134459 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum2" unique_id=322028795 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum2" unique_id=482889095 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum2" unique_id=944445851 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum4" type="Node3D" parent="PublikNode" unique_id=68365083]
|
|
||||||
transform = Transform3D(-0.629974, 0, -0.77661633, 0, 1, 0, 0.77661633, 0, -0.629974, 9.842437, 3.6553268, 10.401334)
|
|
||||||
visible = false
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum4" unique_id=222379180 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum4" unique_id=1871765797 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum4" unique_id=10437017 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum4" unique_id=901414927 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum4" unique_id=2077358822 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum4" unique_id=1131238522 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum5" type="Node3D" parent="PublikNode" unique_id=1254884314]
|
|
||||||
transform = Transform3D(-0.39064395, 0, 0.920542, 0, 1, 0, -0.920542, 0, -0.39064395, -14.538336, 3.6553268, 3.9378712)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum5" unique_id=1914156014 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum5" unique_id=602140062 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum5" unique_id=224404749 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum5" unique_id=958300982 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum5" unique_id=860025476 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum5" unique_id=1052988416 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum6" type="Node3D" parent="PublikNode" unique_id=709827517]
|
|
||||||
transform = Transform3D(-0.7681126, 0, 0.64031476, 0, 1, 0, -0.64031476, 0, -0.7681126, -11.103292, 3.6553288, 9.335619)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum6" unique_id=1007939702 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum6" unique_id=860884329 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum6" unique_id=1928974579 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum6" unique_id=547889733 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum6" unique_id=1501877017 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum6" unique_id=1951764566 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum7" type="Node3D" parent="PublikNode" unique_id=1483978907]
|
|
||||||
transform = Transform3D(0.55061686, 0, -0.8347584, 0, 1, 0, 0.8347584, 0, 0.55061686, 13.163043, 3.4379854, -5.268345)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum7" unique_id=788603784 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum7" unique_id=1537286096 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum7" unique_id=1957279061 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum7" unique_id=1379894897 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum7" unique_id=1920281402 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum7" unique_id=811906038 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum8" type="Node3D" parent="PublikNode" unique_id=1073953916]
|
|
||||||
transform = Transform3D(0.85247177, 0, -0.5227735, 0, 1, 0, 0.5227735, 0, 0.85247177, 11.217102, 4.568224, -10.151715)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum8" unique_id=641607406 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum8" unique_id=740439664 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum8" unique_id=1896496635 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum8" unique_id=1787601370 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum8" unique_id=1153730967 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum8" unique_id=1023874478 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum3" type="Node3D" parent="PublikNode" unique_id=71842083]
|
|
||||||
transform = Transform3D(0.8698311, 0, 0.49334967, 0, 1, 0, -0.49334967, 0, 0.8698311, -4.8910522, 4.627474, -14.224113)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum3" unique_id=1585155222 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum3" unique_id=1301960990 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum3" unique_id=1481094020 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum3" unique_id=1958614289 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum3" unique_id=318927325 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum3" unique_id=492002897 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum9" type="Node3D" parent="PublikNode" unique_id=1844757155]
|
|
||||||
transform = Transform3D(0.9760592, 0, 0.21750516, 0, 1, 0, -0.21750516, 0, 0.9760592, -2.1318102, 3.6553268, -13.557504)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum9" unique_id=896469763 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum9" unique_id=1412552760 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum9" unique_id=59611810 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum9" unique_id=1161683450 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum9" unique_id=1706050589 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum9" unique_id=1929239115 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum11" type="Node3D" parent="PublikNode" unique_id=727404993]
|
|
||||||
transform = Transform3D(-0.95107377, 0, -0.30896416, 0, 1, 0, 0.30896416, 0, -0.95107377, 2.4321594, 3.6553268, 14.119543)
|
|
||||||
visible = false
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum11" unique_id=36716174 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum11" unique_id=2117684869 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum11" unique_id=684167492 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum11" unique_id=678520535 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum11" unique_id=1746700688 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum11" unique_id=462492236 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum13" type="Node3D" parent="PublikNode" unique_id=1816403209]
|
|
||||||
transform = Transform3D(-0.2962035, 0, 0.95512486, 0, 1, 0, -0.95512486, 0, -0.2962035, -14.037817, 3.6553268, 2.0647745)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum13" unique_id=1252057421 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum13" unique_id=1096867731 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.1134007, 0, 0.6326933)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum13" unique_id=1130631283 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum13" unique_id=2008334523 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum13" unique_id=1185367656 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum13" unique_id=649140591 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="publikum15" type="Node3D" parent="PublikNode" unique_id=926602558]
|
|
||||||
transform = Transform3D(0.43092924, 0, -0.90238583, 0, 1, 0, 0.90238583, 0, 0.43092924, 14.766085, 4.568224, -2.378694)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum15" unique_id=2043094085 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum15" unique_id=1927429095 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum15" unique_id=1660696957 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum15" unique_id=1684596154 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum15" unique_id=1401913487 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum15" unique_id=1893327164 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="ANIM_publik_mees" parent="PublikNode" unique_id=413106028 instance=ExtResource("7_xlvrw")]
|
|
||||||
transform = Transform3D(-0.9976194, 0, 0.0689602, 0, 1, 0, -0.0689602, 0, -0.9976194, -2.9310942, 4.1751385, 14.861324)
|
|
||||||
script = ExtResource("8_73fnb")
|
|
||||||
|
|
||||||
[node name="publikum" type="Node3D" parent="PublikNode" unique_id=1311444475]
|
|
||||||
transform = Transform3D(0.70069104, 0, 0.7134648, 0, 1, 0, -0.7134648, 0, 0.70069104, -9.04294, 3.6553268, -10.339719)
|
|
||||||
script = ExtResource("9_pubctrl")
|
|
||||||
|
|
||||||
[node name="mees_plaksutab" parent="PublikNode/publikum" unique_id=1787142704 instance=ExtResource("5_m7jmp")]
|
|
||||||
|
|
||||||
[node name="mees_plaksutab2" parent="PublikNode/publikum" unique_id=330098781 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab3" parent="PublikNode/publikum" unique_id=1406686735 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab4" parent="PublikNode/publikum" unique_id=132494522 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab5" parent="PublikNode/publikum" unique_id=2101925215 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595)
|
|
||||||
|
|
||||||
[node name="mees_plaksutab6" parent="PublikNode/publikum" unique_id=403179328 instance=ExtResource("5_m7jmp")]
|
|
||||||
transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679)
|
|
||||||
|
|
||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=755853425]
|
|
||||||
environment = SubResource("Environment_73fnb")
|
|
||||||
|
|
||||||
[node name="CrowdBillboards" type="MultiMeshInstance3D" parent="."]
|
|
||||||
script = ExtResource("11_crowd")
|
|
||||||
|
|
||||||
[editable path="Player"]
|
[editable path="Player"]
|
||||||
[editable path="Player/bull"]
|
[editable path="Player/bull"]
|
||||||
[editable path="Node3D/arena_placeholder_v01"]
|
|
||||||
[editable path="PublikNode/publikum2/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum2/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum2/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum2/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum2/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum2/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum4/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum4/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum4/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum4/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum4/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum4/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum5/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum5/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum5/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum5/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum5/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum5/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum6/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum6/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum6/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum6/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum6/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum6/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum7/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum7/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum7/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum7/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum7/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum7/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum8/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum8/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum8/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum8/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum8/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum8/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum3/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum3/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum3/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum3/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum3/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum3/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum9/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum9/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum9/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum9/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum9/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum9/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum11/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum11/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum11/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum11/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum11/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum11/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum13/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum13/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum13/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum13/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum13/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum13/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/publikum15/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum15/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum15/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum15/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum15/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum15/mees_plaksutab6"]
|
|
||||||
[editable path="PublikNode/ANIM_publik_mees"]
|
|
||||||
[editable path="PublikNode/publikum/mees_plaksutab"]
|
|
||||||
[editable path="PublikNode/publikum/mees_plaksutab2"]
|
|
||||||
[editable path="PublikNode/publikum/mees_plaksutab3"]
|
|
||||||
[editable path="PublikNode/publikum/mees_plaksutab4"]
|
|
||||||
[editable path="PublikNode/publikum/mees_plaksutab5"]
|
|
||||||
[editable path="PublikNode/publikum/mees_plaksutab6"]
|
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
extends Node
|
||||||
|
## Player-facing game options (autoload `Settings`), persisted to user://settings.cfg.
|
||||||
|
## Distinct from DP (debug tunables) and Controls (keybinds): this is the small set of
|
||||||
|
## comfort/accessibility toggles the options menu exposes to the player.
|
||||||
|
|
||||||
|
signal changed(key: String, value: Variant)
|
||||||
|
|
||||||
|
const SAVE_PATH := "user://settings.cfg"
|
||||||
|
const SECTION := "options"
|
||||||
|
|
||||||
|
## Master screen-shake toggle — gates every camera_iso.trigger_hit (hits and ability FX).
|
||||||
|
var screen_shake: bool = true
|
||||||
|
## Gore toggle — when off, no blood spawns (impact spurt + ongoing spear-wound bleed).
|
||||||
|
var gore: bool = true
|
||||||
|
|
||||||
|
## Audio volumes stored as linear 0–1. Applied to AudioServer buses on load/change.
|
||||||
|
var volume_master: float = 1.0
|
||||||
|
var volume_ambient: float = 1.0
|
||||||
|
var volume_effects: float = 1.0
|
||||||
|
var mute_all: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_ensure_buses()
|
||||||
|
load_saved()
|
||||||
|
|
||||||
|
|
||||||
|
## Ensure Ambient and Effects buses exist as children of Master.
|
||||||
|
func _ensure_buses() -> void:
|
||||||
|
for bus_name: String in ["Ambient", "Effects"]:
|
||||||
|
if AudioServer.get_bus_index(bus_name) == -1:
|
||||||
|
AudioServer.add_bus()
|
||||||
|
var idx := AudioServer.bus_count - 1
|
||||||
|
AudioServer.set_bus_name(idx, bus_name)
|
||||||
|
AudioServer.set_bus_send(idx, &"Master")
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_audio() -> void:
|
||||||
|
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), _vol_db(volume_master))
|
||||||
|
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Ambient"), _vol_db(volume_ambient))
|
||||||
|
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Effects"), _vol_db(volume_effects))
|
||||||
|
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), mute_all)
|
||||||
|
|
||||||
|
|
||||||
|
func _vol_db(linear: float) -> float:
|
||||||
|
return linear_to_db(maxf(linear, 0.0001))
|
||||||
|
|
||||||
|
|
||||||
|
func set_screen_shake(value: bool) -> void:
|
||||||
|
if screen_shake == value:
|
||||||
|
return
|
||||||
|
screen_shake = value
|
||||||
|
changed.emit("screen_shake", value)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func set_gore(value: bool) -> void:
|
||||||
|
if gore == value:
|
||||||
|
return
|
||||||
|
gore = value
|
||||||
|
changed.emit("gore", value)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func set_volume_master(value: float) -> void:
|
||||||
|
volume_master = clampf(value, 0.0, 1.0)
|
||||||
|
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), _vol_db(volume_master))
|
||||||
|
changed.emit("volume_master", volume_master)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func set_volume_ambient(value: float) -> void:
|
||||||
|
volume_ambient = clampf(value, 0.0, 1.0)
|
||||||
|
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Ambient"), _vol_db(volume_ambient))
|
||||||
|
changed.emit("volume_ambient", volume_ambient)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func set_volume_effects(value: float) -> void:
|
||||||
|
volume_effects = clampf(value, 0.0, 1.0)
|
||||||
|
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Effects"), _vol_db(volume_effects))
|
||||||
|
changed.emit("volume_effects", volume_effects)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func set_mute_all(value: bool) -> void:
|
||||||
|
if mute_all == value:
|
||||||
|
return
|
||||||
|
mute_all = value
|
||||||
|
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), mute_all)
|
||||||
|
changed.emit("mute_all", value)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func save() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
cfg.set_value(SECTION, "screen_shake", screen_shake)
|
||||||
|
cfg.set_value(SECTION, "gore", gore)
|
||||||
|
cfg.set_value(SECTION, "volume_master", volume_master)
|
||||||
|
cfg.set_value(SECTION, "volume_ambient", volume_ambient)
|
||||||
|
cfg.set_value(SECTION, "volume_effects", volume_effects)
|
||||||
|
cfg.set_value(SECTION, "mute_all", mute_all)
|
||||||
|
cfg.save(SAVE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
func load_saved() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(SAVE_PATH) != OK:
|
||||||
|
_apply_audio()
|
||||||
|
return
|
||||||
|
screen_shake = cfg.get_value(SECTION, "screen_shake", true) as bool
|
||||||
|
gore = cfg.get_value(SECTION, "gore", true) as bool
|
||||||
|
volume_master = cfg.get_value(SECTION, "volume_master", 1.0) as float
|
||||||
|
volume_ambient = cfg.get_value(SECTION, "volume_ambient", 1.0) as float
|
||||||
|
volume_effects = cfg.get_value(SECTION, "volume_effects", 1.0) as float
|
||||||
|
mute_all = cfg.get_value(SECTION, "mute_all", false) as bool
|
||||||
|
_apply_audio()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dyspgr7m238la
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
class_name SpearProjectile
|
||||||
|
extends RigidBody3D
|
||||||
|
|
||||||
|
# A thrown spear. It flies a ballistic arc (gravity does the work — the launch
|
||||||
|
# velocity is solved by the matador to land on the bull) and keeps its shaft aimed
|
||||||
|
# along its own velocity, so it noses over at the apex and drives in point-first.
|
||||||
|
# On impact it embeds where it struck: the tip buried just past the contact point
|
||||||
|
# with the shaft trailing back along the flight line, so it reads as stuck-in rather
|
||||||
|
# than floating alongside. A hit on the bull leaves the spear riding it like a spine
|
||||||
|
# (see shed_all, which the bull's slam calls to fling them all off); other hits pin
|
||||||
|
# it in place until the despawn timer clears it.
|
||||||
|
|
||||||
|
const _DESPAWN_SEC: float = 8.0
|
||||||
|
const _STUCK_GROUP: StringName = &"stuck_spear"
|
||||||
|
# Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin.
|
||||||
|
const _TIP_Z: float = 1.915
|
||||||
|
const _EMBED_DEPTH: float = 0.72 # how far past the surface the tip buries
|
||||||
|
# Bone the bull's spears ride on. The collision spheres sit outside the hide, so a
|
||||||
|
# spear stuck to the body root reads as floating; anchoring to COG also lets it bob
|
||||||
|
# and rotate with the animated body instead of only the character root.
|
||||||
|
const _COG_BONE: String = "COG"
|
||||||
|
const _ANCHOR_NAME: String = "SpearAnchor"
|
||||||
|
|
||||||
|
var _mesh: Node3D = null
|
||||||
|
var _stopped: bool = false
|
||||||
|
var _flight_dir: Vector3 = Vector3.ZERO # last pre-impact travel direction
|
||||||
|
|
||||||
|
|
||||||
|
# Build, seat and launch a spear from an already-instantiated mesh (the one that was
|
||||||
|
# in the matador's hand). `origin` is the release point; `velocity` is the full
|
||||||
|
# ballistic launch vector; `gravity` is the RigidBody gravity_scale for the arc.
|
||||||
|
static func launch(parent: Node, mesh: Node3D, origin: Vector3, velocity: Vector3,
|
||||||
|
gravity: float) -> SpearProjectile:
|
||||||
|
var s := SpearProjectile.new()
|
||||||
|
s.gravity_scale = gravity
|
||||||
|
s.collision_layer = 0
|
||||||
|
s.collision_mask = 1
|
||||||
|
s.contact_monitor = true
|
||||||
|
s.max_contacts_reported = 6
|
||||||
|
parent.add_child(s)
|
||||||
|
# Seat the body so its local +Z (the shaft, see spear.glb) points along the throw.
|
||||||
|
s.global_transform = Transform3D(_aim_basis(velocity), origin)
|
||||||
|
mesh.reparent(s, false)
|
||||||
|
mesh.transform = Transform3D.IDENTITY
|
||||||
|
s._mesh = mesh
|
||||||
|
s.add_child(_shaft_collider())
|
||||||
|
s.linear_velocity = velocity
|
||||||
|
_despawn(s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# Shed every spear stuck in `bull` — pop each off as a tumbling body that flies clear
|
||||||
|
# and clears itself on the despawn timer. Called by the bull's slam.
|
||||||
|
static func shed_all(bull: Node) -> void:
|
||||||
|
if not (bull is Node3D):
|
||||||
|
return
|
||||||
|
var scene: Node = bull.get_tree().current_scene
|
||||||
|
if scene == null:
|
||||||
|
scene = bull
|
||||||
|
var center: Vector3 = (bull as Node3D).global_position
|
||||||
|
# Stuck spears now live under a BoneAttachment3D deep in the skeleton, so walk the
|
||||||
|
# whole subtree by group rather than only the bull's direct children.
|
||||||
|
for node in bull.get_tree().get_nodes_in_group(_STUCK_GROUP):
|
||||||
|
if not (bull as Node).is_ancestor_of(node):
|
||||||
|
continue
|
||||||
|
var mesh := node as Node3D
|
||||||
|
var gx := mesh.global_transform
|
||||||
|
var rb := RigidBody3D.new()
|
||||||
|
rb.collision_layer = 0
|
||||||
|
rb.collision_mask = 1
|
||||||
|
scene.add_child(rb)
|
||||||
|
rb.global_transform = gx
|
||||||
|
mesh.reparent(rb, false)
|
||||||
|
mesh.transform = Transform3D.IDENTITY
|
||||||
|
mesh.remove_from_group(_STUCK_GROUP)
|
||||||
|
rb.add_child(_shaft_collider())
|
||||||
|
# Fling outward from the bull with a bit of lift and spin.
|
||||||
|
var out := gx.origin - center
|
||||||
|
out.y = 0.0
|
||||||
|
out = out.normalized() if out.length() > 0.01 else Vector3(randf() - 0.5, 0.0, randf() - 0.5)
|
||||||
|
rb.linear_velocity = out * randf_range(3.0, 6.0) + Vector3.UP * randf_range(3.0, 5.0)
|
||||||
|
rb.angular_velocity = Vector3(randf_range(-10.0, 10.0),
|
||||||
|
randf_range(-10.0, 10.0), randf_range(-10.0, 10.0))
|
||||||
|
_despawn(rb)
|
||||||
|
|
||||||
|
|
||||||
|
static func _shaft_collider() -> CollisionShape3D:
|
||||||
|
# Collider along local +Z (mesh runs z≈-0.34 … 1.92), fattened so a fast throw
|
||||||
|
# can't tunnel past the bull's collision spheres.
|
||||||
|
var cap := CapsuleShape3D.new()
|
||||||
|
cap.radius = 0.13
|
||||||
|
cap.height = 2.2
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
cs.shape = cap
|
||||||
|
cs.position = Vector3(0.0, 0.0, 0.79)
|
||||||
|
cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
|
||||||
|
return cs
|
||||||
|
|
||||||
|
|
||||||
|
# Continuous bleed from a spear lodged in the bull: a steady spurt of dark-red gobs out
|
||||||
|
# of the entry wound. Parented to the stuck mesh so it tracks the wound as the bull
|
||||||
|
# moves; world-space particles (local_coords off) so they arc off and fall behind as a
|
||||||
|
# trail. Dies with the mesh when the spear is shed or despawned.
|
||||||
|
static func _wound_emitter() -> CPUParticles3D:
|
||||||
|
var ramp := Gradient.new()
|
||||||
|
ramp.set_color(0, Color(0.5, 0.02, 0.02, 1.0))
|
||||||
|
ramp.set_color(1, Color(0.22, 0.0, 0.0, 0.0))
|
||||||
|
|
||||||
|
var sc := Curve.new()
|
||||||
|
sc.add_point(Vector2(0.0, 1.0))
|
||||||
|
sc.add_point(Vector2(0.6, 0.7))
|
||||||
|
sc.add_point(Vector2(1.0, 0.0))
|
||||||
|
|
||||||
|
var p := CPUParticles3D.new()
|
||||||
|
# Seat at the wound (where the shaft meets the hide) and spray back out along the
|
||||||
|
# shaft: mesh local -Z is the exit direction, rotated into world by the node basis.
|
||||||
|
p.position = Vector3(0.0, 0.0, _TIP_Z - _EMBED_DEPTH)
|
||||||
|
p.emitting = true
|
||||||
|
p.amount = 22
|
||||||
|
p.lifetime = 0.6
|
||||||
|
p.explosiveness = 0.0
|
||||||
|
p.randomness = 0.6
|
||||||
|
p.local_coords = false
|
||||||
|
p.direction = Vector3(0.0, 0.55, -1.0).normalized()
|
||||||
|
p.spread = 32.0
|
||||||
|
p.gravity = Vector3(0.0, -12.0, 0.0)
|
||||||
|
p.initial_velocity_min = 1.6
|
||||||
|
p.initial_velocity_max = 4.0
|
||||||
|
p.scale_amount_min = 0.5
|
||||||
|
p.scale_amount_max = 1.4
|
||||||
|
p.mesh = _blood_mesh()
|
||||||
|
p.color_ramp = ramp
|
||||||
|
p.scale_amount_curve = sc
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
# Small unshaded, vertex-coloured sphere for blood gobs.
|
||||||
|
static func _blood_mesh() -> SphereMesh:
|
||||||
|
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 s := SphereMesh.new()
|
||||||
|
s.radius = 0.045
|
||||||
|
s.height = 0.09
|
||||||
|
s.radial_segments = 4
|
||||||
|
s.rings = 2
|
||||||
|
s.material = mat
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
static func _despawn(node: Node) -> void:
|
||||||
|
node.get_tree().create_timer(_DESPAWN_SEC).timeout.connect(func() -> void:
|
||||||
|
if is_instance_valid(node):
|
||||||
|
node.queue_free()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Basis whose local +Z points along `v` (looking_at points -Z at its target).
|
||||||
|
static func _aim_basis(v: Vector3) -> Basis:
|
||||||
|
var dir := v.normalized()
|
||||||
|
if dir.length_squared() < 0.0001:
|
||||||
|
return Basis.IDENTITY
|
||||||
|
var up := Vector3.UP if absf(dir.dot(Vector3.UP)) < 0.99 else Vector3.FORWARD
|
||||||
|
return Basis.looking_at(-dir, up)
|
||||||
|
|
||||||
|
|
||||||
|
# While flying, keep the shaft aligned with the velocity; on the first contact freeze
|
||||||
|
# and hand off to _embed so the spear sticks where it struck instead of bouncing off.
|
||||||
|
func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
|
||||||
|
if _stopped:
|
||||||
|
return
|
||||||
|
if state.get_contact_count() > 0:
|
||||||
|
_stopped = true
|
||||||
|
# Use the remembered pre-impact heading — state.linear_velocity is already the
|
||||||
|
# bounced (post-collision) velocity by now, which would aim the shaft wrong.
|
||||||
|
var contact := state.get_contact_collider_position(0)
|
||||||
|
var other := state.get_contact_collider_object(0)
|
||||||
|
state.linear_velocity = Vector3.ZERO
|
||||||
|
state.angular_velocity = Vector3.ZERO
|
||||||
|
_embed.call_deferred(other, contact, _flight_dir)
|
||||||
|
return
|
||||||
|
var v := state.linear_velocity
|
||||||
|
if v.length_squared() > 0.04:
|
||||||
|
_flight_dir = v.normalized()
|
||||||
|
var t := state.transform
|
||||||
|
t.basis = _aim_basis(v)
|
||||||
|
state.transform = t
|
||||||
|
state.angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
# Compose the stuck pose (tip buried _EMBED_DEPTH past the contact, shaft along the
|
||||||
|
# flight line) and plant the spear. A bull hit hands the mesh to the bull so it rides
|
||||||
|
# along as a spine; anything else pins the body in place for the despawn timer.
|
||||||
|
func _embed(other: Object, contact: Vector3, flight: Vector3) -> void:
|
||||||
|
var f := flight.normalized()
|
||||||
|
if f.length_squared() < 0.0001:
|
||||||
|
f = global_transform.basis.z.normalized()
|
||||||
|
var pose := Transform3D(_aim_basis(f), contact + f * (_EMBED_DEPTH - _TIP_Z))
|
||||||
|
var bull := other as Node
|
||||||
|
if bull != null and bull.is_in_group(&"player"):
|
||||||
|
bull.call(&"take_sword_hit", "thrown", contact)
|
||||||
|
if is_instance_valid(_mesh) and is_instance_valid(bull):
|
||||||
|
_mesh.reparent(_spear_anchor(bull), false)
|
||||||
|
_mesh.global_transform = pose
|
||||||
|
_mesh.add_to_group(_STUCK_GROUP)
|
||||||
|
if Settings.gore:
|
||||||
|
_mesh.add_child(_wound_emitter()) # keep bleeding from the wound while lodged
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
freeze = true
|
||||||
|
global_transform = pose
|
||||||
|
|
||||||
|
|
||||||
|
# Node embedded spears ride on the bull: a BoneAttachment3D pinned to the COG bone so
|
||||||
|
# they follow the animated body (bob, turn) rather than only the character root. One
|
||||||
|
# anchor is shared by every spear; falls back to the bull root if there's no skeleton.
|
||||||
|
static func _spear_anchor(bull: Node) -> Node3D:
|
||||||
|
var existing := bull.find_child(_ANCHOR_NAME, true, false)
|
||||||
|
if existing is Node3D:
|
||||||
|
return existing as Node3D
|
||||||
|
var skel := bull.find_child("Skeleton3D", true, false) as Skeleton3D
|
||||||
|
if skel == null:
|
||||||
|
return bull as Node3D
|
||||||
|
var ba := BoneAttachment3D.new()
|
||||||
|
ba.name = _ANCHOR_NAME
|
||||||
|
skel.add_child(ba)
|
||||||
|
ba.bone_name = _COG_BONE
|
||||||
|
return ba
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dwjakh1is1jtj
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Bear boss behaviour test. Self-contained: a floor, a real bull (Player.tscn), and the
|
||||||
|
## bear — no level/shell scene needed. Parks the bull at different range bands and checks
|
||||||
|
## the bear commits to band-appropriate routines through wind-up→active→recover, that the
|
||||||
|
## leap lands on the ground (not mid-air), hyper-armour holds mid-swing, and phase 2 +
|
||||||
|
## death fire. Run: godot --headless --script res://tests/bear_boss_test.gd
|
||||||
|
|
||||||
|
var _pass := 0
|
||||||
|
var _fail := 0
|
||||||
|
var _bear: Node3D = null
|
||||||
|
var _bull: Node3D = null
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _ok(c: bool, m: String) -> void:
|
||||||
|
if c:
|
||||||
|
_pass += 1
|
||||||
|
print(" PASS: ", m)
|
||||||
|
else:
|
||||||
|
_fail += 1
|
||||||
|
print(" FAIL: ", m)
|
||||||
|
|
||||||
|
|
||||||
|
func _base(s: String) -> String:
|
||||||
|
return s.split(":")[0].trim_suffix("*")
|
||||||
|
|
||||||
|
|
||||||
|
func _dt() -> float:
|
||||||
|
return 1.0 / Engine.get_physics_ticks_per_second()
|
||||||
|
|
||||||
|
|
||||||
|
# Keep the bull pinned at a flat distance from the bear and alive; report the routine
|
||||||
|
# bases the bear visits over `secs`.
|
||||||
|
func _observe(dist: float, secs: float) -> Dictionary:
|
||||||
|
var seen := {}
|
||||||
|
var t := 0.0
|
||||||
|
while t < secs and is_instance_valid(_bear):
|
||||||
|
_pin_bull(dist)
|
||||||
|
await physics_frame
|
||||||
|
t += _dt()
|
||||||
|
seen[_base(_bear.call("ai_state_name"))] = true
|
||||||
|
return seen
|
||||||
|
|
||||||
|
|
||||||
|
func _pin_bull(dist: float) -> void:
|
||||||
|
if _bull == null or not is_instance_valid(_bull):
|
||||||
|
return
|
||||||
|
if dist > 0.0:
|
||||||
|
_bull.global_position = _bear.global_position + Vector3(dist, 0, 0)
|
||||||
|
_bull.set("_hp", 100000) # never let the bull die and trip a game-over during the test
|
||||||
|
|
||||||
|
|
||||||
|
func _make_floor() -> StaticBody3D:
|
||||||
|
var body := StaticBody3D.new()
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
var box := BoxShape3D.new()
|
||||||
|
box.size = Vector3(400, 1, 400) # oversized so the bear can't chase the bull off an edge
|
||||||
|
cs.shape = box
|
||||||
|
cs.position = Vector3(0, -0.5, 0) # top face at y=0
|
||||||
|
body.add_child(cs)
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
root.add_child(_make_floor())
|
||||||
|
|
||||||
|
_bull = (load("res://Player.tscn") as PackedScene).instantiate()
|
||||||
|
root.add_child(_bull)
|
||||||
|
_bull.global_position = Vector3(4, 1, 0)
|
||||||
|
|
||||||
|
_bear = (load("res://Bear.tscn") as PackedScene).instantiate()
|
||||||
|
root.add_child(_bear)
|
||||||
|
_bear.global_position = Vector3(0, 1, 0)
|
||||||
|
await physics_frame
|
||||||
|
await physics_frame
|
||||||
|
_ok(_bull.is_in_group(&"player"), "bull is in the player group")
|
||||||
|
|
||||||
|
print(" anims: idle=%s run=%s walk2=%s stand=%s swipe=%s smash=%s leap=%s" % [
|
||||||
|
_bear.get("_anim_idle"), _bear.get("_anim_run"), _bear.get("_anim_walk2"),
|
||||||
|
_bear.get("_anim_stand"), _bear.get("_anim_swipe"), _bear.get("_anim_smash"),
|
||||||
|
_bear.get("_anim_leap")])
|
||||||
|
for k in ["_anim_idle", "_anim_run", "_anim_walk2", "_anim_stand", "_anim_swipe", "_anim_smash", "_anim_leap"]:
|
||||||
|
_ok(String(_bear.get(k)) != "", "resolved %s" % k)
|
||||||
|
|
||||||
|
# Close band → melee (swipe / smash).
|
||||||
|
var close_seen := await _observe(2.5, 6.0)
|
||||||
|
print(" close-band states: ", close_seen.keys())
|
||||||
|
_ok(close_seen.has("SWIPE") or close_seen.has("SMASH"), "close range → melee routine")
|
||||||
|
# Charge was removed — the bear must never enter it.
|
||||||
|
_ok(not close_seen.has("CHARGE"), "no charge routine (removed)")
|
||||||
|
|
||||||
|
# It must not climb on top of the bull: pressed right up against a pinned bull while
|
||||||
|
# doing melee (never leaping), the grounded bear's height must stay near the floor.
|
||||||
|
var max_y := -INF
|
||||||
|
var ct := 0.0
|
||||||
|
while ct < 4.0 and is_instance_valid(_bear):
|
||||||
|
_pin_bull(1.5)
|
||||||
|
await physics_frame
|
||||||
|
ct += _dt()
|
||||||
|
if not String(_bear.call("ai_state_name")).begins_with("LEAP"):
|
||||||
|
max_y = maxf(max_y, _bear.global_position.y)
|
||||||
|
_ok(max_y < 0.6, "bear does not climb on top of the bull (max grounded y=%.2f)" % max_y)
|
||||||
|
|
||||||
|
# Claw slashes: an emitter is bound to each paw, and one fires during the 1-2 punch.
|
||||||
|
_ok(_bear.get("_claw_l") != null and _bear.get("_claw_r") != null, "claw emitters bound to both paws")
|
||||||
|
var clawed := false
|
||||||
|
var wt := 0.0
|
||||||
|
while wt < 8.0 and is_instance_valid(_bear) and not clawed:
|
||||||
|
_pin_bull(2.5)
|
||||||
|
await physics_frame
|
||||||
|
wt += _dt()
|
||||||
|
var cl: CPUParticles3D = _bear.get("_claw_l")
|
||||||
|
var cr: CPUParticles3D = _bear.get("_claw_r")
|
||||||
|
if (cl != null and cl.emitting) or (cr != null and cr.emitting):
|
||||||
|
clawed = true
|
||||||
|
_ok(clawed, "claw slash emits during the 1-2 punch")
|
||||||
|
|
||||||
|
# Mid band → walk-in (stalk) or leap.
|
||||||
|
var mid_seen := await _observe(7.0, 8.0)
|
||||||
|
print(" mid-band states: ", mid_seen.keys())
|
||||||
|
_ok(mid_seen.has("STALK") or mid_seen.has("LEAP"), "mid range → stalk/leap")
|
||||||
|
|
||||||
|
# Routines reach ACTIVE (hit) and RECOVER (punish) phases.
|
||||||
|
var saw_active := false
|
||||||
|
var saw_recover := false
|
||||||
|
var t := 0.0
|
||||||
|
while t < 8.0 and is_instance_valid(_bear):
|
||||||
|
_pin_bull(2.5)
|
||||||
|
await physics_frame
|
||||||
|
t += _dt()
|
||||||
|
var s: String = _bear.call("ai_state_name")
|
||||||
|
if s.contains(":ACTIVE"): saw_active = true
|
||||||
|
if s.contains(":RECOVER"): saw_recover = true
|
||||||
|
_ok(saw_active, "routines reach their ACTIVE (hit) phase")
|
||||||
|
_ok(saw_recover, "routines reach their RECOVER (punish) phase")
|
||||||
|
|
||||||
|
# Leap must actually get airborne and then LAND before it recovers — the slam lands on
|
||||||
|
# the ground (impact frame), not mid-air. Reset the bear to a known grounded spot, then
|
||||||
|
# force one leap deterministically and follow it.
|
||||||
|
_bear.call("_enter_neutral")
|
||||||
|
_bear.global_position = Vector3(0, 1, 0)
|
||||||
|
_bear.velocity = Vector3.ZERO
|
||||||
|
var settle := 0
|
||||||
|
while settle < 30 and not _bear.is_on_floor():
|
||||||
|
_pin_bull(7.0)
|
||||||
|
await physics_frame
|
||||||
|
settle += 1
|
||||||
|
var start_y: float = _bear.global_position.y
|
||||||
|
_bear.call("_start_leap")
|
||||||
|
var peak_y := start_y
|
||||||
|
var recovered_on_floor := false
|
||||||
|
var recovered_airborne := false
|
||||||
|
var reached_recover := false
|
||||||
|
t = 0.0
|
||||||
|
while t < 4.0 and is_instance_valid(_bear):
|
||||||
|
_pin_bull(7.0)
|
||||||
|
await physics_frame
|
||||||
|
t += _dt()
|
||||||
|
peak_y = maxf(peak_y, _bear.global_position.y)
|
||||||
|
var s: String = _bear.call("ai_state_name")
|
||||||
|
if s.begins_with("LEAP:RECOVER"):
|
||||||
|
reached_recover = true
|
||||||
|
if _bear.is_on_floor(): recovered_on_floor = true
|
||||||
|
else: recovered_airborne = true
|
||||||
|
break
|
||||||
|
_ok(peak_y > start_y + 0.3, "leap gets airborne (peak +%.2fm)" % (peak_y - start_y))
|
||||||
|
_ok(reached_recover, "leap reaches its slam/recovery")
|
||||||
|
_ok(recovered_on_floor and not recovered_airborne,
|
||||||
|
"leap lands on the ground before recovering (frame-25 contact, no mid-air finish)")
|
||||||
|
|
||||||
|
# Hyper-armour: let the bear commit to a swing, then hit it — it must NOT drop into
|
||||||
|
# STAGGER mid-swing (only its recovery is punishable), yet it must still take the wound.
|
||||||
|
var in_active := false
|
||||||
|
t = 0.0
|
||||||
|
while t < 8.0 and is_instance_valid(_bear) and not in_active:
|
||||||
|
_pin_bull(2.5)
|
||||||
|
await physics_frame
|
||||||
|
t += _dt()
|
||||||
|
in_active = String(_bear.call("ai_state_name")).contains(":ACTIVE")
|
||||||
|
_ok(in_active, "bear commits to an ACTIVE swing")
|
||||||
|
|
||||||
|
var hp_before := int(_bear.get("_hp"))
|
||||||
|
var staggered_mid_swing := false
|
||||||
|
var i := 0
|
||||||
|
while i < 8 and is_instance_valid(_bear) and String(_bear.call("ai_state_name")).contains(":ACTIVE"):
|
||||||
|
_pin_bull(2.5)
|
||||||
|
_bear.call("apply_ability_hit", Vector3.RIGHT, 10.0, 0.0)
|
||||||
|
await physics_frame
|
||||||
|
if String(_bear.call("ai_state_name")).begins_with("STAGGER"):
|
||||||
|
staggered_mid_swing = true
|
||||||
|
i += 1
|
||||||
|
_ok(not staggered_mid_swing, "hyper-armour: hits during a swing don't stagger it")
|
||||||
|
_ok(int(_bear.get("_hp")) < hp_before, "bear still takes damage during the swing")
|
||||||
|
|
||||||
|
# Drive HP down (spaced out so it isn't stun-locked) and confirm phase 2 engages.
|
||||||
|
t = 0.0
|
||||||
|
while t < 8.0 and is_instance_valid(_bear) and not bool(_bear.get("_enraged")):
|
||||||
|
_pin_bull(2.5)
|
||||||
|
_bear.call("apply_ability_hit", Vector3.RIGHT, 10.0, 0.0)
|
||||||
|
for _f in range(18): # ~0.3s gap between hits
|
||||||
|
_pin_bull(2.5)
|
||||||
|
await physics_frame
|
||||||
|
t += _dt()
|
||||||
|
_ok(bool(_bear.get("_enraged")), "phase 2 (enrage) triggered below the HP fraction")
|
||||||
|
|
||||||
|
# Finish it off and confirm death + free.
|
||||||
|
for _k in range(int(_bear.get("_hp")) + 2):
|
||||||
|
if is_instance_valid(_bear):
|
||||||
|
_bear.call("apply_ability_hit", Vector3.RIGHT, 12.0, 0.0)
|
||||||
|
for _f in range(3):
|
||||||
|
await physics_frame
|
||||||
|
await physics_frame
|
||||||
|
_ok(is_instance_valid(_bear) and not _bear.call("is_active"), "bear dead after HP drained")
|
||||||
|
await create_timer(6.0).timeout
|
||||||
|
_ok(not is_instance_valid(_bear), "bear freed after death topple")
|
||||||
|
|
||||||
|
print("Results: %d passed, %d failed" % [_pass, _fail])
|
||||||
|
quit(1 if _fail > 0 else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c7wjoqgkpsepj
|
||||||
+13
-1
@@ -243,9 +243,21 @@ func _check_game_over_wiring(scene: Node) -> void:
|
|||||||
_assert_true(spawners.is_empty() or spawners[0].has_signal(&"all_defeated"),
|
_assert_true(spawners.is_empty() or spawners[0].has_signal(&"all_defeated"),
|
||||||
"spawner exposes an all_defeated signal (win route)")
|
"spawner exposes an all_defeated signal (win route)")
|
||||||
|
|
||||||
|
# The bull now soaks bull_max_hp hits. Zero the i-frame window so the test can
|
||||||
|
# land consecutive blades, then confirm it survives every hit but the last.
|
||||||
|
var dp: Node = root.get_node_or_null("/root/DP")
|
||||||
|
if dp != null:
|
||||||
|
dp.set_value("bull_hit_iframes", 0.0)
|
||||||
|
var max_hp: int = player.call(&"get_max_hp")
|
||||||
|
for i: int in max_hp - 1:
|
||||||
|
player.take_sword_hit()
|
||||||
|
await create_timer(0.05).timeout
|
||||||
|
_assert_true(not hud._game_over, "hit %d of %d does not end the run" % [i + 1, max_hp])
|
||||||
|
_assert_true(player.call(&"get_hp") == 1, "bull sits at 1 HP before the killing blow")
|
||||||
|
|
||||||
player.take_sword_hit()
|
player.take_sword_hit()
|
||||||
await create_timer(0.05).timeout
|
await create_timer(0.05).timeout
|
||||||
_assert_true(hud._game_over, "a single sword hit raises the game-over screen")
|
_assert_true(hud._game_over, "draining the last HP raises the game-over screen")
|
||||||
_assert_true(not hud._result_win, "a bull hit is a loss, not a win")
|
_assert_true(not hud._result_win, "a bull hit is a loss, not a win")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Level-isolation test: when the shell loads the Bear's Den, none of the arena level's
|
||||||
|
## geometry — crucially its wall/floor colliders and audience — may survive into it.
|
||||||
|
## Guards the modular-level contract: switching levels swaps the whole module, it never
|
||||||
|
## hides old nodes (hidden colliders still collide). Run:
|
||||||
|
## godot --headless --script tests/level_switch_test.gd
|
||||||
|
|
||||||
|
var _pass := 0
|
||||||
|
var _fail := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _ok(c: bool, m: String) -> void:
|
||||||
|
if c:
|
||||||
|
_pass += 1
|
||||||
|
print(" PASS: ", m)
|
||||||
|
else:
|
||||||
|
_fail += 1
|
||||||
|
print(" FAIL: ", m)
|
||||||
|
|
||||||
|
|
||||||
|
func _count_named(node: Node, wanted: String, acc: int = 0) -> int:
|
||||||
|
if node.name == wanted:
|
||||||
|
acc += 1
|
||||||
|
for child: Node in node.get_children():
|
||||||
|
acc = _count_named(child, wanted, acc)
|
||||||
|
return acc
|
||||||
|
|
||||||
|
|
||||||
|
func _count_type(node: Node, klass: String, acc: int = 0) -> int:
|
||||||
|
if node.is_class(klass):
|
||||||
|
acc += 1
|
||||||
|
for child: Node in node.get_children():
|
||||||
|
acc = _count_type(child, klass, acc)
|
||||||
|
return acc
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var run: Node = root.get_node("Run")
|
||||||
|
run.ensure_run()
|
||||||
|
# Force the shell to build the Bear's Den regardless of the generated map.
|
||||||
|
run.debug_level_override = run.get_level(&"bear")
|
||||||
|
|
||||||
|
var scene: Node = load("res://scene.tscn").instantiate()
|
||||||
|
root.add_child(scene)
|
||||||
|
current_scene = scene
|
||||||
|
# Let the shell instance the level and the spawner's deferred spawn run.
|
||||||
|
await create_timer(0.8).timeout
|
||||||
|
|
||||||
|
# Shell survives and hosts exactly one level module.
|
||||||
|
var level_root: Node = scene.get_node_or_null("LevelRoot")
|
||||||
|
_ok(level_root != null, "shell has a LevelRoot")
|
||||||
|
_ok(level_root != null and level_root.get_child_count() == 1, "exactly one level module loaded")
|
||||||
|
_ok(not get_nodes_in_group(&"player").is_empty(), "player (shell) present")
|
||||||
|
|
||||||
|
# The leak we are guarding against: no arena geometry may exist in the bear level.
|
||||||
|
_ok(_count_named(scene, "arena_placeholder_v01") == 0, "arena placeholder (+ its wall collider) gone")
|
||||||
|
_ok(_count_named(scene, "Arena2") == 0, "arena mesh 'Arena2' gone")
|
||||||
|
_ok(_count_named(scene, "PublikNode") == 0, "arena audience gone from bear level")
|
||||||
|
|
||||||
|
# The bear level brings its own geometry, one floor collider, and its boss.
|
||||||
|
_ok(_count_named(scene, "bear_level_arena") == 1, "bear arena mesh present")
|
||||||
|
_ok(_count_type(level_root, "StaticBody3D") >= 1, "bear level has its own floor collider")
|
||||||
|
|
||||||
|
await create_timer(0.4).timeout
|
||||||
|
_ok(not get_nodes_in_group(&"bear").is_empty(), "bear boss spawned in bear level")
|
||||||
|
|
||||||
|
print("Results: %d passed, %d failed" % [_pass, _fail])
|
||||||
|
quit(1 if _fail > 0 else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b6ud4uune1clf
|
||||||
@@ -30,6 +30,7 @@ func _run() -> void:
|
|||||||
test_kick_spread()
|
test_kick_spread()
|
||||||
test_gait_phase_crossing()
|
test_gait_phase_crossing()
|
||||||
test_tail_verlet_constraint()
|
test_tail_verlet_constraint()
|
||||||
|
test_run_map()
|
||||||
|
|
||||||
print("=" .repeat(60))
|
print("=" .repeat(60))
|
||||||
print("Results: %d passed, %d failed" % [_passed, _failed])
|
print("Results: %d passed, %d failed" % [_passed, _failed])
|
||||||
@@ -171,6 +172,60 @@ func test_tail_verlet_constraint() -> void:
|
|||||||
_assert_in_range(damping_factor, 0.85, 1.0, "tail damping factor is reasonable")
|
_assert_in_range(damping_factor, 0.85, 1.0, "tail damping factor is reasonable")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Run map (Slay-the-Spire progression) ──────────────────────────────────────
|
||||||
|
|
||||||
|
func test_run_map() -> void:
|
||||||
|
print("\n-- test_run_map --")
|
||||||
|
var run: Node = root.get_node_or_null("/root/Run")
|
||||||
|
if run == null:
|
||||||
|
_assert_true(false, "Run autoload should exist")
|
||||||
|
return
|
||||||
|
|
||||||
|
run.start_new_run()
|
||||||
|
_assert_true(run.has_run(), "start_new_run builds a map")
|
||||||
|
_assert_true(run.map.size() >= 2, "map has multiple rows")
|
||||||
|
|
||||||
|
# Final row is a single boss node the map funnels toward.
|
||||||
|
var last: Array = run.map[run.map.size() - 1]
|
||||||
|
_assert_eq(last.size(), 1, "final row is a single boss node")
|
||||||
|
_assert_true(last[0].level.is_boss, "final node is a boss level")
|
||||||
|
|
||||||
|
# Connectivity: every node links forward, and every next-row node has a parent.
|
||||||
|
for r: int in run.map.size() - 1:
|
||||||
|
var nxt: Array = run.map[r + 1]
|
||||||
|
var reached := {}
|
||||||
|
for from_node in run.map[r]:
|
||||||
|
_assert_true(from_node.links.size() >= 1, "row %d node links forward" % r)
|
||||||
|
for c: int in from_node.links:
|
||||||
|
_assert_true(c >= 0 and c < nxt.size(), "link column in range")
|
||||||
|
reached[c] = true
|
||||||
|
for c: int in nxt.size():
|
||||||
|
_assert_true(reached.has(c), "row %d col %d has a parent" % [r + 1, c])
|
||||||
|
|
||||||
|
# From the start gate the whole first row is the frontier.
|
||||||
|
_assert_eq(run.reachable().size(), run.map[0].size(), "start frontier is row 0")
|
||||||
|
|
||||||
|
# Advancing follows the chosen node's links.
|
||||||
|
var first = run.map[0][0]
|
||||||
|
run.select_node(first)
|
||||||
|
run.complete_current_level()
|
||||||
|
_assert_eq(run.player_row, 0, "player advanced to row 0")
|
||||||
|
_assert_true(first.cleared, "cleared node is flagged")
|
||||||
|
_assert_eq(run.reachable().size(), first.links.size(),
|
||||||
|
"frontier follows the chosen node's links")
|
||||||
|
|
||||||
|
# Walking to a boss node latches run completion.
|
||||||
|
var guard := 0
|
||||||
|
while not run.run_complete() and guard < 64:
|
||||||
|
var opts: Array = run.reachable()
|
||||||
|
if opts.is_empty():
|
||||||
|
break
|
||||||
|
run.select_node(opts[0])
|
||||||
|
run.complete_current_level()
|
||||||
|
guard += 1
|
||||||
|
_assert_true(run.run_complete(), "reaching a boss node completes the run")
|
||||||
|
|
||||||
|
|
||||||
# ── Shared helpers ────────────────────────────────────────────────────────────
|
# ── Shared helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _assert_in_range(val: float, lo: float, hi: float, desc: String) -> void:
|
func _assert_in_range(val: float, lo: float, hi: float, desc: String) -> void:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 393 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cf0mk32r40fbb"
|
||||||
|
path="res://.godot/imported/aerial.png-6f0c31ec14797100c056557b8d5f36d8.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tools/preview/aerial.png"
|
||||||
|
dest_files=["res://.godot/imported/aerial.png-6f0c31ec14797100c056557b8d5f36d8.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: 465 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://qx85guumsl7p"
|
||||||
|
path="res://.godot/imported/top.png-1fab202f4dd35d955a9e4ea2d0fefe58.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://tools/preview/top.png"
|
||||||
|
dest_files=["res://.godot/imported/top.png-1fab202f4dd35d955a9e4ea2d0fefe58.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
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Top-down + angled aerial of the arena with the crowd, plus axis markers, to locate
|
||||||
|
## the ruined section and any billboards left floating over it.
|
||||||
|
## godot --script tools/preview_topdown.gd
|
||||||
|
## Output: tools/preview/top.png (ortho top-down), tools/preview/aerial.png
|
||||||
|
|
||||||
|
const OUT := "res://tools/preview"
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
DirAccess.make_dir_recursive_absolute(OUT)
|
||||||
|
var scene := (load("res://scene.tscn") as PackedScene).instantiate()
|
||||||
|
root.add_child(scene)
|
||||||
|
current_scene = scene
|
||||||
|
|
||||||
|
# Axis markers: red pillar at +X (east), green at +Z (south), so directions read.
|
||||||
|
_marker(scene, Vector3(18, 5, 0), Color(1, 0.2, 0.2))
|
||||||
|
_marker(scene, Vector3(0, 5, 18), Color(0.2, 1, 0.2))
|
||||||
|
|
||||||
|
await create_timer(0.4).timeout
|
||||||
|
var crowd := scene.get_node_or_null(^"CrowdBillboards") as MultiMeshInstance3D
|
||||||
|
if crowd != null and crowd.multimesh != null:
|
||||||
|
print("crowd instances = ", crowd.multimesh.instance_count)
|
||||||
|
|
||||||
|
var top := Camera3D.new()
|
||||||
|
top.projection = Camera3D.PROJECTION_ORTHOGONAL
|
||||||
|
top.size = 46.0
|
||||||
|
scene.add_child(top)
|
||||||
|
top.look_at_from_position(Vector3(0, 60, 0.01), Vector3.ZERO, Vector3(0, 0, -1))
|
||||||
|
top.current = true
|
||||||
|
await create_timer(0.2).timeout
|
||||||
|
_shot("top.png")
|
||||||
|
|
||||||
|
var air := Camera3D.new()
|
||||||
|
air.fov = 60.0
|
||||||
|
scene.add_child(air)
|
||||||
|
air.look_at_from_position(Vector3(0, 26, 34), Vector3(0, 3, 0), Vector3.UP)
|
||||||
|
air.current = true
|
||||||
|
await create_timer(0.2).timeout
|
||||||
|
_shot("aerial.png")
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _marker(scene: Node, pos: Vector3, col: Color) -> void:
|
||||||
|
var m := MeshInstance3D.new()
|
||||||
|
var box := BoxMesh.new()
|
||||||
|
box.size = Vector3(1.5, 10.0, 1.5)
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.albedo_color = col
|
||||||
|
box.material = mat
|
||||||
|
m.mesh = box
|
||||||
|
scene.add_child(m)
|
||||||
|
m.global_position = pos
|
||||||
|
|
||||||
|
|
||||||
|
func _shot(name: String) -> void:
|
||||||
|
var img := root.get_viewport().get_texture().get_image()
|
||||||
|
if img != null:
|
||||||
|
img.save_png(OUT + "/" + name)
|
||||||
|
print("captured ", name)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dfcfb205scjvt
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
## On-screen controls for touch devices: a floating movement joystick on the left
|
||||||
|
## and a cluster of ability buttons on the right. Instantiated by hud.gd.
|
||||||
|
##
|
||||||
|
## All interaction is driven from raw `_input()` with manual hit-testing, NOT from
|
||||||
|
## Control.gui_input. On the web/wasm build the overlay lives in a CanvasLayer nested
|
||||||
|
## under the HUD, and GUI touch routing into a nested CanvasLayer renders fine but
|
||||||
|
## silently drops input — a standard Button here never fires. `_input()` sees every
|
||||||
|
## InputEventScreenTouch regardless of nesting, mouse_filter, or emulate_mouse_from_touch,
|
||||||
|
## so the joystick and buttons work reliably across browsers. Everything is drawn in one
|
||||||
|
## Control (_surface) whose rects are the single source of truth for both drawing and
|
||||||
|
## hit-testing.
|
||||||
|
##
|
||||||
|
## The joystick feeds the same `move_*` input actions the keyboard/gamepad drive (via
|
||||||
|
## Input.action_press with an analog strength), so player.gd needs no touch awareness — it
|
||||||
|
## just reads Input.get_vector() as always. Ability buttons call the player's public
|
||||||
|
## try_activate_ability(), sharing the keyboard's cooldown gate.
|
||||||
|
##
|
||||||
|
## Shown only on touch platforms (see Controls.use_touch_ui), or when DP
|
||||||
|
## "force_touch_controls" is on so the layout can be exercised on a desktop build
|
||||||
|
## (emulate_touch_from_mouse turns a mouse click into a touch event for that).
|
||||||
|
|
||||||
|
# move_* actions, indexed by the four joystick push directions.
|
||||||
|
const _MOVE_RIGHT: StringName = &"move_right"
|
||||||
|
const _MOVE_LEFT: StringName = &"move_left"
|
||||||
|
const _MOVE_BACK: StringName = &"move_back"
|
||||||
|
const _MOVE_FORWARD: StringName = &"move_forward"
|
||||||
|
|
||||||
|
# Ability slots in player.gd order: kick=0, dash=1, slam=2, roll=3.
|
||||||
|
const _ABILITY_ICONS: Array[String] = [
|
||||||
|
"res://HUD/KICK.png",
|
||||||
|
"res://HUD/DASH.png",
|
||||||
|
"res://HUD/SLAM.png",
|
||||||
|
"res://HUD/ROLL.png",
|
||||||
|
]
|
||||||
|
const _ABILITY_LETTERS: Array[String] = ["K", "D", "S", "R"]
|
||||||
|
const _ABILITY_ACCENT: Array[Color] = [
|
||||||
|
Color(1.00, 0.62, 0.16), # Kick — ember orange
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sentinel touch indices. A real finger uses its own non-negative index; the mouse
|
||||||
|
# (desktop force-flag testing, when no real touch has been seen) uses _MOUSE.
|
||||||
|
const _MOUSE: int = -2
|
||||||
|
|
||||||
|
var _surface: Control
|
||||||
|
var _player: Node = null
|
||||||
|
|
||||||
|
# Cached textures + styleboxes so the per-frame redraw (cooldowns animate) allocates nothing.
|
||||||
|
var _ability_tex: Array[Texture2D] = []
|
||||||
|
var _ability_style: Array[StyleBoxFlat] = []
|
||||||
|
var _menu_style: StyleBoxFlat
|
||||||
|
|
||||||
|
# Layout rects in screen space — the single source of truth for drawing AND hit-testing.
|
||||||
|
var _stick_zone: Rect2 = Rect2()
|
||||||
|
var _ability_rects: Array[Rect2] = []
|
||||||
|
var _menu_rect: Rect2 = Rect2()
|
||||||
|
|
||||||
|
# Resting "drag to move" affordance drawn in the lower-left when the stick is idle, so a
|
||||||
|
# first-time player sees where to plant their thumb instead of guessing. _hint_pulse gives
|
||||||
|
# it a slow breathing alpha for discoverability.
|
||||||
|
var _stick_hint_center: Vector2 = Vector2()
|
||||||
|
var _hint_pulse: float = 0.0
|
||||||
|
|
||||||
|
# Floating-joystick state. _origin is where the thumb first touched (base centre), _knob is
|
||||||
|
# the current thumb position; both in screen coordinates.
|
||||||
|
var _stick_active: bool = false
|
||||||
|
var _origin: Vector2 = Vector2.ZERO
|
||||||
|
var _knob: Vector2 = Vector2.ZERO
|
||||||
|
var _stick_radius: float = 110.0
|
||||||
|
|
||||||
|
# Live fingers by index → screen position, plus what each finger is doing ("stick", a slot
|
||||||
|
# int, "menu", or "camera"). Ownership keeps a second finger on a button from stealing the
|
||||||
|
# stick, and keeps steering/ability fingers from being mistaken for a pinch gesture.
|
||||||
|
var _touches: Dictionary = {}
|
||||||
|
var _touch_owner: Dictionary = {}
|
||||||
|
var _saw_real_touch: bool = false
|
||||||
|
|
||||||
|
# Pinch-to-zoom: only "camera" fingers (right side, not on any button) count, so steering
|
||||||
|
# with one thumb while tapping an ability with the other never reads as a zoom.
|
||||||
|
var _pinching: bool = false
|
||||||
|
var _pinch_dist: float = 0.0
|
||||||
|
|
||||||
|
# Diagnostics surfaced by DP "touch_debug" — lets us see, on the actual device, whether
|
||||||
|
# events arrive and where, since no console is reachable there.
|
||||||
|
var _dbg_events: int = 0
|
||||||
|
var _dbg_last_pos: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = 10 # below the HUD (11) so the game-over screen always draws on top
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
|
||||||
|
for slot: int in 4:
|
||||||
|
var tex: Texture2D = null
|
||||||
|
if ResourceLoader.exists(_ABILITY_ICONS[slot]):
|
||||||
|
tex = load(_ABILITY_ICONS[slot])
|
||||||
|
_ability_tex.append(tex)
|
||||||
|
_ability_style.append(_button_style(_ABILITY_ACCENT[slot]))
|
||||||
|
_menu_style = _button_style(Color(1.00, 0.78, 0.22, 0.8))
|
||||||
|
|
||||||
|
_surface = Control.new()
|
||||||
|
_surface.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_surface.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_surface.visible = false
|
||||||
|
_surface.draw.connect(_draw_surface)
|
||||||
|
add_child(_surface)
|
||||||
|
|
||||||
|
_relayout()
|
||||||
|
get_viewport().size_changed.connect(_relayout)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Layout ────────────────────────────────────────────────────────────────────
|
||||||
|
# Left half is the joystick zone (touch anywhere in it to plant the base under the thumb);
|
||||||
|
# the ability cluster and menu button sit in the right half so they never overlap it.
|
||||||
|
|
||||||
|
func _relayout() -> void:
|
||||||
|
var vp := get_viewport().get_visible_rect().size
|
||||||
|
var short := minf(vp.x, vp.y)
|
||||||
|
_stick_radius = clampf(short * 0.16, 80.0, 150.0)
|
||||||
|
_stick_zone = Rect2(Vector2.ZERO, Vector2(vp.x * 0.5, vp.y))
|
||||||
|
|
||||||
|
var bs := clampf(short * 0.16, 72.0, 120.0) # ability button edge
|
||||||
|
var gap := bs * 0.18
|
||||||
|
var margin := bs * 0.28
|
||||||
|
var right := vp.x - margin
|
||||||
|
var bottom := vp.y - margin
|
||||||
|
var col0 := right - bs * 2.0 - gap
|
||||||
|
var col1 := right - bs
|
||||||
|
var row0 := bottom - bs * 2.0 - gap
|
||||||
|
var row1 := bottom - bs
|
||||||
|
_ability_rects = [
|
||||||
|
Rect2(col0, row0, bs, bs), # kick
|
||||||
|
Rect2(col1, row0, bs, bs), # dash
|
||||||
|
Rect2(col0, row1, bs, bs), # slam
|
||||||
|
Rect2(col1, row1, bs, bs), # roll
|
||||||
|
]
|
||||||
|
|
||||||
|
var ms := clampf(short * 0.09, 48.0, 72.0)
|
||||||
|
_menu_rect = Rect2(vp.x - ms - 16.0, 16.0, ms, ms)
|
||||||
|
|
||||||
|
_stick_hint_center = Vector2(vp.x * 0.20, vp.y * 0.74)
|
||||||
|
_surface.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Input ───────────────────────────────────────────────────────────────────
|
||||||
|
# Raw events only. Real touches drive the touch path; once any real touch is seen the
|
||||||
|
# mouse path is disabled (emulate_mouse_from_touch would otherwise double-fire every tap).
|
||||||
|
# The mouse path stays live for desktop force-flag testing where no touchscreen exists.
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if get_tree().paused or not Controls.use_touch_ui():
|
||||||
|
return
|
||||||
|
|
||||||
|
if event is InputEventScreenTouch:
|
||||||
|
_saw_real_touch = true
|
||||||
|
var t := event as InputEventScreenTouch
|
||||||
|
_dbg_events += 1
|
||||||
|
_dbg_last_pos = t.position
|
||||||
|
if t.pressed:
|
||||||
|
_touches[t.index] = t.position
|
||||||
|
_on_press(t.index, t.position)
|
||||||
|
else:
|
||||||
|
_on_release(t.index, t.position)
|
||||||
|
_touches.erase(t.index)
|
||||||
|
_update_pinch_state()
|
||||||
|
_surface.queue_redraw()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
elif event is InputEventScreenDrag:
|
||||||
|
var d := event as InputEventScreenDrag
|
||||||
|
_dbg_events += 1
|
||||||
|
_dbg_last_pos = d.position
|
||||||
|
_touches[d.index] = d.position
|
||||||
|
_on_drag(d.index, d.position)
|
||||||
|
_surface.queue_redraw()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
elif not _saw_real_touch and event is InputEventMouseButton:
|
||||||
|
var mb := event as InputEventMouseButton
|
||||||
|
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
_dbg_events += 1
|
||||||
|
_dbg_last_pos = mb.position
|
||||||
|
if mb.pressed:
|
||||||
|
_touches[_MOUSE] = mb.position
|
||||||
|
_on_press(_MOUSE, mb.position)
|
||||||
|
else:
|
||||||
|
_on_release(_MOUSE, mb.position)
|
||||||
|
_touches.erase(_MOUSE)
|
||||||
|
_update_pinch_state()
|
||||||
|
_surface.queue_redraw()
|
||||||
|
|
||||||
|
elif not _saw_real_touch and event is InputEventMouseMotion:
|
||||||
|
if _touch_owner.get(_MOUSE, null) == "stick":
|
||||||
|
_touches[_MOUSE] = (event as InputEventMouseMotion).position
|
||||||
|
_update_stick(_touches[_MOUSE])
|
||||||
|
_surface.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_press(index: int, pos: Vector2) -> void:
|
||||||
|
if _menu_rect.has_point(pos):
|
||||||
|
_touch_owner[index] = "menu"
|
||||||
|
return
|
||||||
|
for slot: int in _ability_rects.size():
|
||||||
|
if _ability_rects[slot].has_point(pos):
|
||||||
|
_touch_owner[index] = slot
|
||||||
|
if _player != null:
|
||||||
|
_player.call(&"try_activate_ability", slot)
|
||||||
|
return
|
||||||
|
if _stick_zone.has_point(pos) and not _stick_active:
|
||||||
|
_touch_owner[index] = "stick"
|
||||||
|
_begin_stick(pos)
|
||||||
|
return
|
||||||
|
_touch_owner[index] = "camera"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag(index: int, pos: Vector2) -> void:
|
||||||
|
match _touch_owner.get(index, null):
|
||||||
|
"stick":
|
||||||
|
_update_stick(pos)
|
||||||
|
"camera":
|
||||||
|
if _pinching:
|
||||||
|
_apply_pinch()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_release(index: int, pos: Vector2) -> void:
|
||||||
|
match _touch_owner.get(index, null):
|
||||||
|
"stick":
|
||||||
|
_end_stick()
|
||||||
|
"menu":
|
||||||
|
if _menu_rect.has_point(pos):
|
||||||
|
_touch_owner.erase(index)
|
||||||
|
_return_to_menu()
|
||||||
|
return
|
||||||
|
_touch_owner.erase(index)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Joystick ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _begin_stick(pos: Vector2) -> void:
|
||||||
|
_stick_active = true
|
||||||
|
_origin = pos
|
||||||
|
_knob = pos
|
||||||
|
|
||||||
|
|
||||||
|
func _update_stick(pos: Vector2) -> void:
|
||||||
|
if not _stick_active:
|
||||||
|
return
|
||||||
|
var offset := pos - _origin
|
||||||
|
if offset.length() > _stick_radius:
|
||||||
|
offset = offset.normalized() * _stick_radius
|
||||||
|
_knob = _origin + offset
|
||||||
|
_apply_move(offset / _stick_radius)
|
||||||
|
|
||||||
|
|
||||||
|
func _end_stick() -> void:
|
||||||
|
_stick_active = false
|
||||||
|
_release_move()
|
||||||
|
|
||||||
|
|
||||||
|
# Translate a normalised stick offset (screen space: +x right, +y down) into analog
|
||||||
|
# strengths on the four movement actions. Opposite pairs are mutually exclusive, so only
|
||||||
|
# one of each axis is ever pressed at a time.
|
||||||
|
func _apply_move(v: Vector2) -> void:
|
||||||
|
Input.action_press(_MOVE_RIGHT, maxf(0.0, v.x))
|
||||||
|
Input.action_press(_MOVE_LEFT, maxf(0.0, -v.x))
|
||||||
|
Input.action_press(_MOVE_BACK, maxf(0.0, v.y))
|
||||||
|
Input.action_press(_MOVE_FORWARD, maxf(0.0, -v.y))
|
||||||
|
|
||||||
|
|
||||||
|
func _release_move() -> void:
|
||||||
|
for action: StringName in [_MOVE_RIGHT, _MOVE_LEFT, _MOVE_BACK, _MOVE_FORWARD]:
|
||||||
|
Input.action_release(action)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Pinch-to-zoom ─────────────────────────────────────────────────────────────
|
||||||
|
# Only "camera" fingers (open right-side area, not on a button) participate, so it can't
|
||||||
|
# fire while the player is steering or tapping abilities.
|
||||||
|
|
||||||
|
func _camera_touches() -> Array:
|
||||||
|
var pts: Array = []
|
||||||
|
for idx: int in _touches:
|
||||||
|
if _touch_owner.get(idx, null) == "camera":
|
||||||
|
pts.append(_touches[idx])
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
func _update_pinch_state() -> void:
|
||||||
|
var pts := _camera_touches()
|
||||||
|
var was := _pinching
|
||||||
|
_pinching = pts.size() >= 2
|
||||||
|
if _pinching and not was:
|
||||||
|
if _stick_active:
|
||||||
|
_end_stick()
|
||||||
|
_pinch_dist = (pts[0] as Vector2).distance_to(pts[1] as Vector2)
|
||||||
|
elif was and not _pinching:
|
||||||
|
_pinch_dist = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_pinch() -> void:
|
||||||
|
var pts := _camera_touches()
|
||||||
|
if pts.size() < 2:
|
||||||
|
return
|
||||||
|
var dist := (pts[0] as Vector2).distance_to(pts[1] as Vector2)
|
||||||
|
if _pinch_dist <= 0.0 or dist <= 0.0:
|
||||||
|
_pinch_dist = dist
|
||||||
|
return
|
||||||
|
var cam := get_viewport().get_camera_3d()
|
||||||
|
if cam != null and cam.has_method(&"adjust_zoom"):
|
||||||
|
cam.call(&"adjust_zoom", dist / _pinch_dist)
|
||||||
|
_pinch_dist = dist
|
||||||
|
|
||||||
|
|
||||||
|
# ── Drawing ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_surface() -> void:
|
||||||
|
for slot: int in _ability_rects.size():
|
||||||
|
var r: Rect2 = _ability_rects[slot]
|
||||||
|
_surface.draw_style_box(_ability_style[slot], r)
|
||||||
|
var inner := r.grow(-r.size.x * 0.14)
|
||||||
|
if _ability_tex[slot] != null:
|
||||||
|
_draw_icon_fit(_ability_tex[slot], inner)
|
||||||
|
else:
|
||||||
|
_draw_centered_text(_ABILITY_LETTERS[slot], r, 40, _ABILITY_ACCENT[slot])
|
||||||
|
var frac := 0.0
|
||||||
|
if _player != null:
|
||||||
|
frac = clampf(_player.call(&"ability_cooldown_fraction", slot), 0.0, 1.0)
|
||||||
|
if frac > 0.0:
|
||||||
|
var ch := r.size.y * frac
|
||||||
|
_surface.draw_rect(
|
||||||
|
Rect2(r.position.x, r.position.y + r.size.y - ch, r.size.x, ch),
|
||||||
|
Color(0.0, 0.0, 0.0, 0.55))
|
||||||
|
|
||||||
|
_surface.draw_style_box(_menu_style, _menu_rect)
|
||||||
|
_draw_hamburger(_menu_rect)
|
||||||
|
|
||||||
|
if _stick_active:
|
||||||
|
_surface.draw_circle(_origin, _stick_radius, Color(0.0, 0.0, 0.0, 0.20))
|
||||||
|
_surface.draw_arc(
|
||||||
|
_origin, _stick_radius, 0.0, TAU, 48, Color(0.93, 0.88, 0.72, 0.30), 4.0, true)
|
||||||
|
_surface.draw_circle(_knob, _stick_radius * 0.42, Color(1.00, 0.82, 0.32, 0.55))
|
||||||
|
elif not _pinching:
|
||||||
|
_draw_stick_hint()
|
||||||
|
|
||||||
|
if Controls.debug_overlay():
|
||||||
|
_draw_debug()
|
||||||
|
|
||||||
|
|
||||||
|
# A faint "ghost" joystick at rest in the lower-left with a caption, so the movement zone
|
||||||
|
# is discoverable. Breathes gently via _hint_pulse; vanishes the moment the stick is grabbed.
|
||||||
|
func _draw_stick_hint() -> void:
|
||||||
|
var a := 0.16 + 0.12 * (0.5 + 0.5 * sin(_hint_pulse))
|
||||||
|
var c := _stick_hint_center
|
||||||
|
var r := _stick_radius * 0.72
|
||||||
|
_surface.draw_circle(c, r, Color(0.0, 0.0, 0.0, a * 0.45))
|
||||||
|
_surface.draw_arc(c, r, 0.0, TAU, 40, Color(0.93, 0.88, 0.72, a + 0.14), 3.0, true)
|
||||||
|
_surface.draw_circle(c, r * 0.32, Color(1.00, 0.82, 0.32, a + 0.16))
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
if font != null:
|
||||||
|
var txt := "DRAG TO MOVE"
|
||||||
|
var fs := 18
|
||||||
|
var tw := font.get_string_size(txt, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
|
||||||
|
_surface.draw_string(
|
||||||
|
font, c + Vector2(-tw.x * 0.5, r + 28.0), txt, HORIZONTAL_ALIGNMENT_LEFT, -1, fs,
|
||||||
|
Color(0.93, 0.88, 0.72, a + 0.30))
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_icon_fit(tex: Texture2D, box: Rect2) -> void:
|
||||||
|
var ts := tex.get_size()
|
||||||
|
if ts.x <= 0.0 or ts.y <= 0.0:
|
||||||
|
return
|
||||||
|
var scale := minf(box.size.x / ts.x, box.size.y / ts.y)
|
||||||
|
var ds := ts * scale
|
||||||
|
_surface.draw_texture_rect(tex, Rect2(box.position + (box.size - ds) * 0.5, ds), false)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_centered_text(text: String, box: Rect2, size: int, color: Color) -> void:
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
if font == null:
|
||||||
|
return
|
||||||
|
var tw := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
|
||||||
|
var pos := box.position + (box.size - tw) * 0.5 + Vector2(0.0, tw.y * 0.35)
|
||||||
|
_surface.draw_string(font, pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
|
||||||
|
|
||||||
|
|
||||||
|
# Three stacked bars — a "☰" glyph drawn by hand, since the fallback font that ships with
|
||||||
|
# the web build has no U+2630 and renders it as tofu / a codepoint number.
|
||||||
|
func _draw_hamburger(r: Rect2) -> void:
|
||||||
|
var col := Color(1.00, 0.78, 0.22)
|
||||||
|
var bw := r.size.x * 0.5
|
||||||
|
var bh := maxf(3.0, r.size.y * 0.09)
|
||||||
|
var x := r.position.x + (r.size.x - bw) * 0.5
|
||||||
|
var cy := r.position.y + r.size.y * 0.5
|
||||||
|
var gap := r.size.y * 0.22
|
||||||
|
for i: int in [-1, 0, 1]:
|
||||||
|
_surface.draw_rect(Rect2(x, cy + i * gap - bh * 0.5, bw, bh), col)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_debug() -> void:
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
if font == null:
|
||||||
|
return
|
||||||
|
var players := get_tree().get_nodes_in_group(&"player")
|
||||||
|
var mats := get_tree().get_nodes_in_group(&"matador")
|
||||||
|
var lines := [
|
||||||
|
"GPU: %s" % RenderingServer.get_video_adapter_name(),
|
||||||
|
"API: %s" % RenderingServer.get_video_adapter_api_version(),
|
||||||
|
"method=%s web=%s touch_ui=%s" % [
|
||||||
|
ProjectSettings.get_setting("rendering/renderer/rendering_method", "?"),
|
||||||
|
OS.has_feature("web"), Controls.use_touch_ui()],
|
||||||
|
"bull: %s" % _node_report(players),
|
||||||
|
"matador(%d): %s" % [mats.size(), _node_report(mats)],
|
||||||
|
"events=%d last=%s touches=%d stick=%s" % [
|
||||||
|
_dbg_events, str(_dbg_last_pos.round()), _touches.size(), _stick_active],
|
||||||
|
]
|
||||||
|
var fs := 18
|
||||||
|
var line_h := 24.0
|
||||||
|
var w := 0.0
|
||||||
|
for line: String in lines:
|
||||||
|
w = maxf(w, font.get_string_size(line, HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x)
|
||||||
|
_surface.draw_rect(
|
||||||
|
Rect2(10.0, 34.0, w + 16.0, line_h * lines.size() + 12.0), Color(0.0, 0.0, 0.0, 0.62))
|
||||||
|
var y := 34.0 + 22.0
|
||||||
|
for line: String in lines:
|
||||||
|
_surface.draw_string(
|
||||||
|
font, Vector2(18.0, y), line, HORIZONTAL_ALIGNMENT_LEFT, -1, fs,
|
||||||
|
Color(1.0, 1.0, 0.3))
|
||||||
|
y += line_h
|
||||||
|
|
||||||
|
|
||||||
|
# One-line "does this character exist / is it drawable / where is it" report for the debug
|
||||||
|
# overlay, so an invisible bull/matador on-device can be pinned to spawn vs. position vs.
|
||||||
|
# GPU-draw failure.
|
||||||
|
func _node_report(nodes: Array) -> String:
|
||||||
|
if nodes.is_empty():
|
||||||
|
return "NONE"
|
||||||
|
var n := nodes[0] as Node3D
|
||||||
|
if n == null:
|
||||||
|
return "not Node3D"
|
||||||
|
return "vis=%s pos=%s" % [n.is_visible_in_tree(), str(n.global_position.round())]
|
||||||
|
|
||||||
|
|
||||||
|
func _button_style(accent: Color) -> StyleBoxFlat:
|
||||||
|
var s := StyleBoxFlat.new()
|
||||||
|
s.bg_color = Color(0.10, 0.07, 0.03, 0.80)
|
||||||
|
s.set_corner_radius_all(18)
|
||||||
|
s.corner_detail = 6
|
||||||
|
s.set_border_width_all(3)
|
||||||
|
s.border_color = accent
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# ── Menu ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _return_to_menu() -> void:
|
||||||
|
get_tree().paused = false
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
get_tree().change_scene_to_file("res://MainMenu.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Lifecycle ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_hint_pulse = fmod(_hint_pulse + delta * 2.2, TAU)
|
||||||
|
var active := Controls.use_touch_ui() and not get_tree().paused
|
||||||
|
# The game-over / pause screen owns the display when paused — stand down and drop any
|
||||||
|
# held movement so the bull doesn't coast on a stuck joystick.
|
||||||
|
if not active:
|
||||||
|
if _surface.visible:
|
||||||
|
_surface.visible = false
|
||||||
|
if _stick_active:
|
||||||
|
_end_stick()
|
||||||
|
_touches.clear()
|
||||||
|
_touch_owner.clear()
|
||||||
|
_pinching = false
|
||||||
|
return
|
||||||
|
|
||||||
|
_surface.visible = true
|
||||||
|
if _player == null:
|
||||||
|
var players := get_tree().get_nodes_in_group(&"player")
|
||||||
|
if not players.is_empty():
|
||||||
|
_player = players[0]
|
||||||
|
_surface.queue_redraw() # cooldown overlays animate every frame
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d4ky4pbu70bo3
|
||||||
+12
@@ -11,6 +11,7 @@ const _ROMAN := "res://fonts/Cinzel.ttf"
|
|||||||
|
|
||||||
static var _title: FontFile = null
|
static var _title: FontFile = null
|
||||||
static var _body: FontFile = null
|
static var _body: FontFile = null
|
||||||
|
static var _body_bold: FontVariation = null
|
||||||
|
|
||||||
|
|
||||||
## Ornate display face — big banners, menu title, win/lose text.
|
## Ornate display face — big banners, menu title, win/lose text.
|
||||||
@@ -27,6 +28,17 @@ static func body() -> FontFile:
|
|||||||
return _body
|
return _body
|
||||||
|
|
||||||
|
|
||||||
|
## Emboldened Roman face for small, high-legibility bits (keycaps, badges) where
|
||||||
|
## Cinzel's thin regular strokes wash out. Faked via FontVariation since only the
|
||||||
|
## regular weight ships, so no extra .ttf is needed.
|
||||||
|
static func body_bold() -> FontVariation:
|
||||||
|
if _body_bold == null:
|
||||||
|
_body_bold = FontVariation.new()
|
||||||
|
_body_bold.base_font = body()
|
||||||
|
_body_bold.variation_embolden = 0.5
|
||||||
|
return _body_bold
|
||||||
|
|
||||||
|
|
||||||
static func _load(path: String) -> FontFile:
|
static func _load(path: String) -> FontFile:
|
||||||
if ResourceLoader.exists(path):
|
if ResourceLoader.exists(path):
|
||||||
var res := load(path)
|
var res := load(path)
|
||||||
|
|||||||
+21
-4
@@ -21,15 +21,15 @@ const SHADOW := Color(0.00, 0.00, 0.00, 0.55)
|
|||||||
## HUD tiles or CREAM for a hot/hover state; a transparent accent draws a frameless
|
## HUD tiles or CREAM for a hot/hover state; a transparent accent draws a frameless
|
||||||
## plate (still shadowed).
|
## plate (still shadowed).
|
||||||
static func plaque(bg: Color, accent: Color = BORDER,
|
static func plaque(bg: Color, accent: Color = BORDER,
|
||||||
corner: int = 3, h_margin: float = 16.0, v_margin: float = 8.0,
|
corner: int = 1, h_margin: float = 16.0, v_margin: float = 8.0,
|
||||||
shadow: bool = true) -> StyleBoxFlat:
|
shadow: bool = true) -> StyleBoxFlat:
|
||||||
var s := StyleBoxFlat.new()
|
var s := StyleBoxFlat.new()
|
||||||
s.bg_color = bg
|
s.bg_color = bg
|
||||||
s.set_corner_radius_all(corner)
|
s.set_corner_radius_all(corner)
|
||||||
s.corner_detail = 6
|
s.corner_detail = 1
|
||||||
if accent.a > 0.0:
|
if accent.a > 0.0:
|
||||||
s.set_border_width_all(2)
|
s.set_border_width_all(1)
|
||||||
s.border_width_bottom = 5
|
s.border_width_bottom = 2
|
||||||
s.border_color = accent
|
s.border_color = accent
|
||||||
s.content_margin_left = h_margin
|
s.content_margin_left = h_margin
|
||||||
s.content_margin_right = h_margin
|
s.content_margin_right = h_margin
|
||||||
@@ -57,6 +57,23 @@ static func badge(h_margin: float = 8.0, v_margin: float = 2.0) -> StyleBoxFlat:
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
## Round token for the run map — a filled disc with an even border and the shared warm
|
||||||
|
## shadow. Unlike plaque()'s chiselled tablet the border is uniform, so it reads as a
|
||||||
|
## marker rather than a plate. A large corner radius clamps to a full circle on any pip
|
||||||
|
## size. Keeps map pips inside the single styling source instead of a hand-rolled box.
|
||||||
|
static func disc(fill: Color, border: Color, border_w: int = 3) -> StyleBoxFlat:
|
||||||
|
var s := StyleBoxFlat.new()
|
||||||
|
s.bg_color = fill
|
||||||
|
s.set_corner_radius_all(999)
|
||||||
|
s.corner_detail = 8
|
||||||
|
s.set_border_width_all(border_w)
|
||||||
|
s.border_color = border
|
||||||
|
s.shadow_color = SHADOW
|
||||||
|
s.shadow_size = 6
|
||||||
|
s.shadow_offset = Vector2(0.0, 3.0)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
## Thin gold rule for separating rows inside a plaque — brighter at the centre than
|
## Thin gold rule for separating rows inside a plaque — brighter at the centre than
|
||||||
## the surrounding trim so it reads as an inlaid line.
|
## the surrounding trim so it reads as an inlaid line.
|
||||||
static func rule() -> StyleBoxFlat:
|
static func rule() -> StyleBoxFlat:
|
||||||
|
|||||||
Reference in New Issue
Block a user