3 Commits

111 changed files with 1481 additions and 2091 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

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

Before

Width:  |  Height:  |  Size: 12 MiB

@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://7cyyof1476yl"
path="res://.godot/imported/intro_seq_0034.png-e9bc2dbf0d75b6ff302c4cc0bfef8e97.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Animations/Intro_sequence/intro_seq_0034.png"
dest_files=["res://.godot/imported/intro_seq_0034.png-e9bc2dbf0d75b6ff302c4cc0bfef8e97.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.
+14
View File
@@ -96,8 +96,22 @@ godot --headless --script tests/gameplay_test.gd # full scene, bone sanity,
# 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
# Output: tests/output/gameplay/motion_00..04.png, ragdoll_trigger.png, ragdoll_result.png # Output: tests/output/gameplay/motion_00..04.png, ragdoll_trigger.png, ragdoll_result.png
# Difficulty simulation (balance tool, ~2 min):
godot --headless --script tests/difficulty_sim.gd # drives a bot bull vs one matador
``` ```
### Difficulty simulation (`tests/difficulty_sim.gd`)
A balance tool, not a pass/fail test. It drives a kinematic "bot bull" against one
real matador across behaviour profiles (CHARGER / KITER / PASSIVE) and reports the
matador's win rate (how often it gores the bull). Use it to tune matador difficulty
from data. The bot bull has NO abilities (it only kills by ramming), so it can't use
roll/slam/kick — real skilled play beats the matador more than these numbers imply;
read the rates as "how well the matador threatens each movement pattern." A rough
target is CHARGER high (blind charging is punished), KITER moderate (throws are a
dodgeable threat), PASSIVE high (standing still dies). Note: as a `--script` entry it
reads the `DP` autoload via `/root/DP`, not the global identifier.
### What the gameplay test catches ### What the gameplay test catches
| Check | What it detects | | Check | What it detects |
|---|---| |---|---|
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://ypxkr3vla3fr" uid="uid://cyscxku0nw055"
path="res://.godot/imported/HUD_0000_ability_JALG.png-c430df7817ae1f22045a2a27b06f60a8.ctex" path="res://.godot/imported/DASH.png-02cbc39ec03a0c79526d7dc333659c48.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://HUD/HUD_0000_ability_JALG.png" source_file="res://HUD/DASH.png"
dest_files=["res://.godot/imported/HUD_0000_ability_JALG.png-c430df7817ae1f22045a2a27b06f60a8.ctex"] dest_files=["res://.godot/imported/DASH.png-02cbc39ec03a0c79526d7dc333659c48.ctex"]
[params] [params]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

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

After

Width:  |  Height:  |  Size: 168 KiB

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://w4yovxyg0104" uid="uid://bdnc3jw1ykydi"
path="res://.godot/imported/HUD_0001_ability_SLAM.png-7c5f88306e1af474db2c2073888feab7.ctex" path="res://.godot/imported/KICK.png-5482be80e8176ed7a0ae69a48ca1f5ed.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://HUD/HUD_0001_ability_SLAM.png" source_file="res://HUD/KICK.png"
dest_files=["res://.godot/imported/HUD_0001_ability_SLAM.png-7c5f88306e1af474db2c2073888feab7.ctex"] dest_files=["res://.godot/imported/KICK.png-5482be80e8176ed7a0ae69a48ca1f5ed.ctex"]
[params] [params]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://x8eewdg65qgr" uid="uid://d25i88cixy4pn"
path="res://.godot/imported/intro_seq_0014.png-f8346c8267324ffe151c42113f79700d.ctex" path="res://.godot/imported/ROLL.png-306dafcb4b6c13eded9a925aa6d2f62a.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://Animations/Intro_sequence/intro_seq_0014.png" source_file="res://HUD/ROLL.png"
dest_files=["res://.godot/imported/intro_seq_0014.png-f8346c8267324ffe151c42113f79700d.ctex"] dest_files=["res://.godot/imported/ROLL.png-306dafcb4b6c13eded9a925aa6d2f62a.ctex"]
[params] [params]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://nw2pvct1kqcd" uid="uid://bxplqrp4mc5yh"
path="res://.godot/imported/intro_seq_0015.png-1a3c8239c9c1c61cd711f23fcc29af50.ctex" path="res://.godot/imported/SLAM.png-b5036a739251be1240a24080e28d7a8e.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://Animations/Intro_sequence/intro_seq_0015.png" source_file="res://HUD/SLAM.png"
dest_files=["res://.godot/imported/intro_seq_0015.png-1a3c8239c9c1c61cd711f23fcc29af50.ctex"] dest_files=["res://.godot/imported/SLAM.png-b5036a739251be1240a24080e28d7a8e.ctex"]
[params] [params]
+6
View File
@@ -15,6 +15,10 @@ var _aerial_pitch: float = 0.0
var charge_pitch_target: float = 0.0 var charge_pitch_target: float = 0.0
var _charge_pitch_curr: float = 0.0 var _charge_pitch_curr: float = 0.0
# When true, another system (the roll's ball-tumble) owns the bull's full
# orientation — skip writing body wobble so we don't fight it.
var external_rotation: bool = false
# Tail — Verlet chain simulation # Tail — Verlet chain simulation
const TAIL_BONES: Array = [ const TAIL_BONES: Array = [
&"tail_1", &"tail_2", &"tail_3", &"tail_4", &"tail_1", &"tail_2", &"tail_3", &"tail_4",
@@ -128,6 +132,8 @@ func _wobble_body(delta: float) -> void:
_aerial_pitch = lerpf(_aerial_pitch, target_pitch, delta * DP.f("aerial_pitch_speed")) _aerial_pitch = lerpf(_aerial_pitch, target_pitch, delta * DP.f("aerial_pitch_speed"))
_charge_pitch_curr = lerpf(_charge_pitch_curr, charge_pitch_target, delta * DP.f("charge_head_speed")) _charge_pitch_curr = lerpf(_charge_pitch_curr, charge_pitch_target, delta * DP.f("charge_head_speed"))
if external_rotation:
return
_bull.rotation.x = _tilt_x + _aerial_pitch + _charge_pitch_curr _bull.rotation.x = _tilt_x + _aerial_pitch + _charge_pitch_curr
_bull.rotation.z = _tilt_z _bull.rotation.z = _tilt_z
+30 -3
View File
@@ -16,6 +16,12 @@ var _shake_amount: float = 0.0
var _shake_decay: float = 0.0 var _shake_decay: float = 0.0
var _user_fov: float = 0.0 var _user_fov: float = 0.0
# Trailing-camera state: _focus is the smoothed point the camera frames (it lags
# the bull), _drag_offset is the velocity-driven trail that slides it behind.
var _focus: Vector3 = Vector3.ZERO
var _drag_offset: Vector3 = Vector3.ZERO
var _focus_init: bool = false
func _ready() -> void: func _ready() -> void:
set_as_top_level(true) set_as_top_level(true)
@@ -48,11 +54,12 @@ func _process(delta: float) -> void:
) )
var target := get_parent() as Node3D var target := get_parent() as Node3D
var focus := _drag_focus(target, delta)
global_position = target.global_position + offset global_position = focus + offset
look_at(target.global_position, Vector3.UP) look_at(focus, Vector3.UP)
_update_dof(target.global_position) _update_dof(focus)
if _shake_amount > 0.005: if _shake_amount > 0.005:
global_position += Vector3( global_position += Vector3(
@@ -64,6 +71,26 @@ func _process(delta: float) -> void:
_shake_amount = move_toward(_shake_amount, 0.0, _shake_decay * delta) _shake_amount = move_toward(_shake_amount, 0.0, _shake_decay * delta)
# Smoothed follow point that drags behind the bull. The target trail sits opposite
# the bull's flat velocity (cam_drag metres per m/s); the offset and the focus both
# ease toward their targets so the camera lags and settles rather than snapping.
func _drag_focus(target: Node3D, delta: float) -> Vector3:
var pos := target.global_position
if not _focus_init:
_focus = pos
_focus_init = true
var vel := Vector3.ZERO
if target is CharacterBody3D:
vel = (target as CharacterBody3D).velocity
vel.y = 0.0
var drag_target := -vel * DP.f("cam_drag")
_drag_offset = _drag_offset.lerp(drag_target, clampf(DP.f("cam_drag_speed") * delta, 0.0, 1.0))
_focus = _focus.lerp(pos + _drag_offset, clampf(DP.f("cam_follow_lerp") * delta, 0.0, 1.0))
return _focus
func trigger_hit(strength: float) -> void: func trigger_hit(strength: float) -> void:
_shake_amount = strength * 0.5 _shake_amount = strength * 0.5
_shake_decay = _shake_amount / 0.4 _shake_decay = _shake_amount / 0.4
+8 -5
View File
@@ -387,12 +387,15 @@ func _reconcile(delta: float) -> void:
if scene != null: if scene != null:
_collect(scene, col, hit, bone, anim, wanted) _collect(scene, col, hit, bone, anim, wanted)
# Drop overlays whose shape is gone or no longer wanted. # Drop overlays whose shape is gone or no longer wanted. The overlay is a child of
# the collision shape, so it's freed with it — hold it as Variant, since assigning a
# freed instance to any *typed* var (even Object) throws before is_instance_valid()
# can filter it out.
for id: int in _overlays.keys(): for id: int in _overlays.keys():
var mi: MeshInstance3D = _overlays[id] var mi: Variant = _overlays[id]
if not wanted.has(id) or not is_instance_valid(mi): if not wanted.has(id) or not is_instance_valid(mi):
if is_instance_valid(mi): if is_instance_valid(mi):
mi.queue_free() (mi as Node).queue_free()
_overlays.erase(id) _overlays.erase(id)
# Add overlays for newly seen shapes. # Add overlays for newly seen shapes.
for id: int in wanted: for id: int in wanted:
@@ -442,9 +445,9 @@ func _attach(cs: CollisionShape3D, is_area: bool) -> MeshInstance3D:
func _clear_overlays() -> void: func _clear_overlays() -> void:
for id: int in _overlays: for id: int in _overlays:
var mi: MeshInstance3D = _overlays[id] var mi: Variant = _overlays[id]
if is_instance_valid(mi): if is_instance_valid(mi):
mi.queue_free() (mi as Node).queue_free()
_overlays.clear() _overlays.clear()
+70 -47
View File
@@ -22,25 +22,17 @@ func _ready() -> void:
func _register_all() -> void: func _register_all() -> void:
# ── Movement ────────────────────────────────────────────────────────────── # ── Movement ──────────────────────────────────────────────────────────────
_reg_f("Movement", "thrust_impulse", 18.0, 1.0, 200.0) # WASD/arrows drive the bull: move_accel ramps flat velocity toward max_speed in
_reg_f("Movement", "thrust_vertical", 2.0, 0.0, 20.0) # the input direction; move_friction bleeds it off when no direction is held.
_reg_f("Movement", "thrust_min_angle", 15.0, 0.0, 90.0, 1.0)
_reg_f("Movement", "thrust_max_angle", 90.0, 0.0, 180.0, 1.0)
_reg_f("Movement", "thrust_charge_time", 0.5, 0.05, 2.0, 0.05)
_reg_f("Movement", "max_speed", 50.0, 2.0, 1000.0) _reg_f("Movement", "max_speed", 50.0, 2.0, 1000.0)
_reg_f("Movement", "roll_damping", 0.15, 0.01, 0.99, 0.01) _reg_f("Movement", "move_accel", 120.0, 5.0, 600.0)
_reg_f("Movement", "move_friction", 90.0, 5.0, 600.0)
_reg_f("Movement", "jump_velocity", 4.5, 1.0, 200.0) _reg_f("Movement", "jump_velocity", 4.5, 1.0, 200.0)
_reg_f("Movement", "visual_turn_speed", 12.0, 1.0, 200.0) _reg_f("Movement", "visual_turn_speed", 12.0, 1.0, 200.0)
_reg_f("Movement", "bounce_restitution", 0.55, 0.0, 2.0, 0.05) _reg_f("Movement", "bounce_restitution", 0.55, 0.0, 2.0, 0.05)
_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)
_reg_f("Movement", "kick_impulse", 8.0, 1.0, 50.0)
_reg_f("Movement", "kick_interval", 0.25, 0.05, 1.0, 0.01)
_reg_f("Movement", "kick_vertical", 0.0, 0.0, 15.0)
_reg_f("Movement", "kick_force_var", 0.2, 0.0, 1.0, 0.01)
_reg_f("Movement", "kick_spread", 10.0, 0.0, 45.0, 1.0)
_reg_f("Movement", "charge_kick_mult", 2.0, 1.0, 5.0)
# ── 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)
@@ -63,10 +55,7 @@ func _register_all() -> void:
_reg_f("Tail", "tail_body_radius", 0.35, 0.05, 2.0, 0.05) _reg_f("Tail", "tail_body_radius", 0.35, 0.05, 2.0, 0.05)
_reg_f("Tail", "tail_wag_freq", 4.0, 0.5, 15.0) _reg_f("Tail", "tail_wag_freq", 4.0, 0.5, 15.0)
_reg_f("Tail", "tail_wag_idle", 0.3, 0.0, 2.0, 0.05) _reg_f("Tail", "tail_wag_idle", 0.3, 0.0, 2.0, 0.05)
# ── Charge (both mouse buttons) ────────────────────────────────────────── # ── Charge pose (horns-down lean during dash / roll) ─────────────────────
_reg_f("Charge", "bull_charge_accel", 80.0, 1.0, 400.0)
_reg_f("Charge", "bull_charge_max_speed", 76.0, 5.0, 200.0)
_reg_f("Charge", "charge_ramp_time", 0.75, 0.1, 5.0, 0.05)
_reg_f("Charge", "charge_head_pitch", 0.3, 0.0, 1.2, 0.01) _reg_f("Charge", "charge_head_pitch", 0.3, 0.0, 1.2, 0.01)
_reg_f("Charge", "charge_head_speed", 5.0, 0.5, 20.0) _reg_f("Charge", "charge_head_speed", 5.0, 0.5, 20.0)
# ── Body ────────────────────────────────────────────────────────────────── # ── Body ──────────────────────────────────────────────────────────────────
@@ -96,10 +85,21 @@ func _register_all() -> void:
_reg_f("Camera", "cam_spring_length", 7.0, 1.0, 30.0) _reg_f("Camera", "cam_spring_length", 7.0, 1.0, 30.0)
_reg_f("Camera", "cam_min_vert_angle", -60.0, -90.0, 0.0, 1.0) _reg_f("Camera", "cam_min_vert_angle", -60.0, -90.0, 0.0, 1.0)
_reg_f("Camera", "cam_max_vert_angle", 45.0, 0.0, 90.0, 1.0) _reg_f("Camera", "cam_max_vert_angle", 45.0, 0.0, 90.0, 1.0)
# Trailing "drag" camera: the focus point lags the bull and slides opposite to
# its velocity, so the camera visibly drags behind during a fast charge.
# cam_drag = metres of trail per m/s of bull speed (0 = rigid follow)
# cam_drag_speed = how fast the trail offset eases toward its target
# cam_follow_lerp = how tightly the focus point chases the bull (higher = snappier)
_reg_f("Camera", "cam_drag", 0.12, 0.0, 1.0, 0.01)
_reg_f("Camera", "cam_drag_speed", 3.5, 0.5, 20.0)
_reg_f("Camera", "cam_follow_lerp", 10.0, 1.0, 40.0)
# ── Matador ─────────────────────────────────────────────────────────────── # ── Matador ───────────────────────────────────────────────────────────────
_reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0) _reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0)
_reg_f("Matador", "mat_walk_speed", 4.0, 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
# they stay off the benches). Tune in the console to hug the bench edge.
_reg_f("Matador", "arena_spawn_radius", 10.0, 2.0, 22.0, 0.5)
_reg_f("Matador", "mat_idle_min", 0.5, 0.0, 5.0, 0.1) _reg_f("Matador", "mat_idle_min", 0.5, 0.0, 5.0, 0.1)
_reg_f("Matador", "mat_idle_max", 2.5, 0.5, 10.0, 0.1) _reg_f("Matador", "mat_idle_max", 2.5, 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)
@@ -110,33 +110,55 @@ func _register_all() -> void:
_reg_f("Matador", "mat_joint_twist", 40.0, 0.0, 180.0, 1.0) _reg_f("Matador", "mat_joint_twist", 40.0, 0.0, 180.0, 1.0)
_reg_f("Matador", "mat_ragdoll_damp", 1.5, 0.0, 10.0, 0.1) _reg_f("Matador", "mat_ragdoll_damp", 1.5, 0.0, 10.0, 0.1)
_reg_f("Matador", "mat_flee_range", 3.5, 2.0, 40.0) _reg_f("Matador", "mat_flee_range", 3.5, 2.0, 40.0)
_reg_f("Matador", "mat_flee_speed", 5.5, 0.5, 12.0) _reg_f("Matador", "mat_flee_speed", 7.0, 0.5, 12.0)
_reg_f("Matador", "mat_charge_speed", 10.0, 1.0, 30.0) _reg_f("Matador", "mat_charge_speed", 10.0, 1.0, 30.0)
_reg_f("Matador", "mat_brace_range", 5.0, 2.0, 20.0) _reg_f("Matador", "mat_brace_range", 5.0, 2.0, 20.0)
_reg_f("Matador", "mat_brace_duration", 0.7, 0.05, 1.5, 0.05) _reg_f("Matador", "mat_brace_duration", 0.7, 0.05, 1.5, 0.05)
_reg_f("Matador", "mat_commit_dist", 2.5, 0.5, 6.0, 0.1) _reg_f("Matador", "mat_commit_dist", 2.5, 0.5, 6.0, 0.1)
_reg_f("Matador", "mat_step_speed", 3.5, 2.0, 20.0) _reg_f("Matador", "mat_step_speed", 6.5, 2.0, 20.0)
_reg_f("Matador", "mat_step_duration", 0.35, 0.1, 1.0, 0.05) _reg_f("Matador", "mat_step_duration", 0.35, 0.1, 1.0, 0.05)
# How strongly the sidestep biases toward the bull so the blade sweeps through # How strongly the sidestep biases toward the bull so the blade sweeps through
# its path during the pass (0 = pure lateral dodge, 1 = straight at the bull). # its path during the pass (0 = pure lateral dodge, 1 = straight at the bull).
_reg_f("Matador", "mat_pass_lunge", 0.35, 0.0, 1.0, 0.05) _reg_f("Matador", "mat_pass_lunge", 0.35, 0.0, 1.0, 0.05)
_reg_f("Matador", "mat_dodge_cooldown", 2.5, 0.5, 12.0, 0.25) _reg_f("Matador", "mat_dodge_cooldown", 2.5, 0.5, 12.0, 0.25)
_reg_f("Matador", "mat_dodge_chance", 0.55, 0.0, 1.0, 0.05) _reg_f("Matador", "mat_dodge_chance", 0.72, 0.0, 1.0, 0.05)
_reg_f("Matador", "mat_attack_range", 8.0, 2.0, 20.0) _reg_f("Matador", "mat_attack_range", 8.0, 2.0, 20.0)
_reg_f("Matador", "mat_attack_speed", 5.5, 1.0, 12.0) _reg_f("Matador", "mat_attack_speed", 7.5, 1.0, 12.0)
_reg_f("Matador", "mat_attack_duration", 1.8, 0.5, 5.0, 0.1) _reg_f("Matador", "mat_attack_duration", 1.8, 0.5, 5.0, 0.1)
_reg_f("Matador", "mat_attack_min_dist", 3.0, 0.5, 6.0, 0.1) _reg_f("Matador", "mat_attack_min_dist", 3.2, 0.5, 6.0, 0.1)
# Forward drive during the opening of a melee swing — turns the stationary stab # Forward drive during the opening of a melee swing — turns the stationary stab
# into an estocada lunge so the blade covers ground toward the bull. # into an estocada lunge so the blade covers ground toward the bull.
_reg_f("Matador", "mat_lunge_speed", 9.0, 0.0, 20.0) _reg_f("Matador", "mat_lunge_speed", 13.0, 0.0, 20.0)
_reg_f("Matador", "mat_lunge_time", 0.18, 0.0, 0.6, 0.01) _reg_f("Matador", "mat_lunge_time", 0.22, 0.0, 0.6, 0.01)
# Reach of the reach-based stab (the reliable melee hit) and its re-strike gap.
# The bull dies if it comes inside mat_stab_reach of a matador that's attacking,
# bracing, or passing with a drawn blade — closing in is genuinely deadly.
_reg_f("Matador", "mat_stab_reach", 1.7, 0.5, 4.0, 0.1)
_reg_f("Matador", "mat_stab_cooldown", 0.6, 0.1, 3.0, 0.1)
# Half-angle (deg) the matador must be facing the bull within for a stab to land.
# Stops "sword's off to the side but I still died" — you're only gored when the
# blade is actually presented at you.
_reg_f("Matador", "mat_stab_arc", 55.0, 5.0, 180.0, 5.0)
_reg_f("Matador", "mat_roll_chance", 0.35, 0.0, 1.0, 0.05) _reg_f("Matador", "mat_roll_chance", 0.35, 0.0, 1.0, 0.05)
_reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05) _reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05)
# Playback rate of the Draw_weapon clip used for both drawing and sheathing — # Playback rate of the Draw_weapon clip used for both drawing and sheathing —
# higher = snappier unholster/holster. # higher = snappier unholster/holster.
_reg_f("Matador", "mat_draw_speed", 1.25, 0.25, 3.0, 0.05) _reg_f("Matador", "mat_draw_speed", 1.9, 0.25, 3.0, 0.05)
# Cross-fade between matador animation clips — smooths Run/Attack/Taunt cuts. # Cross-fade between matador animation clips — smooths Run/Attack/Taunt cuts.
_reg_f("Matador", "mat_anim_blend", 0.12, 0.0, 0.5, 0.01) _reg_f("Matador", "mat_anim_blend", 0.12, 0.0, 0.5, 0.01)
# ── Matador heuristics (utility AI) ───────────────────────────────────────
# Every mat_decide_interval seconds the matador scores its options — dodge the
# pass, close for a strike, retreat, throw the blade, or strut/taunt — and picks
# the highest. These weights bias the personality: crank aggression for a reckless
# bullfighter, caution for a slippery one. mat_w_commit is a stickiness bonus for
# the current choice so it doesn't dither between two near-tied options.
_reg_f("Matador", "mat_decide_interval", 0.2, 0.05, 1.0, 0.05)
_reg_f("Matador", "mat_w_aggression", 1.4, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_w_caution", 1.0, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_w_flee", 0.8, 0.0, 3.0, 0.05)
_reg_f("Matador", "mat_w_showmanship", 1.0, 0.0, 3.0, 0.05)
# 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)
# ── 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
@@ -157,17 +179,20 @@ func _register_all() -> void:
_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) ────────────────────────────────── # ── Sword throw (matador ranged attack) ──────────────────────────────────
# Probability of committing to a throw each time the matador becomes eligible;
# kept low so they favour closing in for a melee Attack over throwing.
_reg_f("Sword", "mat_throw_chance", 0.15, 0.0, 1.0, 0.05)
# Delay before a fresh sword appears in the holster after one is thrown. # Delay before a fresh sword appears in the holster after one is thrown.
_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.7, 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", 18.0, 5.0, 60.0) _reg_f("Sword", "mat_throw_speed", 24.0, 5.0, 60.0)
_reg_f("Sword", "mat_throw_spin", 14.0, 0.0, 40.0) _reg_f("Sword", "mat_throw_spin", 14.0, 0.0, 40.0)
# Gravity scale on the thrown blade (low = flies straight to the aim point) and
# the vertical aim offset from the bull origin down onto its low collision body.
_reg_f("Sword", "mat_throw_gravity", 0.25, 0.0, 1.0, 0.05)
_reg_f("Sword", "mat_throw_aim_y", -0.45,-1.5, 0.5, 0.05)
# Aim scatter (± degrees) so thrown blades can be read and dodged on the move.
_reg_f("Sword", "mat_throw_spread", 11.0, 0.0, 30.0, 0.5)
_reg_f("Sword", "mat_throw_arm_cock", 70.0,-180.0, 180.0, 1.0) _reg_f("Sword", "mat_throw_arm_cock", 70.0,-180.0, 180.0, 1.0)
_reg_f("Sword", "mat_throw_arm_release", -90.0,-180.0, 180.0, 1.0) _reg_f("Sword", "mat_throw_arm_release", -90.0,-180.0, 180.0, 1.0)
_reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0) _reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0)
@@ -186,23 +211,21 @@ func _register_all() -> void:
_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", 2.0, 0.5, 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 (boulder charge) ───────────────────────────────────────────────── # ── Roll (Rammus Powerball) ───────────────────────────────────────────────
# A held-momentum roll: inherits your current charge, ramps toward roll_max_speed, # The bull curls into a ball and ACCELERATES the longer it rolls, from
# and RICOCHETS off walls with a speed boost (bank shots build ludicrous speed). # roll_base_speed up to roll_max_speed over roll_rampup_time. Turning is wide
_reg_f("Roll", "roll_cooldown", 3.0, 0.5, 15.0, 0.1) # (momentum steering via roll_turn). On slamming into a matador the ball POPS:
_reg_f("Roll", "roll_duration", 2.5, 0.5, 6.0, 0.1) # the target is launched skyward (roll_knockup) and the roll ends.
_reg_f("Roll", "roll_min_speed", 30.0, 5.0, 120.0) # launch floor (combines charge) _reg_f("Roll", "roll_cooldown", 3.0, 0.5, 15.0, 0.1)
_reg_f("Roll", "roll_max_speed", 95.0, 10.0, 200.0) # natural top from acceleration _reg_f("Roll", "roll_duration", 2.5, 0.5, 6.0, 0.1) # max roll time
_reg_f("Roll", "roll_accel", 55.0, 0.0, 300.0) _reg_f("Roll", "roll_base_speed", 28.0, 5.0, 120.0) # speed at the start of the roll
# Per-second speed retained above roll_max_speed — low friction so wall-boosted _reg_f("Roll", "roll_max_speed", 95.0, 10.0, 200.0) # top speed once fully ramped
# momentum bleeds off slowly rather than snapping back to the cruise speed. _reg_f("Roll", "roll_rampup_time", 2.0, 0.2, 6.0, 0.1) # time to reach top speed
_reg_f("Roll", "roll_momentum", 0.9, 0.1, 0.99, 0.01) _reg_f("Roll", "roll_turn", 7.0, 0.2, 20.0) # steer rate (WASD/arrows; higher = tighter)
_reg_f("Roll", "roll_turn", 3.5, 0.2, 20.0) # steer rate (sluggish = momentum feel) _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_wall_boost", 1.25, 1.0, 2.0, 0.05) # speed × on each ricochet _reg_f("Roll", "roll_hit_radius", 2.4, 0.5, 6.0, 0.1) # contact radius that pops the ball
_reg_f("Roll", "roll_wall_cap", 150.0, 10.0, 300.0) # ceiling reachable via boosts _reg_f("Roll", "roll_hit_strength", 22.0, 1.0, 40.0) # horizontal launch on the pop
_reg_f("Roll", "roll_hit_radius", 2.4, 0.5, 6.0, 0.1) _reg_f("Roll", "roll_knockup", 11.0, 0.0, 25.0) # vertical knock-up on the pop
_reg_f("Roll", "roll_hit_strength", 22.0, 1.0, 40.0)
_reg_f("Roll", "roll_hit_up", 4.5, 0.0, 15.0)
# ── Debug overlays (see debug_draw.gd) ─────────────────────────────────── # ── Debug overlays (see debug_draw.gd) ───────────────────────────────────
_reg_b("Debug", "show_collisions", false) _reg_b("Debug", "show_collisions", false)
_reg_b("Debug", "show_hitboxes", false) _reg_b("Debug", "show_hitboxes", false)
BIN
View File
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://fu84fchl1im6"
path="res://.godot/imported/Cinzel.ttf-b9b78caa7497d2ec78b922cb77f34333.fontdata"
[deps]
source_file="res://fonts/Cinzel.ttf"
dest_files=["res://.godot/imported/Cinzel.ttf-b9b78caa7497d2ec78b922cb77f34333.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://rcfn6xhs7fur"
path="res://.godot/imported/CinzelDecorative-Bold.ttf-fd8dd0c7ef2ceb6acaeb4b78a3c0a47b.fontdata"
[deps]
source_file="res://fonts/CinzelDecorative-Bold.ttf"
dest_files=["res://.godot/imported/CinzelDecorative-Bold.ttf-fd8dd0c7ef2ceb6acaeb4b78a3c0a47b.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cgppsmy8krkhw"
path="res://.godot/imported/CinzelDecorative-Regular.ttf-818749a2a58435ecbb00dae02eb6079a.fontdata"
[deps]
source_file="res://fonts/CinzelDecorative-Regular.ttf"
dest_files=["res://.godot/imported/CinzelDecorative-Regular.ttf-818749a2a58435ecbb00dae02eb6079a.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+33
View File
@@ -0,0 +1,33 @@
class_name GameVersion
extends RefCounted
## Single source of truth for the game's version string.
##
## The numeric version lives in project.godot (application/config/version) so it
## also feeds export metadata; here we read it back and layer on the pre-release
## CHANNEL and a human-readable STAGE label. We're pre-1.0 — every 0.x build is
## understood to be unstable, in-development software (see the recommendations in
## the commit / PR notes for why 0.x is the convention for early game dev).
## Pre-release channel appended to the number, SemVer-style (e.g. 0.1.0-alpha).
const CHANNEL: String = "alpha"
## Friendly stage banner shown to players so it's unmistakably an early build.
const STAGE: String = "Early Development Build"
## Bare numeric version, e.g. "0.1.0". Read from project settings.
static func number() -> String:
return str(ProjectSettings.get_setting("application/config/version", "0.0.0"))
## Full version tag, e.g. "v0.1.0-alpha".
static func string() -> String:
var s := "v" + number()
if not CHANNEL.is_empty():
s += "-" + CHANNEL
return s
## Version tag + stage banner, e.g. "v0.1.0-alpha · Early Development Build".
static func full() -> String:
return "%s · %s" % [string(), STAGE]
+1
View File
@@ -0,0 +1 @@
uid://f4bvu1mnvpiu
+176 -262
View File
@@ -1,27 +1,28 @@
extends CanvasLayer extends CanvasLayer
const _GOLD := Color(1.00, 0.78, 0.22) const _GOLD := Color(1.00, 0.78, 0.22)
const _CREAM := Color(0.88, 0.83, 0.72) const _CREAM := Color(0.93, 0.88, 0.72)
const _MUTED := Color(0.65, 0.60, 0.48) const _MUTED := Color(0.66, 0.60, 0.46)
const _BG := Color(0.06, 0.04, 0.02, 0.78)
const _BADGE := Color(0.16, 0.10, 0.03, 0.90)
const _BORDER := Color(0.65, 0.48, 0.15, 0.55)
# Scoring: each kill scores _KILL_BASE × the live combo count; the combo grows with # Per-ability trim colour, tying each tile (and its key cap) to that ability's FX.
# every kill inside _COMBO_WINDOW seconds of the last and resets when that lapses. # Order matches _ABILITY_ICONS: kick, slam, dash, roll.
const _KILL_BASE: int = 100 const _ABILITY_ACCENT: Array[Color] = [
const _COMBO_WINDOW: float = 3.0 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(1.00, 0.82, 0.32), # Roll — gold
]
const _ABILITY_ICONS: Array[String] = [ const _ABILITY_ICONS: Array[String] = [
"res://HUD/HUD_0000_ability_JALG.png", "res://HUD/KICK.png",
"res://HUD/HUD_0001_ability_SLAM.png", "res://HUD/SLAM.png",
"res://HUD/HUD_0002_ability_DASH.png", "res://HUD/DASH.png",
"res://HUD/HUD_0003_ability_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_slam", &"ability_dash", &"ability_roll",
] ]
# Fallback label shown in a slot whose icon file doesn't exist yet (e.g. ROLL). # Fallback label shown in a slot whose icon file fails to load.
const _ABILITY_NAMES: Array[String] = ["Kick", "Slam", "Dash", "Roll"] const _ABILITY_NAMES: Array[String] = ["Kick", "Slam", "Dash", "Roll"]
var _controls_visible: bool = false var _controls_visible: bool = false
@@ -30,20 +31,21 @@ var _toggle_btn: Button
var _tab_hint: Label var _tab_hint: Label
var _spawner: Node var _spawner: Node
var _player: Node = null var _player: Node = null
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 _health_segments: Array[ColorRect] = [] var _ability_bar: HBoxContainer
var _health_connected: bool = false var _ability_panels: Array[PanelContainer] = []
var _score: int = 0 # Win/lose screen. _game_over latches so the result is shown once; _result_win
var _combo: int = 0 # records which way it went (read by the gameplay test).
var _combo_timer: float = 0.0 const _WIN_VIDEO := "res://videos/bull_win.ogv" # the bull triumphs
var _score_label: Label const _LOSS_VIDEO := "res://videos/matador_win.ogv" # the matador triumphs
var _wave_label: Label
var _combo_label: Label var _game_over: bool = false
var _combo_track: ColorRect var _result_win: bool = false
var _combo_fill: ColorRect var _over_root: Control
var _banner_label: Label var _over_video: VideoStreamPlayer
func _ready() -> void: func _ready() -> void:
@@ -52,22 +54,35 @@ func _ready() -> void:
_build_controls_panel() _build_controls_panel()
_build_tab_hint() _build_tab_hint()
_build_ability_bar() _build_ability_bar()
_build_health_bar() _build_version_label()
_build_scoreboard() _build_game_over()
_find_spawner.call_deferred() _find_spawner.call_deferred()
func _process(delta: float) -> void: # Unobtrusive early-build tag in the bottom-right corner.
_update_combo(delta) func _build_version_label() -> void:
var lbl := Label.new()
lbl.text = GameVersion.full()
lbl.add_theme_color_override("font_color", _MUTED)
lbl.add_theme_font_size_override("font_size", 12)
lbl.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
lbl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
lbl.grow_vertical = Control.GROW_DIRECTION_BEGIN
lbl.offset_right = -14.0
lbl.offset_bottom = -10.0
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(lbl)
func _process(_delta: float) -> void:
if _player == null: if _player == null:
var players := get_tree().get_nodes_in_group(&"player") var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty(): if not players.is_empty():
_player = players[0] _player = players[0]
return return
if not _health_connected: if not _died_connected:
_health_connected = true _died_connected = true
_player.health_changed.connect(_on_health_changed) _player.died.connect(_on_player_died)
_on_health_changed(_player.health)
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]
@@ -87,14 +102,14 @@ func _find_spawner() -> void:
if spawners.is_empty(): if spawners.is_empty():
return return
_spawner = spawners[0] _spawner = spawners[0]
_spawner.matador_killed.connect(_on_matador_killed) _spawner.all_defeated.connect(_on_all_defeated)
_spawner.wave_changed.connect(_on_wave_changed)
_on_wave_changed(_spawner.current_wave())
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 event.physical_keycode == KEY_TAB: if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
_restart()
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:
_return_to_menu() _return_to_menu()
@@ -106,6 +121,12 @@ func _return_to_menu() -> void:
get_tree().change_scene_to_file("res://MainMenu.tscn") get_tree().change_scene_to_file("res://MainMenu.tscn")
func _restart() -> void:
get_tree().paused = false
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
get_tree().reload_current_scene()
func _toggle_controls() -> void: func _toggle_controls() -> void:
_controls_visible = not _controls_visible _controls_visible = not _controls_visible
_controls_panel.visible = _controls_visible _controls_panel.visible = _controls_visible
@@ -113,215 +134,111 @@ func _toggle_controls() -> void:
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls" _toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
func _on_reset_pressed() -> void: # ── Win / lose ──────────────────────────────────────────────────────────────
if get_tree().paused: # The bull takes one clean blade and it's over; clear the last matador and it's a
get_tree().paused = false # victory. Both routes end here — the arena freezes and the result is proclaimed.
if _spawner:
_spawner.reset_spawn() func _on_player_died(cause: String = "") -> void:
_score = 0 _show_game_over(false, cause)
_score_label.text = "SCORE 0"
_reset_combo()
# A bordered, rounded, gold-trimmed panel background — the HUD's one repeated look. func _on_all_defeated() -> void:
# Margins are symmetric (L=R, T=B); pass a transparent border to draw none. _show_game_over(true)
func _panel_style(bg: Color, corner: int, h_margin: float, v_margin: float,
border: Color = _BORDER) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = bg
s.set_corner_radius_all(corner)
if border.a > 0.0:
s.set_border_width_all(1)
s.border_color = border
s.content_margin_left = h_margin
s.content_margin_right = h_margin
s.content_margin_top = v_margin
s.content_margin_bottom = v_margin
return s
func _build_health_bar() -> void: func _show_game_over(win: bool, _cause: String = "") -> void:
var bar := HBoxContainer.new() if _game_over:
bar.add_theme_constant_override("separation", 4)
bar.anchor_left = 0.5
bar.anchor_right = 0.5
bar.anchor_top = 0.0
bar.anchor_bottom = 0.0
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
bar.offset_left = -90.0
bar.offset_top = 16.0
bar.offset_bottom = 16.0
add_child(bar)
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", _panel_style(_BG, 4, 6.0, 6.0))
bar.add_child(panel)
var inner := HBoxContainer.new()
inner.add_theme_constant_override("separation", 3)
panel.add_child(inner)
for i: int in 10:
var seg := ColorRect.new()
seg.custom_minimum_size = Vector2(14.0, 14.0)
seg.color = Color(0.72, 0.08, 0.08)
inner.add_child(seg)
_health_segments.append(seg)
func _on_health_changed(new_health: int) -> void:
for i: int in _health_segments.size():
_health_segments[i].color = Color(0.72, 0.08, 0.08) if i < new_health \
else Color(0.20, 0.10, 0.10)
# ── Scoreboard: score · wave · kill-combo ──────────────────────────────────────
func _build_scoreboard() -> void:
var panel := PanelContainer.new()
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
panel.anchor_left = 1.0
panel.anchor_right = 1.0
panel.anchor_top = 0.0
panel.anchor_bottom = 0.0
panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
panel.offset_left = -172.0
panel.offset_right = -14.0
panel.offset_top = 14.0
panel.add_theme_stylebox_override("panel", _panel_style(_BG, 6, 12.0, 8.0))
add_child(panel)
var vbox := VBoxContainer.new()
vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
vbox.add_theme_constant_override("separation", 2)
panel.add_child(vbox)
_score_label = _score_line(vbox, "SCORE 0", _GOLD, 20)
_wave_label = _score_line(vbox, "WAVE 1", _CREAM, 14)
_combo_label = _score_line(vbox, "", _CREAM, 16)
_combo_track = ColorRect.new()
_combo_track.color = Color(0.0, 0.0, 0.0, 0.4)
_combo_track.custom_minimum_size = Vector2(0.0, 4.0)
_combo_track.mouse_filter = Control.MOUSE_FILTER_IGNORE
_combo_track.visible = false
vbox.add_child(_combo_track)
_combo_fill = ColorRect.new()
_combo_fill.color = _GOLD
_combo_fill.set_anchors_preset(Control.PRESET_FULL_RECT)
_combo_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
_combo_track.add_child(_combo_fill)
# Centre banner that flashes on each new wave.
_banner_label = Label.new()
_banner_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_banner_label.anchor_left = 0.5
_banner_label.anchor_right = 0.5
_banner_label.anchor_top = 0.26
_banner_label.anchor_bottom = 0.26
_banner_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
_banner_label.grow_vertical = Control.GROW_DIRECTION_BOTH
_banner_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_banner_label.add_theme_color_override("font_color", _GOLD)
_banner_label.add_theme_font_size_override("font_size", 44)
_banner_label.modulate.a = 0.0
add_child(_banner_label)
func _score_line(parent: VBoxContainer, text: String, color: Color, size: int) -> Label:
var lbl := Label.new()
lbl.text = text
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
lbl.add_theme_color_override("font_color", color)
lbl.add_theme_font_size_override("font_size", size)
parent.add_child(lbl)
return lbl
# Every kill lengthens the combo; scoring is base × combo so a fast chain is worth
# far more than the same kills spread out — rewarding aggressive, uninterrupted play.
func _on_matador_killed() -> void:
_combo += 1
_combo_timer = _COMBO_WINDOW
_score += _KILL_BASE * _combo
_score_label.text = "SCORE %d" % _score
_pulse(_score_label, 1.15)
if _combo >= 2:
_combo_label.text = "%d× COMBO" % _combo
_combo_label.add_theme_color_override("font_color", _combo_color())
_combo_fill.color = _combo_color()
_combo_track.visible = true
_pulse(_combo_label, 1.25)
else:
_combo_label.text = ""
_combo_track.visible = false
func _on_wave_changed(wave: int) -> void:
_wave_label.text = "WAVE %d" % wave
if wave > 1:
_flash_banner("WAVE %d" % wave)
func _update_combo(delta: float) -> void:
if _combo <= 0:
return return
_combo_timer -= delta _game_over = true
if _combo_timer <= 0.0: _result_win = win
_reset_combo()
elif _combo_track.visible: if _controls_panel:
_combo_fill.anchor_right = clampf(_combo_timer / _COMBO_WINDOW, 0.0, 1.0) _controls_panel.visible = false
if _tab_hint:
_tab_hint.visible = false
# Swap in the matching outcome clip and loop it behind the buttons.
var path := _WIN_VIDEO if win else _LOSS_VIDEO
if ResourceLoader.exists(path):
_over_video.stream = load(path)
_over_video.play()
_over_root.visible = true
_over_root.modulate.a = 0.0
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
get_tree().paused = true
create_tween().tween_property(_over_root, "modulate:a", 1.0, 0.5)
func _reset_combo() -> void: func _build_game_over() -> void:
_combo = 0 _over_root = Control.new()
_combo_timer = 0.0 _over_root.set_anchors_preset(Control.PRESET_FULL_RECT)
_combo_label.text = "" _over_root.process_mode = Node.PROCESS_MODE_ALWAYS
_combo_track.visible = false _over_root.visible = false
add_child(_over_root)
# Full-screen outcome clip (bull win / matador win), looping.
_over_video = VideoStreamPlayer.new()
_over_video.set_anchors_preset(Control.PRESET_FULL_RECT)
_over_video.expand = true
_over_video.process_mode = Node.PROCESS_MODE_ALWAYS
_over_video.mouse_filter = Control.MOUSE_FILTER_IGNORE
_over_video.finished.connect(_over_video.play) # loop the outcome clip
_over_root.add_child(_over_video)
# Buttons overlaid at the bottom of the clip.
var row := HBoxContainer.new()
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 16)
row.anchor_left = 0.5
row.anchor_right = 0.5
row.anchor_top = 1.0
row.anchor_bottom = 1.0
row.grow_horizontal = Control.GROW_DIRECTION_BOTH
row.grow_vertical = Control.GROW_DIRECTION_BEGIN
row.offset_bottom = -48.0
_over_root.add_child(row)
var again := _make_button("Go Again (Enter)")
again.pressed.connect(_restart)
row.add_child(again)
var menu := _make_button("Main Menu")
menu.pressed.connect(_return_to_menu)
row.add_child(menu)
func _combo_color() -> Color: const _SLOT_SEPARATION: int = 10
if _combo >= 8:
return Color(1.0, 0.25, 0.10)
if _combo >= 5:
return Color(1.0, 0.50, 0.10)
if _combo >= 3:
return Color(1.0, 0.85, 0.20)
return _CREAM
func _pulse(ctrl: Control, amount: float) -> void:
ctrl.pivot_offset = ctrl.size * 0.5
ctrl.scale = Vector2.ONE * amount
create_tween().tween_property(ctrl, "scale", Vector2.ONE, 0.25) \
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
func _flash_banner(text: String) -> void:
_banner_label.text = text
_banner_label.modulate.a = 0.0
var tw := create_tween()
tw.tween_property(_banner_label, "modulate:a", 1.0, 0.2)
tw.tween_interval(0.7)
tw.tween_property(_banner_label, "modulate:a", 0.0, 0.6)
func _build_ability_bar() -> void: func _build_ability_bar() -> void:
var bar := HBoxContainer.new() _ability_bar = HBoxContainer.new()
bar.add_theme_constant_override("separation", 10) _ability_bar.add_theme_constant_override("separation", _SLOT_SEPARATION)
bar.anchor_left = 0.5 _ability_bar.anchor_left = 0.5
bar.anchor_right = 0.5 _ability_bar.anchor_right = 0.5
bar.anchor_top = 1.0 _ability_bar.anchor_top = 1.0
bar.anchor_bottom = 1.0 _ability_bar.anchor_bottom = 1.0
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH _ability_bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
bar.grow_vertical = Control.GROW_DIRECTION_BEGIN _ability_bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
bar.offset_left = -160.0 _ability_bar.offset_bottom = -20.0
bar.offset_bottom = -20.0 add_child(_ability_bar)
add_child(bar)
for i: int in 4: for i: int in 4:
bar.add_child(_build_ability_slot(i)) _ability_bar.add_child(_build_ability_slot(i))
_layout_ability_bar()
get_viewport().size_changed.connect(_layout_ability_bar)
# 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.
func _layout_ability_bar() -> void:
var vp := get_viewport().get_visible_rect().size
var slot := clampf(minf(vp.x * 0.055, vp.y * 0.11), 56.0, 104.0)
for panel: PanelContainer in _ability_panels:
panel.custom_minimum_size = Vector2(slot, slot)
var total := slot * 4.0 + float(_SLOT_SEPARATION) * 3.0
_ability_bar.offset_left = -total * 0.5
_ability_bar.offset_right = total * 0.5
func _build_ability_slot(idx: int) -> Control: func _build_ability_slot(idx: int) -> Control:
@@ -330,8 +247,10 @@ func _build_ability_slot(idx: int) -> Control:
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)
panel.add_theme_stylebox_override("panel", _panel_style(_BG, 6, 4.0, 4.0)) panel.add_theme_stylebox_override(
"panel", UiTheme.plaque(UiTheme.LEATHER, _ABILITY_ACCENT[idx], 4, 5.0, 5.0))
vbox.add_child(panel) vbox.add_child(panel)
_ability_panels.append(panel)
var stack := Control.new() var stack := Control.new()
stack.set_anchors_preset(Control.PRESET_FULL_RECT) stack.set_anchors_preset(Control.PRESET_FULL_RECT)
@@ -340,8 +259,12 @@ func _build_ability_slot(idx: int) -> Control:
if ResourceLoader.exists(_ABILITY_ICONS[idx]): if ResourceLoader.exists(_ABILITY_ICONS[idx]):
var icon := TextureRect.new() var icon := TextureRect.new()
icon.texture = load(_ABILITY_ICONS[idx]) icon.texture = load(_ABILITY_ICONS[idx])
# Ignore the texture's native size so a large source PNG doesn't blow the
# slot's minimum size out; scale-to-fit the box while keeping aspect.
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.set_anchors_preset(Control.PRESET_FULL_RECT) icon.set_anchors_preset(Control.PRESET_FULL_RECT)
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
stack.add_child(icon) stack.add_child(icon)
else: else:
var name_lbl := Label.new() var name_lbl := Label.new()
@@ -378,8 +301,11 @@ func _build_ability_slot(idx: int) -> Control:
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_color_override("font_color", _GOLD) key_lbl.add_theme_font_override("font", UiFonts.body())
key_lbl.add_theme_font_size_override("font_size", 12) key_lbl.add_theme_color_override("font_color", _ABILITY_ACCENT[idx].lerp(_CREAM, 0.35))
key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
key_lbl.add_theme_constant_override("outline_size", 3)
key_lbl.add_theme_font_size_override("font_size", 13)
vbox.add_child(key_lbl) vbox.add_child(key_lbl)
return vbox return vbox
@@ -402,7 +328,8 @@ func _build_tab_hint() -> void:
func _build_controls_panel() -> void: func _build_controls_panel() -> void:
_controls_panel = PanelContainer.new() _controls_panel = PanelContainer.new()
_controls_panel.add_theme_stylebox_override("panel", _panel_style(_BG, 7, 14.0, 10.0)) _controls_panel.add_theme_stylebox_override(
"panel", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 16.0, 12.0))
_controls_panel.anchor_left = 0.0 _controls_panel.anchor_left = 0.0
_controls_panel.anchor_top = 1.0 _controls_panel.anchor_top = 1.0
@@ -424,37 +351,22 @@ func _build_controls_panel() -> void:
Controls.get_key_label(&"move_back"), Controls.get_key_label(&"move_back"),
Controls.get_key_label(&"move_right"), Controls.get_key_label(&"move_right"),
] ]
_row(vbox, move_keys, "Move (not intended)", _MUTED) _row(vbox, move_keys, "Move", _CREAM)
_row(vbox, "LMB (hold)", "Right horn", _CREAM)
_row(vbox, "RMB (hold)", "Left horn", _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_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_roll"), "Roll (bank off walls!)", _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, "R", "Respawn matadors (cooldown)", _MUTED)
_row(vbox, "Tab", "Toggle controls", _MUTED) _row(vbox, "Tab", "Toggle controls", _MUTED)
_row(vbox, "Esc", "Back to menu", _MUTED) _row(vbox, "Esc", "Back to menu", _MUTED)
var sep := HSeparator.new() var sep := HSeparator.new()
var sep_style := StyleBoxFlat.new() sep.add_theme_stylebox_override("separator", UiTheme.rule())
sep_style.bg_color = Color(0.65, 0.48, 0.15, 0.30)
sep_style.content_margin_top = 4.0
sep_style.content_margin_bottom = 4.0
sep.add_theme_stylebox_override("separator", sep_style)
vbox.add_child(sep) vbox.add_child(sep)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 8)
vbox.add_child(hbox)
_toggle_btn = _make_button("Show controls") _toggle_btn = _make_button("Show controls")
_toggle_btn.pressed.connect(_toggle_controls) _toggle_btn.pressed.connect(_toggle_controls)
hbox.add_child(_toggle_btn) vbox.add_child(_toggle_btn)
var reset_btn := _make_button("Reset")
reset_btn.pressed.connect(_on_reset_pressed)
hbox.add_child(reset_btn)
func _make_button(label_text: String) -> Button: func _make_button(label_text: String) -> Button:
@@ -462,15 +374,17 @@ func _make_button(label_text: String) -> Button:
btn.text = label_text btn.text = label_text
btn.process_mode = Node.PROCESS_MODE_ALWAYS btn.process_mode = Node.PROCESS_MODE_ALWAYS
var normal := _panel_style(Color(0.18, 0.12, 0.04, 0.90), 5, 12.0, 6.0) var normal := UiTheme.plaque(Color(0.10, 0.07, 0.03, 0.90), _GOLD, 3, 18.0, 9.0)
var hover := _panel_style(Color(0.30, 0.20, 0.06, 0.95), 5, 12.0, 6.0, _GOLD) var hover := UiTheme.plaque(Color(0.16, 0.10, 0.04, 0.96), _CREAM, 3, 18.0, 9.0)
btn.add_theme_stylebox_override("normal", normal) btn.add_theme_stylebox_override("normal", normal)
btn.add_theme_stylebox_override("hover", hover) btn.add_theme_stylebox_override("hover", hover)
btn.add_theme_stylebox_override("pressed", hover) btn.add_theme_stylebox_override("pressed", hover)
btn.add_theme_stylebox_override("focus", hover)
btn.add_theme_font_override("font", UiFonts.body())
btn.add_theme_color_override("font_color", _GOLD) btn.add_theme_color_override("font_color", _GOLD)
btn.add_theme_color_override("font_hover_color", _GOLD) btn.add_theme_color_override("font_hover_color", _CREAM)
btn.add_theme_font_size_override("font_size", 12) btn.add_theme_font_size_override("font_size", 18)
return btn return btn
@@ -481,7 +395,7 @@ func _row(parent: VBoxContainer, key: String, desc: String, desc_color: Color) -
parent.add_child(row) parent.add_child(row)
var badge := PanelContainer.new() var badge := PanelContainer.new()
badge.add_theme_stylebox_override("panel", _panel_style(_BADGE, 3, 6.0, 2.0, Color(0, 0, 0, 0))) badge.add_theme_stylebox_override("panel", UiTheme.badge())
badge.custom_minimum_size = Vector2(100.0, 0.0) badge.custom_minimum_size = Vector2(100.0, 0.0)
row.add_child(badge) row.add_child(badge)
+67
View File
@@ -6,6 +6,10 @@ extends Control
const GAME_SCENE := "res://scene.tscn" const GAME_SCENE := "res://scene.tscn"
const FADE_TIME := 0.6 const FADE_TIME := 0.6
# Arena palette: aged gold on dark leather, warm cream on hover.
const MENU_GOLD := Color(0.85, 0.66, 0.20)
const MENU_CREAM := Color(0.93, 0.88, 0.72)
## Cape centroid per frame (normalized 0-1), detected from red-pixel centroid. ## Cape centroid per frame (normalized 0-1), detected from red-pixel centroid.
## 34 frames at 24 fps = 1.4167 s loop. ## 34 frames at 24 fps = 1.4167 s loop.
const VIDEO_FPS := 24.0 const VIDEO_FPS := 24.0
@@ -90,10 +94,69 @@ func _ready() -> void:
exit_button.pressed.connect(_on_exit_pressed) exit_button.pressed.connect(_on_exit_pressed)
options_back.pressed.connect(_close_subpanels) options_back.pressed.connect(_close_subpanels)
credits_back.pressed.connect(_close_subpanels) credits_back.pressed.connect(_close_subpanels)
_apply_theme()
_build_version_label()
_build_rebind_rows() _build_rebind_rows()
play_button.grab_focus() play_button.grab_focus()
# Version banner in the bottom-right corner so it's clear this is an early build.
func _build_version_label() -> void:
var lbl := Label.new()
lbl.text = GameVersion.full()
lbl.add_theme_font_override("font", UiFonts.body())
lbl.add_theme_font_size_override("font_size", 16)
lbl.add_theme_color_override("font_color", MENU_CREAM)
lbl.add_theme_color_override("font_outline_color", Color.BLACK)
lbl.add_theme_constant_override("outline_size", 4)
lbl.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
lbl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
lbl.grow_vertical = Control.GROW_DIRECTION_BEGIN
lbl.offset_right = -16.0
lbl.offset_bottom = -12.0
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(lbl)
# Give the whole menu the Cinzel look and swap the generic translucent boxes for
# engraved bronze plaques (dark leather fill, gold border with a heavier bottom
# edge for a chiselled-tablet feel).
func _apply_theme() -> void:
_style_button(play_button, true)
for btn: Button in [options_button, credits_button, exit_button, options_back, credits_back]:
_style_button(btn, false)
for path: String in ["OptionsPanel/VBox/TitleLabel", "CreditsPanel/VBox/TitleLabel"]:
var title := get_node_or_null(path) as Label
if title != null:
title.add_theme_font_override("font", UiFonts.title())
title.add_theme_color_override("font_color", MENU_GOLD)
for path: String in ["OptionsPanel/VBox/Hint", "CreditsPanel/VBox/Names"]:
var lbl := get_node_or_null(path) as Label
if lbl != null:
lbl.add_theme_font_override("font", UiFonts.body())
lbl.add_theme_color_override("font_color", MENU_CREAM)
func _style_button(btn: Button, big: bool) -> void:
btn.add_theme_font_override("font", UiFonts.title() if big else UiFonts.body())
btn.add_theme_color_override("font_color", MENU_GOLD)
btn.add_theme_color_override("font_hover_color", MENU_CREAM)
btn.add_theme_color_override("font_focus_color", MENU_CREAM)
btn.add_theme_color_override("font_pressed_color", MENU_CREAM)
btn.add_theme_color_override("font_outline_color", Color(0, 0, 0, 1))
btn.add_theme_constant_override("outline_size", 8 if big else 5)
btn.add_theme_stylebox_override("normal", _plaque(false))
btn.add_theme_stylebox_override("hover", _plaque(true))
btn.add_theme_stylebox_override("pressed", _plaque(true))
btn.add_theme_stylebox_override("focus", _plaque(true))
func _plaque(hot: bool) -> StyleBoxFlat:
var bg := Color(0.14, 0.09, 0.04, 0.94) if hot else Color(0.07, 0.05, 0.03, 0.82)
var trim := MENU_CREAM if hot else MENU_GOLD
return UiTheme.plaque(bg, trim, 2, 24.0, 9.0)
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
if not main_panel.visible or video.stream == null: if not main_panel.visible or video.stream == null:
return return
@@ -117,6 +180,9 @@ func _build_rebind_rows() -> void:
var name_label := Label.new() var name_label := Label.new()
name_label.text = ACTION_LABELS[action] name_label.text = ACTION_LABELS[action]
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL 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_color_override("font_color", MENU_CREAM)
name_label.add_theme_color_override("font_outline_color", Color.BLACK) name_label.add_theme_color_override("font_outline_color", Color.BLACK)
name_label.add_theme_constant_override("outline_size", 4) name_label.add_theme_constant_override("outline_size", 4)
row.add_child(name_label) row.add_child(name_label)
@@ -124,6 +190,7 @@ func _build_rebind_rows() -> void:
var key_button := Button.new() var key_button := Button.new()
key_button.custom_minimum_size = Vector2(160, 0) key_button.custom_minimum_size = Vector2(160, 0)
key_button.text = Controls.get_key_label(action) key_button.text = Controls.get_key_label(action)
_style_button(key_button, false)
key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button)) key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button))
row.add_child(key_button) row.add_child(key_button)
+183 -64
View File
@@ -10,6 +10,10 @@ extends CharacterBody3D
# RAGDOLL → hit by bull, no roll chance # RAGDOLL → hit by bull, no roll chance
enum State { WANDER, FLEE, BRACE, SIDESTEP, ATTACK, ROLL, THROW, RAGDOLL } enum State { WANDER, FLEE, BRACE, SIDESTEP, ATTACK, ROLL, THROW, RAGDOLL }
# The matador's high-level *want*, chosen by a weighted utility vote (see
# _choose_intent). Each intent maps onto one or more concrete States below.
enum Intent { WANDER, FLEE, ATTACK, DODGE, THROW }
signal killed signal killed
var _state: State = State.WANDER var _state: State = State.WANDER
@@ -35,6 +39,8 @@ var _throw_aim_cd: float = 0.0
var _throw_dir: Vector3 = Vector3.ZERO var _throw_dir: Vector3 = Vector3.ZERO
var _steer_dir: Vector3 = Vector3.ZERO var _steer_dir: Vector3 = Vector3.ZERO
var _steer_cd: float = 0.0 var _steer_cd: float = 0.0
var _intent: Intent = Intent.WANDER
var _decide_cd: float = 0.0
var _ole_player: AudioStreamPlayer = null var _ole_player: AudioStreamPlayer = null
var _sword_hand_attach: BoneAttachment3D = null var _sword_hand_attach: BoneAttachment3D = null
var _sword_rest_attach: BoneAttachment3D = null var _sword_rest_attach: BoneAttachment3D = null
@@ -74,11 +80,12 @@ const _STEER_INTERVAL: float = 0.12 # recompute direction at most ~8×/sec
# Side angles (radians) tried when the straight-ahead path is blocked. # Side angles (radians) tried when the straight-ahead path is blocked.
const _STEER_CANDIDATES: Array = [-0.35, 0.35, -0.7, 0.7, -1.1, 1.1, PI] const _STEER_CANDIDATES: Array = [-0.35, 0.35, -0.7, 0.7, -1.1, 1.1, PI]
# Movement tuning # Movement tuning. Ramp/turn rates are deliberately snappy so the matador reads as
const _ACCEL_FACTOR: float = 8.0 # velocity ramp = speed * factor * delta # a quick, dangerous opponent rather than a sluggish one.
const _TURN_MOVE: float = 8.0 # face the travel direction while roaming const _ACCEL_FACTOR: float = 13.0 # velocity ramp = speed * factor * delta
const _TURN_FACE: float = 12.0 # face the bull while engaging const _TURN_MOVE: float = 11.0 # face the travel direction while roaming
const _TURN_SHARP: float = 20.0 # snap onto a sidestep / roll direction const _TURN_FACE: float = 16.0 # face the bull while engaging
const _TURN_SHARP: float = 24.0 # snap onto a sidestep / roll direction
func _ready() -> void: func _ready() -> void:
@@ -97,7 +104,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.8, 2.5) _throw_aim_cd = randf_range(0.3, 1.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()
@@ -148,7 +155,7 @@ func _physics_process(delta: float) -> void:
if _resword_timer <= 0.0: if _resword_timer <= 0.0:
_spawn_sword() _spawn_sword()
if _state != State.RAGDOLL and _bull != null: if _state != State.RAGDOLL and _bull != null:
_update_ai_state() _update_ai_state(delta)
match _state: match _state:
State.WANDER: _tick_wander(delta) State.WANDER: _tick_wander(delta)
@@ -164,42 +171,110 @@ func _physics_process(delta: float) -> void:
_advance_draw(delta) _advance_draw(delta)
func _update_ai_state() -> void: # Utility-driven brain. The physically committed states (a brace, a pass, a roll, a
if _state == State.THROW: # throw wind-up) run to completion on their own timers; from the free states the
# matador re-scores its options every mat_decide_interval and acts on the winner.
func _update_ai_state(delta: float) -> void:
if _state == State.BRACE or _state == State.SIDESTEP \
or _state == State.ROLL or _state == State.THROW:
return return
_decide_cd -= delta
if _decide_cd > 0.0:
return
_decide_cd = DP.f("mat_decide_interval")
var intent := _choose_intent()
_intent = intent
if not _realizes_intent(intent):
_enter_intent(intent)
# Score each intent from weighted heuristics and return the winner. Weights (the
# mat_w_* params) shape the personality; a small commit bonus on the current intent
# stops it flip-flopping between two near-tied options.
func _choose_intent() -> Intent:
var dist := global_position.distance_to(_bull.global_position) var dist := global_position.distance_to(_bull.global_position)
if (_state == State.WANDER or _state == State.FLEE or _state == State.ATTACK) \ var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
and _throw_aim_cd <= 0.0 and is_instance_valid(_sword_node) \ var bull_speed := bull_flat.length()
and dist >= DP.f("mat_throw_min_dist") and dist <= DP.f("mat_throw_range"):
# Roll for a throw; on a miss, wait before becoming eligible again so the # close = 1 right on top of us, 0 at attack range or beyond. `beyond` measures
# matador commits to chasing for a melee Attack instead of throwing early. # how far past throw range the bull is (0 point-blank, 1 at/outside throw range):
if randf() < DP.f("mat_throw_chance"): # the matador only idles when the bull is out there, and engages otherwise.
var reach := maxf(DP.f("mat_attack_range"), 0.5)
var throw_range := maxf(DP.f("mat_throw_range"), 1.0)
var close := clampf(1.0 - dist / reach, 0.0, 1.0)
var beyond := clampf(dist / throw_range, 0.0, 1.0)
# danger blends "the bull is fast and near" with "a charge is aimed at me".
var danger := clampf(bull_speed / maxf(DP.f("mat_charge_speed"), 0.1), 0.0, 1.0) * close
if _is_charge_incoming():
danger = clampf(danger + 0.7, 0.0, 1.0)
# A matador won't commit to a strike it can't bail out of, so a spent dodge
# cooldown makes attacking (and dodging outright) less attractive.
var dodge_ready := 1.0 if _dodge_cd <= 0.0 else 0.35
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")
var score := {
# Only drift when the bull is out past throw range; up close this is ~0.
Intent.WANDER: DP.f("mat_w_showmanship") * 0.3 * beyond,
# Retreat only when the bull is close AND actually dangerous.
Intent.FLEE: DP.f("mat_w_flee") * close * (0.25 + 0.75 * danger),
# Press the attack; ease off a touch when a charge is barrelling in.
Intent.ATTACK: DP.f("mat_w_aggression") * (0.45 + 0.55 * close) \
* (1.0 - 0.55 * danger) * dodge_ready,
# Sidestep / plant against an incoming charge.
Intent.DODGE: DP.f("mat_w_caution") * danger * (1.0 if _dodge_cd <= 0.0 else 0.0),
# Hurl the blade whenever there's a clean lane at range — the ranged threat
# that punishes a bull for hanging back out of horn reach.
Intent.THROW: ((DP.f("mat_w_showmanship") * 0.55 + DP.f("mat_w_aggression") * 0.6) \
* (0.35 + 0.65 * (1.0 - close))) if throw_ok else -1.0,
}
score[_intent] = float(score[_intent]) + DP.f("mat_w_commit")
var best: Intent = Intent.WANDER
var best_score := -INF
for intent: int in score:
if float(score[intent]) > best_score:
best_score = float(score[intent])
best = intent
return best
# True when the current State already carries out the intent, so we needn't restart
# it (restarting ATTACK every tick would reset its swing and stutter the animation).
func _realizes_intent(intent: Intent) -> bool:
match intent:
Intent.WANDER:
return _state == State.WANDER
Intent.FLEE:
return _state == State.FLEE
Intent.ATTACK:
return _state == State.ATTACK
Intent.DODGE:
return _state == State.BRACE or _state == State.SIDESTEP
Intent.THROW:
return _state == State.THROW
return false
func _enter_intent(intent: Intent) -> void:
match intent:
Intent.WANDER:
_state = State.WANDER
_steer_cd = 0.0
_pick_wander_target()
Intent.FLEE:
_state = State.FLEE
_steer_cd = 0.0
Intent.ATTACK:
_steer_cd = 0.0
_start_attack()
Intent.DODGE:
_start_brace()
Intent.THROW:
_start_throw() _start_throw()
return
_throw_aim_cd = randf_range(2.5, 5.0)
match _state:
State.WANDER:
if dist < DP.f("mat_flee_range"):
_state = State.FLEE
elif _dodge_cd <= 0.0 and dist < DP.f("mat_attack_range") and not _is_charge_incoming():
_steer_cd = 0.0
_start_attack()
State.FLEE:
if _dodge_cd <= 0.0 and (dist < DP.f("mat_commit_dist") or _is_charge_incoming()):
_start_brace()
elif dist > DP.f("mat_flee_range") * 1.3:
_state = State.WANDER
_steer_cd = 0.0
_pick_wander_target()
State.BRACE:
pass # brace timer drives transition
State.SIDESTEP:
pass # step timer drives transition
State.ATTACK:
if _attack_timer <= 0.0 or dist > DP.f("mat_attack_range") * 1.5:
_state = State.FLEE
State.ROLL:
pass # roll timer drives transition
# Bull is moving fast and aimed within ~30° of the matador # Bull is moving fast and aimed within ~30° of the matador
@@ -339,16 +414,21 @@ func _tick_brace(delta: float) -> void:
_brace_timer -= delta _brace_timer -= delta
var dist := global_position.distance_to(_bull.global_position) var dist := global_position.distance_to(_bull.global_position)
if dist < DP.f("mat_commit_dist") or _brace_timer <= 0.0: if _will_dodge:
if _will_dodge: # Dodger: wait for the bull to commit, then whip into the lateral pass.
if dist < DP.f("mat_commit_dist") or _brace_timer <= 0.0:
_state = State.SIDESTEP _state = State.SIDESTEP
_step_timer = DP.f("mat_step_duration") _step_timer = DP.f("mat_step_duration")
if _ole_player: if _ole_player:
_ole_player.pitch_scale = randf_range(0.88, 1.12) _ole_player.pitch_scale = randf_range(0.88, 1.12)
_ole_player.play() _ole_player.play()
else: else:
_state = State.FLEE # Planter (estocada): the bull only dies to an active, presented thrust, not to
# brushing a braced matador — so when it commits in, break into a real attack
# SWING (which lunges the blade forward and stabs), otherwise hold then press.
if dist < DP.f("mat_commit_dist") or (_brace_timer <= 0.0 and not _is_charge_incoming()):
_dodge_cd = DP.f("mat_dodge_cooldown") _dodge_cd = DP.f("mat_dodge_cooldown")
_start_attack()
# Pase phase: sharp lateral step biased toward the bull so the drawn blade sweeps # Pase phase: sharp lateral step biased toward the bull so the drawn blade sweeps
@@ -368,6 +448,7 @@ func _tick_sidestep(delta: float) -> void:
velocity.z = dir.z * spd velocity.z = dir.z * spd
_face_point(_bull.global_position, delta, _TURN_SHARP) _face_point(_bull.global_position, delta, _TURN_SHARP)
_play_anim(_ANIM_ATTACK) _play_anim(_ANIM_ATTACK)
_try_melee_hit(DP.f("mat_stab_reach"))
move_and_slide() move_and_slide()
@@ -406,6 +487,7 @@ func _tick_attack(delta: float) -> void:
else: else:
_decelerate(22.0, delta) _decelerate(22.0, delta)
_play_anim(_ANIM_ATTACK) _play_anim(_ANIM_ATTACK)
_try_melee_hit(DP.f("mat_stab_reach"))
else: else:
var dir := _steer_clear(to_bull.normalized(), delta) var dir := _steer_clear(to_bull.normalized(), delta)
_accelerate(dir, DP.f("mat_attack_speed"), delta) _accelerate(dir, DP.f("mat_attack_speed"), delta)
@@ -481,6 +563,10 @@ func _tick_throw(delta: float) -> void:
func _end_throw() -> void: func _end_throw() -> void:
_state = State.FLEE _state = State.FLEE
_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,
# but keep them frequent enough to pressure a bull that hangs back at range.
_throw_aim_cd = randf_range(1.6, 3.2)
_intent = Intent.FLEE
# Blend straight into locomotion so the manually-posed throw arm eases out # Blend straight into locomotion so the manually-posed throw arm eases out
# (a bare resume() would snap and leave the player on whatever it paused on). # (a bare resume() would snap and leave the player on whatever it paused on).
if _anim_player: if _anim_player:
@@ -539,15 +625,20 @@ func _release_sword() -> void:
rb.collision_layer = 0 rb.collision_layer = 0
rb.collision_mask = 1 rb.collision_mask = 1
rb.contact_monitor = true rb.contact_monitor = true
rb.max_contacts_reported = 4 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) get_tree().current_scene.add_child(rb)
rb.global_transform = gx rb.global_transform = gx
sword.reparent(rb, true) sword.reparent(rb, true)
# Physics collider along the blade (GLB +Z), which equals rb-local +Z because # Physics collider along the blade (GLB +Z), which equals rb-local +Z because
# the mesh kept its global transform and rb adopted it. # the mesh kept its global transform and rb adopted it. Fattened so a fast throw
# reliably overlaps the bull's collision spheres instead of tunnelling past.
var cap := CapsuleShape3D.new() var cap := CapsuleShape3D.new()
cap.radius = 0.06 cap.radius = 0.14
cap.height = 1.4 cap.height = 1.4
var cs := CollisionShape3D.new() var cs := CollisionShape3D.new()
cs.shape = cap cs.shape = cap
@@ -555,19 +646,25 @@ func _release_sword() -> void:
cs.rotation_degrees = Vector3(90.0, 0.0, 0.0) cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
rb.add_child(cs) rb.add_child(cs)
# Lead the target: aim where the bull will be when the sword arrives, so a # Aim at the bull's BODY (its collision spheres ride low, ~0.5 m below the
# moving bull isn't simply behind the throw by the time it lands. # origin), leading a moving target so it arrives where the bull will be. The aim
var aim := _throw_dir # keeps its vertical component so the blade drives into the body, not over it.
var speed := maxf(DP.f("mat_throw_speed"), 0.1)
var dir := (_throw_dir + Vector3.UP * 0.0)
if is_instance_valid(_bull): if is_instance_valid(_bull):
var speed := maxf(DP.f("mat_throw_speed"), 0.1) var target := _bull.global_position + Vector3(0.0, DP.f("mat_throw_aim_y"), 0.0)
var flat := _bull.global_position - gx.origin var flat := target - gx.origin
flat.y = 0.0 flat.y = 0.0
var lead := (_bull.global_position + _bull.velocity * (flat.length() / speed)) - gx.origin var flight := flat.length() / speed
lead.y = 0.0 target += _bull.velocity * flight
if lead.length() > 0.1: var to_target := target - gx.origin
aim = lead.normalized() if to_target.length() > 0.1:
var dir := (aim + Vector3.UP * 0.12).normalized() dir = to_target.normalized()
rb.linear_velocity = dir * DP.f("mat_throw_speed") # A little aim scatter so throws are a threat to READ and dodge, not a hitscan —
# a bull that keeps moving can slip them, a stationary one gets pinned.
var spread := deg_to_rad(DP.f("mat_throw_spread"))
dir = dir.rotated(Vector3.UP, randf_range(-spread, spread)).normalized()
rb.linear_velocity = dir * speed
rb.angular_velocity = dir.cross(Vector3.UP).normalized() * -DP.f("mat_throw_spin") rb.angular_velocity = dir.cross(Vector3.UP).normalized() * -DP.f("mat_throw_spin")
var hit_done := [false] var hit_done := [false]
@@ -576,7 +673,7 @@ func _release_sword() -> void:
return return
if body.is_in_group(&"player"): if body.is_in_group(&"player"):
hit_done[0] = true hit_done[0] = true
body.call(&"take_sword_hit") body.call(&"take_sword_hit", "thrown")
) )
var cleanup := get_tree().create_timer(6.0) var cleanup := get_tree().create_timer(6.0)
@@ -623,14 +720,36 @@ func _on_body_entered(body: Node3D) -> void:
func _on_blade_hit(body: Node3D) -> void: func _on_blade_hit(body: Node3D) -> void:
if _state != State.ATTACK and _state != State.SIDESTEP: if _state != State.ATTACK and _state != State.SIDESTEP and _state != State.BRACE:
return return
if not body.is_in_group(&"player"): if not body.is_in_group(&"player"):
return return
if _blade_hit_cd > 0.0: if _blade_hit_cd > 0.0:
return return
_blade_hit_cd = 0.5 _blade_hit_cd = DP.f("mat_stab_cooldown")
body.call(&"take_sword_hit") body.call(&"take_sword_hit", "gored")
# Reach-based stab. A charging bull crosses metres per physics frame, so it easily
# tunnels through the thin blade Area between frames — this distance check is the
# reliable hit. Called from the offensive ticks (active swing / pass) whenever the
# blade is drawn: if the bull is inside reach AND the matador is facing it — the
# blade actually presented, not held off to the side — it's gored.
func _try_melee_hit(reach: float) -> void:
if _blade_hit_cd > 0.0 or not _sword_in_hand or _bull == null:
return
var to_bull := _bull.global_position - global_position
to_bull.y = 0.0
var dist := to_bull.length()
if dist > reach or dist < 0.01:
return
# Only lands within the frontal arc — you're gored on a presented blade, not by
# brushing a matador whose sword is pointing elsewhere.
var facing := Vector3(sin(_mesh.rotation.y), 0.0, cos(_mesh.rotation.y))
if facing.dot(to_bull / dist) < cos(deg_to_rad(DP.f("mat_stab_arc"))):
return
_blade_hit_cd = DP.f("mat_stab_cooldown")
_bull.call(&"take_sword_hit", "gored")
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float, up_boost: float = 0.0) -> void: func _enter_ragdoll(hit_dir: Vector3, bull_speed: float, up_boost: float = 0.0) -> void:
@@ -833,8 +952,8 @@ func _spawn_sword() -> void:
# Blade collider spans the steel (hilt at local origin, blade running +Z out # Blade collider spans the steel (hilt at local origin, blade running +Z out
# to ~1.4 m). Only monitors while the sword is in hand (see _carry_sword). # to ~1.4 m). 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.10 blade_cap.radius = 0.18
blade_cap.height = 1.10 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)
+33 -49
View File
@@ -1,61 +1,53 @@
extends Node3D extends Node3D
signal matador_killed signal matador_killed
signal wave_changed(wave: int) signal all_defeated
const MATADOR := preload("res://Matador.tscn") const MATADOR := preload("res://Matador.tscn")
const RESPAWN_COOLDOWN := 8.0
var _respawn_cooldown: float = 0.0 var _spawn_count: int = 1
var _spawn_count: int = 1 # matadors in the current wave — doubles on each clear
var _wave: int = 1 # wave number, surfaced on the HUD
var _alive: int = 0 var _alive: int = 0
var _resolved: bool = false
func _ready() -> void: func _ready() -> void:
add_to_group(&"matador_spawn") add_to_group(&"matador_spawn")
_spawn_count = maxi(1, int(DP.f("mat_spawn_count"))) _spawn_count = maxi(1, int(DP.f("mat_spawn_count")))
_start_wave(1) # Deferred so it's safe from _ready and matches the previous spawn timing.
# Latest wave number, for a HUD that connects after the first wave has begun.
func current_wave() -> int:
return _wave
func _process(delta: float) -> void:
_respawn_cooldown = maxf(0.0, _respawn_cooldown - delta)
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if event.physical_keycode == KEY_R and _respawn_cooldown <= 0.0:
_respawn_cooldown = RESPAWN_COOLDOWN
reset_spawn()
# Announce a wave and spawn its matadors. Deferred spawn keeps it safe to call
# from _ready and from inside a kill handler mid-signal.
func _start_wave(number: int) -> void:
_wave = number
wave_changed.emit(_wave)
_spawn.call_deferred() _spawn.call_deferred()
func _spawn() -> void: func _spawn() -> void:
var positions := _spread_positions(_spawn_count) # Bull and matadors start at opposite ends of a random diameter, so they always
for pos: Vector3 in positions: # 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) _spawn_at(pos)
func _spread_positions(count: int) -> Array[Vector3]: 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] = [] var positions: Array[Vector3] = []
var arena_r := DP.f("mat_wander_radius") * 0.72 if count <= 1:
var min_r := 5.0 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: for i: int in count:
var sector := TAU / count var t := float(i) / float(count - 1) - 0.5 # -0.5 .. 0.5 across the arc
var angle := sector * i + randf_range(-sector * 0.35, sector * 0.35) var angle := center_angle + t * arc
var radius := randf_range(min_r, arena_r) var radius := randf_range(arena_r * 0.7, arena_r)
positions.append(Vector3(cos(angle) * radius, 1.0, sin(angle) * radius)) positions.append(Vector3(cos(angle) * radius, 1.0, sin(angle) * radius))
return positions return positions
@@ -68,18 +60,10 @@ func _spawn_at(pos: Vector3) -> void:
_alive += 1 _alive += 1
# Every matador down = the bull wins. No waves, no respawns — one clean arena.
func _on_matador_killed() -> void: func _on_matador_killed() -> void:
matador_killed.emit() matador_killed.emit()
_alive -= 1 _alive -= 1
if _alive <= 0: if _alive <= 0 and not _resolved:
_spawn_count *= 2 _resolved = true
_start_wave(_wave + 1) all_defeated.emit()
func reset_spawn() -> void:
for m: Node in get_tree().get_nodes_in_group(&"matador"):
m.queue_free()
await get_tree().process_frame
_spawn_count = maxi(1, int(DP.f("mat_spawn_count")))
_alive = 0
_start_wave(1)
+132 -267
View File
@@ -22,12 +22,8 @@ const _ANIM_ROLL: StringName = &"Armature|ROLL"
var _hoof_emitters: Array[CPUParticles3D] = [] var _hoof_emitters: Array[CPUParticles3D] = []
var _legs: Node var _legs: Node
var _kick_timer: float = 0.0
var _was_on_floor: bool = false var _was_on_floor: bool = false
var _pre_slide_vel_y: float = 0.0 var _pre_slide_vel_y: float = 0.0
var _thrust_left_charge: float = 0.0
var _thrust_right_charge: float = 0.0
var _charge_was_full: bool = false
var _slam_ring_mesh: ArrayMesh = null var _slam_ring_mesh: ArrayMesh = null
@@ -38,30 +34,24 @@ 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 _roll_dir: Vector3 = Vector3.ZERO var _roll_dir: Vector3 = Vector3.ZERO
var _roll_ramp: float = 0.0
var _roll_spin: float = 0.0
var _bull_anim: AnimationPlayer = null var _bull_anim: AnimationPlayer = null
var _charge_ready_emitter: CPUParticles3D = null
var _slam_pending: bool = false var _slam_pending: bool = false
var _charge_trail_emitter: CPUParticles3D = null var _charge_trail_emitter: CPUParticles3D = null
var _roll_spark_emitter: CPUParticles3D = null
var _cube_scale: Vector3 = Vector3.ONE
# Floating 3D charge bar (mouse-button thrust charge indicator) # One clean hit ends the run — see take_sword_hit(). The HUD listens for `died`
const _CHARGE_BAR_WIDTH: float = 1.1 # to raise the lose screen (the `cause` tags how it happened, for the death notice);
const _CHARGE_BAR_HEIGHT: float = 1.45 # _dead latches so a flurry of hits fires it only once.
var _charge_bar: Node3D = null signal died(cause: String)
var _charge_bar_fill: MeshInstance3D = null var _dead: bool = false
var _charge_bar_fill_mat: StandardMaterial3D = null
var _charge_bar_ramp: Gradient = null
var _charge_bar_time: float = 0.0
var _charge_bar_shown: float = 0.0
const MAX_HEALTH: int = 10
var health: int = MAX_HEALTH
signal health_changed(new_health: int)
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
var _roll_damping: float = 0.15
enum DustState { NONE, WALK, CHARGE } enum DustState { NONE, WALK, CHARGE }
var _dust_state: DustState = DustState.NONE var _dust_state: DustState = DustState.NONE
@@ -71,15 +61,15 @@ var _charge_dust_ramp: Gradient = null
func _ready() -> void: func _ready() -> void:
add_to_group(&"player") add_to_group(&"player")
_cube_scale = cube_guy.scale
_setup_hoof_dust() _setup_hoof_dust()
_setup_legs() _setup_legs()
_setup_charge_indicators() _setup_charge_trail()
_setup_charge_bar() _setup_roll_sparks()
_setup_audio() _setup_audio()
_bull_anim = _find_anim_player(cube_guy) _bull_anim = _find_anim_player(cube_guy)
_setup_bull_idle() _setup_bull_idle()
DP.any_changed.connect(_on_dp_changed) DP.any_changed.connect(_on_dp_changed)
_roll_damping = DP.f("roll_damping")
func _setup_legs() -> void: func _setup_legs() -> void:
@@ -129,39 +119,8 @@ func _setup_hoof_dust() -> void:
_hoof_emitters.append(p) _hoof_emitters.append(p)
func _setup_charge_indicators() -> void: func _setup_charge_trail() -> void:
var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material()) # Blue energy trail — streams backward during a fast run / roll
# Orange spark burst when single-button charge hits max
var burst_ramp := Gradient.new()
burst_ramp.set_color(0, Color(1.0, 0.55, 0.0, 1.0))
burst_ramp.set_color(1, Color(1.0, 0.15, 0.0, 0.0))
var burst_scale := Curve.new()
burst_scale.add_point(Vector2(0.0, 1.0))
burst_scale.add_point(Vector2(1.0, 0.0))
_charge_ready_emitter = CPUParticles3D.new()
_charge_ready_emitter.position = Vector3(0.0, 0.3, 0.0)
_charge_ready_emitter.emitting = false
_charge_ready_emitter.one_shot = true
_charge_ready_emitter.amount = 16
_charge_ready_emitter.lifetime = 0.5
_charge_ready_emitter.explosiveness = 1.0
_charge_ready_emitter.randomness = 0.4
_charge_ready_emitter.local_coords = false
_charge_ready_emitter.direction = Vector3.UP
_charge_ready_emitter.spread = 120.0
_charge_ready_emitter.initial_velocity_min = 2.5
_charge_ready_emitter.initial_velocity_max = 5.0
_charge_ready_emitter.scale_amount_min = 0.2
_charge_ready_emitter.scale_amount_max = 0.5
_charge_ready_emitter.mesh = sphere
_charge_ready_emitter.color_ramp = burst_ramp
_charge_ready_emitter.scale_amount_curve = burst_scale
cube_guy.add_child(_charge_ready_emitter)
# Blue energy trail — streams backward during charge
var trail_sphere := _particle_sphere(0.065, 4, 2, _unshaded_material()) var trail_sphere := _particle_sphere(0.065, 4, 2, _unshaded_material())
var trail_ramp := Gradient.new() var trail_ramp := Gradient.new()
@@ -194,93 +153,39 @@ func _setup_charge_indicators() -> void:
cube_guy.add_child(_charge_trail_emitter) cube_guy.add_child(_charge_trail_emitter)
func _setup_charge_bar() -> void: func _setup_roll_sparks() -> void:
_charge_bar = Node3D.new() # Rammus Powerball ground spray — dirt/sparks flung outward from the spinning
_charge_bar.visible = false # ball. World-space (local_coords off) so they peel off into a trail behind it.
add_child(_charge_bar) var spark := _particle_sphere(0.05, 4, 2, _unshaded_material())
var w := _CHARGE_BAR_WIDTH var ramp := Gradient.new()
var thickness := 0.16 ramp.set_color(0, Color(1.00, 0.78, 0.32, 1.0))
ramp.set_color(1, Color(0.70, 0.42, 0.14, 0.0))
# Dark translucent track behind the fill, slightly larger for a border look. var sc := Curve.new()
var track_mat := StandardMaterial3D.new() sc.add_point(Vector2(0.0, 0.30))
track_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED sc.add_point(Vector2(0.3, 1.00))
track_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA sc.add_point(Vector2(1.0, 0.00))
track_mat.albedo_color = Color(0.02, 0.02, 0.04, 0.55)
var track_box := BoxMesh.new()
track_box.size = Vector3(w + 0.07, thickness + 0.07, thickness + 0.07)
track_box.material = track_mat
var track := MeshInstance3D.new()
track.mesh = track_box
track.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_charge_bar.add_child(track)
# Glowing fill bar; scaled along X each frame to show progress. _roll_spark_emitter = CPUParticles3D.new()
_charge_bar_fill_mat = StandardMaterial3D.new() _roll_spark_emitter.position = Vector3(0.0, 0.10, 0.0)
_charge_bar_fill_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED _roll_spark_emitter.emitting = false
_charge_bar_fill_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA _roll_spark_emitter.one_shot = false
_charge_bar_fill_mat.emission_enabled = true _roll_spark_emitter.amount = 46
var fill_box := BoxMesh.new() _roll_spark_emitter.lifetime = 0.45
fill_box.size = Vector3(w, thickness, thickness) _roll_spark_emitter.randomness = 0.7
fill_box.material = _charge_bar_fill_mat _roll_spark_emitter.local_coords = false
_charge_bar_fill = MeshInstance3D.new() _roll_spark_emitter.direction = Vector3(0.0, 1.0, 0.0)
_charge_bar_fill.mesh = fill_box _roll_spark_emitter.spread = 180.0
_charge_bar_fill.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _roll_spark_emitter.gravity = Vector3(0.0, -6.0, 0.0)
_charge_bar.add_child(_charge_bar_fill) _roll_spark_emitter.initial_velocity_min = 3.0
_roll_spark_emitter.initial_velocity_max = 8.0
# Cool → hot colour ramp as the charge fills (matches the spark FX palette). _roll_spark_emitter.scale_amount_min = 0.5
_charge_bar_ramp = Gradient.new() _roll_spark_emitter.scale_amount_max = 1.3
_charge_bar_ramp.offsets = PackedFloat32Array([0.0, 0.6, 1.0]) _roll_spark_emitter.mesh = spark
_charge_bar_ramp.colors = PackedColorArray([ _roll_spark_emitter.color_ramp = ramp
Color(0.25, 0.65, 1.0), _roll_spark_emitter.scale_amount_curve = sc
Color(1.0, 0.85, 0.2), cube_guy.add_child(_roll_spark_emitter)
Color(1.0, 0.28, 0.05),
])
func _process(delta: float) -> void:
_update_charge_bar(delta)
func _update_charge_bar(delta: float) -> void:
if _charge_bar == null:
return
_charge_bar_time += delta
var max_charge := DP.f("thrust_charge_time")
var charge := maxf(_thrust_left_charge, _thrust_right_charge)
var frac := clampf(charge / max_charge, 0.0, 1.0) if max_charge > 0.0 else 0.0
# Ease the whole bar in/out so it pops up when charging and fades when done.
var target := 1.0 if frac > 0.001 else 0.0
_charge_bar_shown = move_toward(_charge_bar_shown, target, delta * 6.0)
if _charge_bar_shown <= 0.001:
_charge_bar.visible = false
return
_charge_bar.visible = true
# Billboard toward the camera on the Y axis, staying upright.
var to_cam := camera_pivot.global_position - _charge_bar.global_position
_charge_bar.rotation.y = atan2(to_cam.x, to_cam.z)
# Grow the fill from the left edge.
var w := _CHARGE_BAR_WIDTH
_charge_bar_fill.scale.x = maxf(frac, 0.0001)
_charge_bar_fill.position.x = -0.5 * w + 0.5 * w * frac
# Colour + glow, with a fast pulse once fully charged.
var col := _charge_bar_ramp.sample(frac)
var pulse := 1.0 + (0.4 * sin(_charge_bar_time * 16.0) if frac >= 1.0 else 0.0)
_charge_bar_fill_mat.albedo_color = Color(col.r, col.g, col.b, 0.95)
_charge_bar_fill_mat.emission = col
_charge_bar_fill_mat.emission_energy_multiplier = 1.7 * pulse
# Float above the bull with a gentle bob; a small bounce + rise on the pop-in.
var bob := 0.03 * sin(_charge_bar_time * 4.0)
_charge_bar.position.y = _CHARGE_BAR_HEIGHT + bob - (1.0 - _charge_bar_shown) * 0.3
var appear := _charge_bar_shown * (1.06 - 0.06 * cos(_charge_bar_shown * PI))
var full_pop := 1.0 + (0.08 * sin(_charge_bar_time * 16.0) if frac >= 1.0 else 0.0)
_charge_bar.scale = Vector3.ONE * appear * full_pop
func _setup_audio() -> void: func _setup_audio() -> void:
@@ -331,7 +236,6 @@ func _set_dust_state(new_state: DustState) -> void:
func _on_dp_changed(_key: String, _val: Variant) -> void: func _on_dp_changed(_key: String, _val: Variant) -> void:
_roll_damping = DP.f("roll_damping")
if _hoof_emitters.is_empty() or _dust_state == DustState.NONE: if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
return return
var prev := _dust_state var prev := _dust_state
@@ -460,15 +364,17 @@ func _activate_roll() -> void:
_ability_active = true _ability_active = true
_active_ability = 3 _active_ability = 3
_ability_timer = DP.f("roll_duration") _ability_timer = DP.f("roll_duration")
# Combine with any charge already underway: keep the current heading + speed, _roll_ramp = 0.0
# floored to a launch minimum so even a standing roll shoves off like a boulder. _roll_spin = 0.0
# Curl into the ball along the current heading (or facing when standing still)
# and launch at the base speed — it ramps up from here the longer you roll.
var flat := Vector3(velocity.x, 0.0, velocity.z) var flat := Vector3(velocity.x, 0.0, velocity.z)
if flat.length() > 1.0: if 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
_roll_dir = Vector3(f.x, 0.0, f.z).normalized() _roll_dir = Vector3(f.x, 0.0, f.z).normalized()
var launch := maxf(flat.length(), DP.f("roll_min_speed")) var launch := DP.f("roll_base_speed")
velocity.x = _roll_dir.x * launch velocity.x = _roll_dir.x * launch
velocity.z = _roll_dir.z * launch velocity.z = _roll_dir.z * launch
if _bull_anim and _bull_anim.has_animation(_ANIM_ROLL): if _bull_anim and _bull_anim.has_animation(_ANIM_ROLL):
@@ -476,6 +382,9 @@ func _activate_roll() -> void:
_bull_anim.play(_ANIM_ROLL) _bull_anim.play(_ANIM_ROLL)
_legs.set(&"tail_ragdoll", true) # tail streams out behind _legs.set(&"tail_ragdoll", true) # tail streams out behind
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch")) # horns down, plowing _legs.set(&"charge_pitch_target", DP.f("charge_head_pitch")) # horns down, plowing
_legs.set(&"external_rotation", true) # roll owns the model's full orientation (tumble)
if _roll_spark_emitter:
_roll_spark_emitter.emitting = true
_spawn_dash_burst() _spawn_dash_burst()
camera_pivot.call(&"trigger_hit", 0.35) camera_pivot.call(&"trigger_hit", 0.35)
if _huff_player: if _huff_player:
@@ -483,27 +392,32 @@ func _activate_roll() -> void:
_huff_player.play() _huff_player.play()
# One rolling frame: steer, ramp/retain speed, move, then ricochet off walls and # One rolling frame (Rammus Powerball): steer with momentum, accelerate the longer
# bowl through matadors. Fully replaces the normal locomotion path while active. # the roll lasts, move (walls just slide), then pop the ball on the first matador we
# ram. Fully replaces the normal locomotion path while active.
func _tick_roll(delta: float, steer: Vector3) -> void: func _tick_roll(delta: float, steer: Vector3) -> void:
var steer_flat := Vector3(steer.x, 0.0, steer.z) var steer_flat := Vector3(steer.x, 0.0, steer.z)
if steer_flat.length() > 0.1: if steer_flat.length() > 0.1:
var t := clampf(DP.f("roll_turn") * delta, 0.0, 1.0) var t := clampf(DP.f("roll_turn") * delta, 0.0, 1.0)
_roll_dir = _roll_dir.slerp(steer_flat.normalized(), t).normalized() _roll_dir = _roll_dir.slerp(steer_flat.normalized(), t).normalized()
# Ramp toward the cruise top speed; anything above it (from wall boosts) bleeds # Speed builds over time: base → max across roll_rampup_time, so a long roll
# off slowly via roll_momentum instead of snapping back — that's the fun carry. # winds up into a runaway boulder while a quick tap barely gets going.
var speed := Vector2(velocity.x, velocity.z).length() _roll_ramp += delta
var ramp := clampf(_roll_ramp / maxf(DP.f("roll_rampup_time"), 0.01), 0.0, 1.0)
var top := DP.f("roll_max_speed") var top := DP.f("roll_max_speed")
if speed < top: var speed := lerpf(DP.f("roll_base_speed"), top, ramp)
speed = move_toward(speed, top, DP.f("roll_accel") * delta)
else:
speed = maxf(top, speed * pow(DP.f("roll_momentum"), delta))
speed = minf(speed, DP.f("roll_wall_cap"))
velocity.x = _roll_dir.x * speed velocity.x = _roll_dir.x * speed
velocity.z = _roll_dir.z * speed velocity.z = _roll_dir.z * speed
cube_guy.rotation.y = lerp_angle(cube_guy.rotation.y, atan2(_roll_dir.x, _roll_dir.z), delta * 12.0) # Tumble the whole model like a Powerball: face the travel direction (yaw) then
# spin forward about the horizontal axis perpendicular to it, angular speed = v/r
# so it "rolls" without slipping. Legs yield rotation control while active.
var radius := maxf(DP.f("roll_spin_radius"), 0.1)
_roll_spin = wrapf(_roll_spin + (speed / radius) * delta, 0.0, TAU)
var facing := Basis(Vector3.UP, atan2(_roll_dir.x, _roll_dir.z))
var tumble_axis := Vector3.UP.cross(_roll_dir).normalized()
cube_guy.basis = (Basis(tumble_axis, _roll_spin) * facing).scaled(_cube_scale)
if _bull_anim and _bull_anim.has_animation(_ANIM_ROLL): if _bull_anim and _bull_anim.has_animation(_ANIM_ROLL):
_bull_anim.speed_scale = clampf(speed / maxf(top, 1.0), 0.6, 2.4) _bull_anim.speed_scale = clampf(speed / maxf(top, 1.0), 0.6, 2.4)
@@ -511,40 +425,46 @@ func _tick_roll(delta: float, steer: Vector3) -> void:
if _charge_trail_emitter: if _charge_trail_emitter:
_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()
var pre_wall := Vector3(velocity.x, 0.0, velocity.z)
move_and_slide() move_and_slide()
_was_on_floor = is_on_floor() _was_on_floor = is_on_floor()
_roll_wall_ricochet(pre_wall) _roll_try_pop(speed)
# Bowling strike: fling matadors we plow into up and outward like pins (and the
# resulting kills feed the HUD combo meter).
_hit_matadors_radius(DP.f("roll_hit_radius"), DP.f("roll_hit_strength"), DP.f("roll_hit_up"))
# On hitting a wall mid-roll, mirror the heading and BOOST speed (capped) instead of # Pop the ball on ramming a matador: launch every matador in the hit radius up and
# bleeding it — banking off walls is how you build ludicrous momentum. # outward, then end the roll (Rammus stops when Powerball connects). Returns true if
func _roll_wall_ricochet(pre_wall: Vector3) -> void: # it popped so the caller can stop touching roll state this frame.
for i in get_slide_collision_count(): func _roll_try_pop(speed: float) -> bool:
var col := get_slide_collision(i) var radius := DP.f("roll_hit_radius")
var normal := col.get_normal() for mat: Node in get_tree().get_nodes_in_group(&"matador"):
if absf(normal.y) >= 0.5: var to_mat: Vector3 = (mat as Node3D).global_position - global_position
to_mat.y = 0.0
if to_mat.length() > radius:
continue continue
var flat_n := Vector3(normal.x, 0.0, normal.z).normalized() _hit_matadors_radius(radius, DP.f("roll_hit_strength"), DP.f("roll_knockup"))
var refl := pre_wall - 2.0 * pre_wall.dot(flat_n) * flat_n camera_pivot.call(&"trigger_hit", clampf(speed / 90.0, 0.35, 1.0))
refl.y = 0.0
if refl.length() < 0.01:
return
_roll_dir = refl.normalized()
var boosted := minf(pre_wall.length() * DP.f("roll_wall_boost"), DP.f("roll_wall_cap"))
velocity.x = _roll_dir.x * boosted
velocity.z = _roll_dir.z * boosted
var hit := col.get_position()
_spawn_kick_sparks(hit, flat_n)
_spawn_kick_flash(hit)
camera_pivot.call(&"trigger_hit", clampf(boosted / 90.0, 0.25, 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()
return _end_roll()
return true
return false
# End the Powerball early: brake hard (the ball "pops" and unrolls) and drop back to
# the normal locomotion path next frame.
func _end_roll() -> void:
_ability_active = false
_active_ability = -1
_ability_timer = 0.0
_roll_ramp = 0.0
_legs.set(&"charge_pitch_target", 0.0)
_legs.set(&"tail_ragdoll", false)
_legs.set(&"external_rotation", false)
if _roll_spark_emitter:
_roll_spark_emitter.emitting = false
velocity.x *= 0.15
velocity.z *= 0.15
_play_idle()
func _hit_matadors_cone(range_m: float, half_angle_rad: float, strength: float, func _hit_matadors_cone(range_m: float, half_angle_rad: float, strength: float,
@@ -884,36 +804,6 @@ func _make_slam_ring_mesh() -> ArrayMesh:
return mesh return mesh
func _apply_thrust(dir: Vector3) -> void:
var impulse := DP.f("thrust_impulse")
velocity.x += dir.x * impulse
velocity.z += dir.z * impulse
if is_on_floor():
velocity.y += DP.f("thrust_vertical")
var flat := Vector2(velocity.x, velocity.z)
var spd := flat.length()
if spd > DP.f("max_speed"):
var s := DP.f("max_speed") / spd
velocity.x *= s
velocity.z *= s
func _apply_kick(direction: Vector3) -> void:
var force := DP.f("kick_impulse") * randf_range(1.0 - DP.f("kick_force_var"), 1.0 + DP.f("kick_force_var"))
var spread := deg_to_rad(randf_range(-DP.f("kick_spread"), DP.f("kick_spread")))
var kick_dir := direction.rotated(Vector3.UP, spread)
velocity.x += kick_dir.x * force
velocity.z += kick_dir.z * force
if is_on_floor():
velocity.y += DP.f("kick_vertical") * randf_range(0.75, 1.25)
var flat := Vector2(velocity.x, velocity.z)
var spd := flat.length()
if spd > DP.f("max_speed"):
var s := DP.f("max_speed") / spd
velocity.x *= s
velocity.z *= s
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
# ── Gravity ─────────────────────────────────────────────────────────────── # ── Gravity ───────────────────────────────────────────────────────────────
if not is_on_floor(): if not is_on_floor():
@@ -939,12 +829,15 @@ func _physics_process(delta: float) -> void:
if _ability_active: if _ability_active:
_ability_timer -= delta _ability_timer -= delta
if _ability_timer <= 0.0: if _ability_timer <= 0.0:
_ability_active = false if _active_ability == 3:
_active_ability = -1 _end_roll() # tears down roll-specific state (sparks, tumble control)
_legs.set(&"charge_pitch_target", 0.0) else:
_legs.set(&"tail_ragdoll", false) _ability_active = false
_slam_pending = false _active_ability = -1
_play_idle() _legs.set(&"charge_pitch_target", 0.0)
_legs.set(&"tail_ragdoll", false)
_slam_pending = false
_play_idle()
# ── Boulder roll ────────────────────────────────────────────────────────── # ── Boulder roll ──────────────────────────────────────────────────────────
# While rolling, the ability owns movement entirely (its own accel, low friction # While rolling, the ability owns movement entirely (its own accel, low friction
@@ -953,56 +846,23 @@ func _physics_process(delta: float) -> void:
_tick_roll(delta, direction) _tick_roll(delta, direction)
return return
# ── Mouse button single-thrust actions ───────────────────────────────────────
# Left only → thrust NE (forward-right diagonal)
# Right only → thrust NW (forward-left diagonal)
var left_held := Input.is_action_pressed(&"thrust_left")
var right_held := Input.is_action_pressed(&"thrust_right")
if left_held and right_held:
_thrust_left_charge = 0.0
_thrust_right_charge = 0.0
else:
if left_held:
_thrust_left_charge = minf(_thrust_left_charge + delta, DP.f("thrust_charge_time"))
elif Input.is_action_just_released(&"thrust_left") and _thrust_left_charge > 0.0:
var t := _thrust_left_charge / DP.f("thrust_charge_time")
var angle := lerpf(deg_to_rad(DP.f("thrust_min_angle")), deg_to_rad(DP.f("thrust_max_angle")), t)
var raw := cube_guy.global_transform.basis.z
_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, -angle))
_thrust_left_charge = 0.0
if right_held:
_thrust_right_charge = minf(_thrust_right_charge + delta, DP.f("thrust_charge_time"))
elif Input.is_action_just_released(&"thrust_right") and _thrust_right_charge > 0.0:
var t := _thrust_right_charge / DP.f("thrust_charge_time")
var angle := lerpf(deg_to_rad(DP.f("thrust_min_angle")), deg_to_rad(DP.f("thrust_max_angle")), t)
var raw := cube_guy.global_transform.basis.z
_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, angle))
_thrust_right_charge = 0.0
# Fire spark burst the frame either single-button charge first hits max
var max_charge := DP.f("thrust_charge_time")
var either_full := _thrust_left_charge >= max_charge or _thrust_right_charge >= max_charge
if either_full and not _charge_was_full:
_charge_ready_emitter.restart()
_charge_was_full = either_full
# Reset kick timer on landing so the first kick after a bounce is immediate
if on_floor and not _was_on_floor and direction:
_kick_timer = 0.0
_was_on_floor = on_floor _was_on_floor = on_floor
# ── Periodic kick ───────────────────────────────────────────────────────── # ── Ground locomotion — smooth accelerate / friction ──────────────────────
_kick_timer = maxf(0.0, _kick_timer - delta) # WASD/arrows drive the bull directly: hold a direction to ramp up to max_speed
if direction and _kick_timer <= 0.0: # and steer, release to coast to a stop. Ability bursts (dash) and wall bounces
_apply_kick(direction) # can exceed max_speed — while a direction is held we steer that momentum without
_kick_timer = DP.f("kick_interval") # braking it (cap tracks the current speed); friction only bleeds the excess once
# input is released.
# ── Rolling friction (exponential, frame-rate independent) ──────────────── var flat_vel := Vector3(velocity.x, 0.0, velocity.z)
var retain := pow(_roll_damping, delta) var max_speed := DP.f("max_speed")
velocity.x *= retain if direction.length_squared() > 0.01:
velocity.z *= retain var cap := maxf(max_speed, flat_vel.length())
flat_vel = flat_vel.move_toward(direction * cap, DP.f("move_accel") * delta)
else:
flat_vel = flat_vel.move_toward(Vector3.ZERO, DP.f("move_friction") * delta)
velocity.x = flat_vel.x
velocity.z = flat_vel.z
# ── Visual rotation follows velocity direction ───────────────────────────── # ── Visual rotation follows velocity direction ─────────────────────────────
var flat_speed := Vector2(velocity.x, velocity.z).length() var flat_speed := Vector2(velocity.x, velocity.z).length()
@@ -1065,6 +925,11 @@ func _physics_process(delta: float) -> void:
# ── Hoof sounds ─────────────────────────────────────────────────────────── # ── Hoof sounds ───────────────────────────────────────────────────────────
func take_sword_hit() -> void: # A single clean hit — thrown or swung blade — ends the run. `cause` ("gored" /
health = maxi(0, health - 1) # "thrown") tags how it happened for the death notice. Latches so the lose screen is
health_changed.emit(health) # raised exactly once even if several blades land on the same frame.
func take_sword_hit(cause: String = "") -> void:
if _dead:
return
_dead = true
died.emit(cause)
+1 -12
View File
@@ -11,6 +11,7 @@ config_version=5
[application] [application]
config/name="Bullosseum" config/name="Bullosseum"
config/version="0.1.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", "Forward Plus")
config/icon="res://icon.svg" config/icon="res://icon.svg"
@@ -76,18 +77,6 @@ toggle_mouse_capture={
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":4,"position":Vector2(461, 22),"global_position":Vector2(470, 70),"factor":1.0,"button_index":3,"canceled":false,"pressed":true,"double_click":false,"script":null) "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":4,"position":Vector2(461, 22),"global_position":Vector2(470, 70),"factor":1.0,"button_index":3,"canceled":false,"pressed":true,"double_click":false,"script":null)
] ]
} }
thrust_left={
"deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":4,"axis_value":1.0,"script":null)
]
}
thrust_right={
"deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null)
]
}
ability_kick={ ability_kick={
"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":70,"key_label":0,"unicode":102,"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":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
+258
View File
@@ -0,0 +1,258 @@
extends SceneTree
## Headless difficulty simulator.
##
## Drives a kinematic "bot bull" against ONE real matador (the actual Matador.tscn
## + matador.gd AI) across a few behaviour profiles, many trials each, and reports
## how often the matador wins — i.e. gores the bull — versus how often the bull
## wins by ramming the matador. It exists so matador difficulty can be tuned from
## measurements instead of guesswork.
##
## godot --headless --script tests/difficulty_sim.gd
##
## Read the CHARGER row: that models a player who just charges in. If the matador
## almost never wins there, blind charging is unpunished and the game is too easy.
const CHARGER_TRIALS := 16
const KITER_TRIALS := 16
const PASSIVE_TRIALS := 8
const TRIAL_TIMEOUT := 5.0
const SPAWN_SETTLE := 0.4
const CHARGE_SPEED := 42.0 # a fast, committed horn-charge
const KITE_DIST := 12.0
const KITE_SPEED := 17.0 # a real bull easily out-paces the matador; only throws threaten
const START_DIST := 12.0
var _scene: Node
var _dp: Node
var _mat_scene: PackedScene
var _player: CharacterBody3D
var _matador: Node3D
var _bull_dead := false
var _mat_dead := false
var _dt := 1.0 / 60.0
# Charger bull state
var _charge_dir: Vector3 = Vector3.ZERO
var _charge_timer: float = 0.0
var _recover_timer: float = 0.0
# Kiter juke state
var _juke_dir: float = 1.0
var _juke_timer: float = 0.0
var _juke_range: float = KITE_DIST
func _init() -> void:
_run.call_deferred()
func _run() -> void:
_dt = 1.0 / float(Engine.physics_ticks_per_second)
_dp = root.get_node_or_null("/root/DP")
_mat_scene = load("res://Matador.tscn") as PackedScene
_scene = (load("res://scene.tscn") as PackedScene).instantiate()
root.add_child(_scene)
current_scene = _scene
await create_timer(0.5).timeout
var players := get_nodes_in_group(&"player")
if players.is_empty():
push_error("no player in scene"); quit(1); return
_player = players[0]
_player.set_physics_process(false) # we drive the bull kinematically
_player.died.connect(func(_cause: String) -> void: _bull_dead = true)
_strip_match_machinery() # no HUD pause / spawner interference
print("=".repeat(64))
print("DIFFICULTY SIMULATION (physics %d Hz)" % Engine.physics_ticks_per_second)
print("=".repeat(64))
var charger := await _run_profile("CHARGER (fast straight charges)", CHARGER_TRIALS, &"charger")
var kiter := await _run_profile("KITER (circles at ~11 m)", KITER_TRIALS, &"kiter")
var passive := await _run_profile("PASSIVE (holds at attack range)", PASSIVE_TRIALS, &"passive")
print("\n" + "=".repeat(64))
print("SUMMARY — matador win %% (bull gored)")
print("=".repeat(64))
_print_row("CHARGER", charger)
_print_row("KITER ", kiter)
_print_row("PASSIVE", passive)
print("=".repeat(64))
quit(0)
var _state_hist: Dictionary = {}
var _throws: int = 0
var _diag_printed: bool = false
func _run_profile(label: String, trials: int, mode: StringName) -> Dictionary:
var wins := 0
var losses := 0
var timeouts := 0
var total_t := 0.0
_state_hist = {}
_throws = 0
for _i in trials:
var res := await _run_trial(mode)
total_t += res[1] as float
match res[0] as StringName:
&"mat": wins += 1
&"bull": losses += 1
_: timeouts += 1
print("\n-- %s --" % label)
print(" matador wins: %d/%d bull wins: %d timeouts: %d avg %.2fs" % [
wins, trials, losses, timeouts, total_t / maxf(float(trials), 1.0)])
print(" swords thrown: %d states: %s" % [_throws, _fmt_hist()])
return {"win": wins, "loss": losses, "timeout": timeouts, "n": trials}
func _fmt_hist() -> String:
var parts: Array[String] = []
var total := 0
for k: String in _state_hist:
total += _state_hist[k] as int
for k: String in _state_hist:
parts.append("%s %.0f%%" % [k, 100.0 * float(_state_hist[k]) / maxf(float(total), 1.0)])
return ", ".join(parts)
func _count_thrown_swords() -> int:
var n := 0
for c: Node in _scene.get_children():
if c is RigidBody3D:
n += 1
return n
func _run_trial(mode: StringName) -> Array:
if is_instance_valid(_matador):
_matador.queue_free()
await physics_frame
_matador = _mat_scene.instantiate()
_scene.add_child(_matador)
_matador.global_position = Vector3(0.0, 1.0, 0.0)
_matador.killed.connect(func() -> void: _mat_dead = true)
var ang := randf() * TAU
_player.global_position = Vector3(cos(ang), 0.0, sin(ang)) * START_DIST + Vector3(0.0, 1.0, 0.0)
_player.velocity = Vector3.ZERO
_player._dead = false
_bull_dead = false
_mat_dead = false
_charge_dir = Vector3.ZERO
_charge_timer = 0.0
_recover_timer = 0.0
_juke_dir = 1.0 if randf() < 0.5 else -1.0
_juke_timer = 0.0
_juke_range = KITE_DIST
# Let the matador's _ready (ragdoll rig, sword) settle before the duel counts.
var settle := SPAWN_SETTLE
while settle > 0.0:
await physics_frame
settle -= _dt
_bull_dead = false
_mat_dead = false
_player._dead = false
if not _diag_printed:
_diag_printed = true
print(" [diag] matador has_sword=%s in_hand=%s throw_range=[%.0f,%.0f]" % [
is_instance_valid(_matador._sword_node), _matador._sword_in_hand,
_dp.f("mat_throw_min_dist"), _dp.f("mat_throw_range")])
var t := 0.0
var max_swords := 0
while t < TRIAL_TIMEOUT:
_drive(mode)
await physics_frame
t += _dt
var s: String = _matador.ai_state_name()
_state_hist[s] = int(_state_hist.get(s, 0)) + 1
max_swords = maxi(max_swords, _count_thrown_swords())
if _bull_dead: # the bull was gored → matador win
_throws += max_swords
return [&"mat", t]
if _mat_dead: # the matador was rammed → bull win
_throws += max_swords
return [&"bull", t]
_throws += max_swords
return [&"timeout", t]
func _drive(mode: StringName) -> void:
var to_mat := _matador.global_position - _player.global_position
to_mat.y = 0.0
var dist := to_mat.length()
var aim := to_mat.normalized() if dist > 0.05 else Vector3.FORWARD
var vel := Vector3.ZERO
match mode:
&"charger":
if _recover_timer > 0.0:
_recover_timer -= _dt
vel = -aim * 14.0
else:
if _charge_timer <= 0.0:
_charge_dir = aim # commit a straight line (dodgeable)
_charge_timer = 0.6
_charge_timer -= _dt
vel = _charge_dir * CHARGE_SPEED
if dist < 0.8 or _charge_timer <= 0.0:
_recover_timer = 0.35
_charge_timer = 0.0
&"kiter":
# Juke: flip strafe direction and shift the hold distance at random so the
# matador can't perfectly lead the throw — models an evasive player.
_juke_timer -= _dt
if _juke_timer <= 0.0:
_juke_timer = randf_range(0.4, 0.9)
_juke_dir = -_juke_dir if randf() < 0.6 else _juke_dir
_juke_range = randf_range(8.0, 13.0)
var inward := aim * clampf(dist - _juke_range, -1.0, 1.0)
var tangent := Vector3(-aim.z, 0.0, aim.x) * _juke_dir
vel = (tangent + inward).normalized() * KITE_SPEED
&"passive":
# Sit just inside the matador's attack range so it commits to a strike.
if dist > 6.5:
vel = aim * 6.0
elif dist < 5.0:
vel = -aim * 4.0
_player.velocity = Vector3(vel.x, 0.0, vel.z)
_player.move_and_slide()
var p := _player.global_position
p.y = 1.0
_player.global_position = p
func _strip_match_machinery() -> void:
for m: Node in get_nodes_in_group(&"matador"):
m.queue_free()
for s: Node in get_nodes_in_group(&"matador_spawn"):
s.queue_free()
var hud := _find_hud(_scene)
if hud != null:
hud.queue_free()
func _find_hud(n: Node) -> Node:
if n is CanvasLayer and n.has_method(&"_show_game_over"):
return n
for c: Node in n.get_children():
var r := _find_hud(c)
if r != null:
return r
return null
func _print_row(label: String, r: Dictionary) -> void:
var n: int = r["n"]
var win: int = r["win"]
var pct := 100.0 * float(win) / maxf(float(n), 1.0)
print(" %s %5.1f%% (%d win / %d loss / %d timeout)" % [
label, pct, win, r["loss"], r["timeout"]])

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