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):
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
# 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
| Check | What it detects |
|---|---|
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://ypxkr3vla3fr"
path="res://.godot/imported/HUD_0000_ability_JALG.png-c430df7817ae1f22045a2a27b06f60a8.ctex"
uid="uid://cyscxku0nw055"
path="res://.godot/imported/DASH.png-02cbc39ec03a0c79526d7dc333659c48.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://HUD/HUD_0000_ability_JALG.png"
dest_files=["res://.godot/imported/HUD_0000_ability_JALG.png-c430df7817ae1f22045a2a27b06f60a8.ctex"]
source_file="res://HUD/DASH.png"
dest_files=["res://.godot/imported/DASH.png-02cbc39ec03a0c79526d7dc333659c48.ctex"]
[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"
type="CompressedTexture2D"
uid="uid://w4yovxyg0104"
path="res://.godot/imported/HUD_0001_ability_SLAM.png-7c5f88306e1af474db2c2073888feab7.ctex"
uid="uid://bdnc3jw1ykydi"
path="res://.godot/imported/KICK.png-5482be80e8176ed7a0ae69a48ca1f5ed.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://HUD/HUD_0001_ability_SLAM.png"
dest_files=["res://.godot/imported/HUD_0001_ability_SLAM.png-7c5f88306e1af474db2c2073888feab7.ctex"]
source_file="res://HUD/KICK.png"
dest_files=["res://.godot/imported/KICK.png-5482be80e8176ed7a0ae69a48ca1f5ed.ctex"]
[params]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

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

After

Width:  |  Height:  |  Size: 80 KiB

@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://nw2pvct1kqcd"
path="res://.godot/imported/intro_seq_0015.png-1a3c8239c9c1c61cd711f23fcc29af50.ctex"
uid="uid://bxplqrp4mc5yh"
path="res://.godot/imported/SLAM.png-b5036a739251be1240a24080e28d7a8e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Animations/Intro_sequence/intro_seq_0015.png"
dest_files=["res://.godot/imported/intro_seq_0015.png-1a3c8239c9c1c61cd711f23fcc29af50.ctex"]
source_file="res://HUD/SLAM.png"
dest_files=["res://.godot/imported/SLAM.png-b5036a739251be1240a24080e28d7a8e.ctex"]
[params]
+6
View File
@@ -15,6 +15,10 @@ var _aerial_pitch: float = 0.0
var charge_pitch_target: 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
const TAIL_BONES: Array = [
&"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"))
_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.z = _tilt_z
+30 -3
View File
@@ -16,6 +16,12 @@ var _shake_amount: float = 0.0
var _shake_decay: 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:
set_as_top_level(true)
@@ -48,11 +54,12 @@ func _process(delta: float) -> void:
)
var target := get_parent() as Node3D
var focus := _drag_focus(target, delta)
global_position = target.global_position + offset
look_at(target.global_position, Vector3.UP)
global_position = focus + offset
look_at(focus, Vector3.UP)
_update_dof(target.global_position)
_update_dof(focus)
if _shake_amount > 0.005:
global_position += Vector3(
@@ -64,6 +71,26 @@ func _process(delta: float) -> void:
_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:
_shake_amount = strength * 0.5
_shake_decay = _shake_amount / 0.4
+8 -5
View File
@@ -387,12 +387,15 @@ func _reconcile(delta: float) -> void:
if scene != null:
_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():
var mi: MeshInstance3D = _overlays[id]
var mi: Variant = _overlays[id]
if not wanted.has(id) or not is_instance_valid(mi):
if is_instance_valid(mi):
mi.queue_free()
(mi as Node).queue_free()
_overlays.erase(id)
# Add overlays for newly seen shapes.
for id: int in wanted:
@@ -442,9 +445,9 @@ func _attach(cs: CollisionShape3D, is_area: bool) -> MeshInstance3D:
func _clear_overlays() -> void:
for id: int in _overlays:
var mi: MeshInstance3D = _overlays[id]
var mi: Variant = _overlays[id]
if is_instance_valid(mi):
mi.queue_free()
(mi as Node).queue_free()
_overlays.clear()
+70 -47
View File
@@ -22,25 +22,17 @@ func _ready() -> void:
func _register_all() -> void:
# ── Movement ──────────────────────────────────────────────────────────────
_reg_f("Movement", "thrust_impulse", 18.0, 1.0, 200.0)
_reg_f("Movement", "thrust_vertical", 2.0, 0.0, 20.0)
_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)
# WASD/arrows drive the bull: move_accel ramps flat velocity toward max_speed in
# the input direction; move_friction bleeds it off when no direction is held.
_reg_f("Movement", "max_speed", 50.0, 2.0, 1000.0)
_reg_f("Movement", "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", "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_min_impact", 1.0, 0.0, 20.0)
_reg_f("Movement", "wall_bounce_restitution", 0.6, 0.0, 1.5, 0.05)
_reg_f("Movement", "wall_min_bounce_speed", 6.0, 0.0, 30.0)
_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 ─────────────────────────────────────────────────
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.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_wag_freq", 4.0, 0.5, 15.0)
_reg_f("Tail", "tail_wag_idle", 0.3, 0.0, 2.0, 0.05)
# ── Charge (both mouse buttons) ──────────────────────────────────────────
_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)
# ── Charge pose (horns-down lean during dash / roll) ─────────────────────
_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)
# ── 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_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)
# 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 ───────────────────────────────────────────────────────────────
_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)
# Radius of the inner fighting floor: bull + matadors spawn on this ring (so
# they stay off the benches). Tune in the console to hug the bench edge.
_reg_f("Matador", "arena_spawn_radius", 10.0, 2.0, 22.0, 0.5)
_reg_f("Matador", "mat_idle_min", 0.5, 0.0, 5.0, 0.1)
_reg_f("Matador", "mat_idle_max", 2.5, 0.5, 10.0, 0.1)
_reg_f("Matador", "mat_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_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_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_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_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)
# 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).
_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_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_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_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
# 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_time", 0.18, 0.0, 0.6, 0.01)
_reg_f("Matador", "mat_lunge_speed", 13.0, 0.0, 20.0)
_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_duration", 0.45, 0.1, 1.5, 0.05)
# Playback rate of the Draw_weapon clip used for both drawing and sheathing —
# higher = snappier unholster/holster.
_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.
_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 ─────────────────────────────────────────────────────────────────
# Local seating of the sword in the right hand (drawn / fighting).
# 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_z", 0.0, -180.0, 180.0, 1.0)
# ── 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.
_reg_f("Sword", "mat_resword_delay", 2.0, 0.0, 10.0, 0.5)
_reg_f("Sword", "mat_throw_range", 18.0, 5.0, 40.0)
_reg_f("Sword", "mat_throw_min_dist", 5.0, 2.0, 15.0, 0.1)
_reg_f("Sword", "mat_throw_windup", 0.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_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)
# 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_release", -90.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", "dash_cooldown", 2.0, 0.5, 10.0, 0.1)
_reg_f("Abilities", "dash_speed", 66.0, 5.0, 200.0)
# ── Roll (boulder charge) ─────────────────────────────────────────────────
# A held-momentum roll: inherits your current charge, ramps toward roll_max_speed,
# and RICOCHETS off walls with a speed boost (bank shots build ludicrous speed).
_reg_f("Roll", "roll_cooldown", 3.0, 0.5, 15.0, 0.1)
_reg_f("Roll", "roll_duration", 2.5, 0.5, 6.0, 0.1)
_reg_f("Roll", "roll_min_speed", 30.0, 5.0, 120.0) # launch floor (combines charge)
_reg_f("Roll", "roll_max_speed", 95.0, 10.0, 200.0) # natural top from acceleration
_reg_f("Roll", "roll_accel", 55.0, 0.0, 300.0)
# Per-second speed retained above roll_max_speed — low friction so wall-boosted
# momentum bleeds off slowly rather than snapping back to the cruise speed.
_reg_f("Roll", "roll_momentum", 0.9, 0.1, 0.99, 0.01)
_reg_f("Roll", "roll_turn", 3.5, 0.2, 20.0) # steer rate (sluggish = momentum feel)
_reg_f("Roll", "roll_wall_boost", 1.25, 1.0, 2.0, 0.05) # speed × on each ricochet
_reg_f("Roll", "roll_wall_cap", 150.0, 10.0, 300.0) # ceiling reachable via boosts
_reg_f("Roll", "roll_hit_radius", 2.4, 0.5, 6.0, 0.1)
_reg_f("Roll", "roll_hit_strength", 22.0, 1.0, 40.0)
_reg_f("Roll", "roll_hit_up", 4.5, 0.0, 15.0)
# ── Roll (Rammus Powerball) ───────────────────────────────────────────────
# The bull curls into a ball and ACCELERATES the longer it rolls, from
# roll_base_speed up to roll_max_speed over roll_rampup_time. Turning is wide
# (momentum steering via roll_turn). On slamming into a matador the ball POPS:
# the target is launched skyward (roll_knockup) and the roll ends.
_reg_f("Roll", "roll_cooldown", 3.0, 0.5, 15.0, 0.1)
_reg_f("Roll", "roll_duration", 2.5, 0.5, 6.0, 0.1) # max roll time
_reg_f("Roll", "roll_base_speed", 28.0, 5.0, 120.0) # speed at the start of the roll
_reg_f("Roll", "roll_max_speed", 95.0, 10.0, 200.0) # top speed once fully ramped
_reg_f("Roll", "roll_rampup_time", 2.0, 0.2, 6.0, 0.1) # time to reach top speed
_reg_f("Roll", "roll_turn", 7.0, 0.2, 20.0) # steer rate (WASD/arrows; higher = tighter)
_reg_f("Roll", "roll_spin_radius", 1.4, 0.3, 4.0, 0.1) # ball radius for tumble spin (smaller = faster)
_reg_f("Roll", "roll_hit_radius", 2.4, 0.5, 6.0, 0.1) # contact radius that pops the ball
_reg_f("Roll", "roll_hit_strength", 22.0, 1.0, 40.0) # horizontal launch on the pop
_reg_f("Roll", "roll_knockup", 11.0, 0.0, 25.0) # vertical knock-up on the pop
# ── Debug overlays (see debug_draw.gd) ───────────────────────────────────
_reg_b("Debug", "show_collisions", 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
const _GOLD := Color(1.00, 0.78, 0.22)
const _CREAM := Color(0.88, 0.83, 0.72)
const _MUTED := Color(0.65, 0.60, 0.48)
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)
const _GOLD := Color(1.00, 0.78, 0.22)
const _CREAM := Color(0.93, 0.88, 0.72)
const _MUTED := Color(0.66, 0.60, 0.46)
# Scoring: each kill scores _KILL_BASE × the live combo count; the combo grows with
# every kill inside _COMBO_WINDOW seconds of the last and resets when that lapses.
const _KILL_BASE: int = 100
const _COMBO_WINDOW: float = 3.0
# Per-ability trim colour, tying each tile (and its key cap) to that ability's FX.
# Order matches _ABILITY_ICONS: kick, slam, dash, roll.
const _ABILITY_ACCENT: Array[Color] = [
Color(1.00, 0.62, 0.16), # Kick — ember orange
Color(0.98, 0.34, 0.20), # Slam — crimson
Color(0.45, 0.75, 1.00), # Dash — sky blue
Color(1.00, 0.82, 0.32), # Roll — gold
]
const _ABILITY_ICONS: Array[String] = [
"res://HUD/HUD_0000_ability_JALG.png",
"res://HUD/HUD_0001_ability_SLAM.png",
"res://HUD/HUD_0002_ability_DASH.png",
"res://HUD/HUD_0003_ability_ROLL.png",
"res://HUD/KICK.png",
"res://HUD/SLAM.png",
"res://HUD/DASH.png",
"res://HUD/ROLL.png",
]
const _ABILITY_ACTIONS: Array[StringName] = [
&"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"]
var _controls_visible: bool = false
@@ -30,20 +31,21 @@ var _toggle_btn: Button
var _tab_hint: Label
var _spawner: Node
var _player: Node = null
var _died_connected: bool = false
var _cd_overlays: Array[ColorRect] = []
var _cd_labels: Array[Label] = []
var _health_segments: Array[ColorRect] = []
var _health_connected: bool = false
var _ability_bar: HBoxContainer
var _ability_panels: Array[PanelContainer] = []
var _score: int = 0
var _combo: int = 0
var _combo_timer: float = 0.0
var _score_label: Label
var _wave_label: Label
var _combo_label: Label
var _combo_track: ColorRect
var _combo_fill: ColorRect
var _banner_label: Label
# Win/lose screen. _game_over latches so the result is shown once; _result_win
# records which way it went (read by the gameplay test).
const _WIN_VIDEO := "res://videos/bull_win.ogv" # the bull triumphs
const _LOSS_VIDEO := "res://videos/matador_win.ogv" # the matador triumphs
var _game_over: bool = false
var _result_win: bool = false
var _over_root: Control
var _over_video: VideoStreamPlayer
func _ready() -> void:
@@ -52,22 +54,35 @@ func _ready() -> void:
_build_controls_panel()
_build_tab_hint()
_build_ability_bar()
_build_health_bar()
_build_scoreboard()
_build_version_label()
_build_game_over()
_find_spawner.call_deferred()
func _process(delta: float) -> void:
_update_combo(delta)
# Unobtrusive early-build tag in the bottom-right corner.
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:
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
_player = players[0]
return
if not _health_connected:
_health_connected = true
_player.health_changed.connect(_on_health_changed)
_on_health_changed(_player.health)
if not _died_connected:
_died_connected = true
_player.died.connect(_on_player_died)
for i: int in _cd_overlays.size():
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
var ov := _cd_overlays[i]
@@ -87,14 +102,14 @@ func _find_spawner() -> void:
if spawners.is_empty():
return
_spawner = spawners[0]
_spawner.matador_killed.connect(_on_matador_killed)
_spawner.wave_changed.connect(_on_wave_changed)
_on_wave_changed(_spawner.current_wave())
_spawner.all_defeated.connect(_on_all_defeated)
func _unhandled_input(event: InputEvent) -> void:
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()
elif event.physical_keycode == KEY_ESCAPE:
_return_to_menu()
@@ -106,6 +121,12 @@ func _return_to_menu() -> void:
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:
_controls_visible = not _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"
func _on_reset_pressed() -> void:
if get_tree().paused:
get_tree().paused = false
if _spawner:
_spawner.reset_spawn()
_score = 0
_score_label.text = "SCORE 0"
_reset_combo()
# ── Win / lose ──────────────────────────────────────────────────────────────
# The bull takes one clean blade and it's over; clear the last matador and it's a
# victory. Both routes end here — the arena freezes and the result is proclaimed.
func _on_player_died(cause: String = "") -> void:
_show_game_over(false, cause)
# A bordered, rounded, gold-trimmed panel background — the HUD's one repeated look.
# Margins are symmetric (L=R, T=B); pass a transparent border to draw none.
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 _on_all_defeated() -> void:
_show_game_over(true)
func _build_health_bar() -> void:
var bar := HBoxContainer.new()
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:
func _show_game_over(win: bool, _cause: String = "") -> void:
if _game_over:
return
_combo_timer -= delta
if _combo_timer <= 0.0:
_reset_combo()
elif _combo_track.visible:
_combo_fill.anchor_right = clampf(_combo_timer / _COMBO_WINDOW, 0.0, 1.0)
_game_over = true
_result_win = win
if _controls_panel:
_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:
_combo = 0
_combo_timer = 0.0
_combo_label.text = ""
_combo_track.visible = false
func _build_game_over() -> void:
_over_root = Control.new()
_over_root.set_anchors_preset(Control.PRESET_FULL_RECT)
_over_root.process_mode = Node.PROCESS_MODE_ALWAYS
_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:
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)
const _SLOT_SEPARATION: int = 10
func _build_ability_bar() -> void:
var bar := HBoxContainer.new()
bar.add_theme_constant_override("separation", 10)
bar.anchor_left = 0.5
bar.anchor_right = 0.5
bar.anchor_top = 1.0
bar.anchor_bottom = 1.0
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
bar.offset_left = -160.0
bar.offset_bottom = -20.0
add_child(bar)
_ability_bar = HBoxContainer.new()
_ability_bar.add_theme_constant_override("separation", _SLOT_SEPARATION)
_ability_bar.anchor_left = 0.5
_ability_bar.anchor_right = 0.5
_ability_bar.anchor_top = 1.0
_ability_bar.anchor_bottom = 1.0
_ability_bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
_ability_bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
_ability_bar.offset_bottom = -20.0
add_child(_ability_bar)
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:
@@ -330,8 +247,10 @@ func _build_ability_slot(idx: int) -> Control:
var panel := PanelContainer.new()
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)
_ability_panels.append(panel)
var stack := Control.new()
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]):
var icon := TextureRect.new()
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.set_anchors_preset(Control.PRESET_FULL_RECT)
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
stack.add_child(icon)
else:
var name_lbl := Label.new()
@@ -378,8 +301,11 @@ func _build_ability_slot(idx: int) -> Control:
var key_lbl := Label.new()
key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
key_lbl.add_theme_color_override("font_color", _GOLD)
key_lbl.add_theme_font_size_override("font_size", 12)
key_lbl.add_theme_font_override("font", UiFonts.body())
key_lbl.add_theme_color_override("font_color", _ABILITY_ACCENT[idx].lerp(_CREAM, 0.35))
key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
key_lbl.add_theme_constant_override("outline_size", 3)
key_lbl.add_theme_font_size_override("font_size", 13)
vbox.add_child(key_lbl)
return vbox
@@ -402,7 +328,8 @@ func _build_tab_hint() -> void:
func _build_controls_panel() -> void:
_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_top = 1.0
@@ -424,37 +351,22 @@ func _build_controls_panel() -> void:
Controls.get_key_label(&"move_back"),
Controls.get_key_label(&"move_right"),
]
_row(vbox, move_keys, "Move (not intended)", _MUTED)
_row(vbox, "LMB (hold)", "Right horn", _CREAM)
_row(vbox, "RMB (hold)", "Left horn", _CREAM)
_row(vbox, move_keys, "Move", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_kick"), "Kick", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_slam"), "Slam", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_dash"), "Dash", _CREAM)
_row(vbox, Controls.get_key_label(&"ability_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, "R", "Respawn matadors (cooldown)", _MUTED)
_row(vbox, "Tab", "Toggle controls", _MUTED)
_row(vbox, "Esc", "Back to menu", _MUTED)
var sep := HSeparator.new()
var sep_style := StyleBoxFlat.new()
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)
sep.add_theme_stylebox_override("separator", UiTheme.rule())
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.pressed.connect(_toggle_controls)
hbox.add_child(_toggle_btn)
var reset_btn := _make_button("Reset")
reset_btn.pressed.connect(_on_reset_pressed)
hbox.add_child(reset_btn)
vbox.add_child(_toggle_btn)
func _make_button(label_text: String) -> Button:
@@ -462,15 +374,17 @@ func _make_button(label_text: String) -> Button:
btn.text = label_text
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 hover := _panel_style(Color(0.30, 0.20, 0.06, 0.95), 5, 12.0, 6.0, _GOLD)
var normal := UiTheme.plaque(Color(0.10, 0.07, 0.03, 0.90), _GOLD, 3, 18.0, 9.0)
var hover := UiTheme.plaque(Color(0.16, 0.10, 0.04, 0.96), _CREAM, 3, 18.0, 9.0)
btn.add_theme_stylebox_override("normal", normal)
btn.add_theme_stylebox_override("hover", 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_hover_color", _GOLD)
btn.add_theme_font_size_override("font_size", 12)
btn.add_theme_color_override("font_hover_color", _CREAM)
btn.add_theme_font_size_override("font_size", 18)
return btn
@@ -481,7 +395,7 @@ func _row(parent: VBoxContainer, key: String, desc: String, desc_color: Color) -
parent.add_child(row)
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)
row.add_child(badge)
+67
View File
@@ -6,6 +6,10 @@ extends Control
const GAME_SCENE := "res://scene.tscn"
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.
## 34 frames at 24 fps = 1.4167 s loop.
const VIDEO_FPS := 24.0
@@ -90,10 +94,69 @@ func _ready() -> void:
exit_button.pressed.connect(_on_exit_pressed)
options_back.pressed.connect(_close_subpanels)
credits_back.pressed.connect(_close_subpanels)
_apply_theme()
_build_version_label()
_build_rebind_rows()
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:
if not main_panel.visible or video.stream == null:
return
@@ -117,6 +180,9 @@ func _build_rebind_rows() -> void:
var name_label := Label.new()
name_label.text = ACTION_LABELS[action]
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_constant_override("outline_size", 4)
row.add_child(name_label)
@@ -124,6 +190,7 @@ func _build_rebind_rows() -> void:
var key_button := Button.new()
key_button.custom_minimum_size = Vector2(160, 0)
key_button.text = Controls.get_key_label(action)
_style_button(key_button, false)
key_button.pressed.connect(_on_rebind_pressed.bind(action, key_button))
row.add_child(key_button)
+183 -64
View File
@@ -10,6 +10,10 @@ extends CharacterBody3D
# RAGDOLL → hit by bull, no roll chance
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
var _state: State = State.WANDER
@@ -35,6 +39,8 @@ var _throw_aim_cd: float = 0.0
var _throw_dir: Vector3 = Vector3.ZERO
var _steer_dir: Vector3 = Vector3.ZERO
var _steer_cd: float = 0.0
var _intent: Intent = Intent.WANDER
var _decide_cd: float = 0.0
var _ole_player: AudioStreamPlayer = null
var _sword_hand_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.
const _STEER_CANDIDATES: Array = [-0.35, 0.35, -0.7, 0.7, -1.1, 1.1, PI]
# Movement tuning
const _ACCEL_FACTOR: float = 8.0 # velocity ramp = speed * factor * delta
const _TURN_MOVE: float = 8.0 # face the travel direction while roaming
const _TURN_FACE: float = 12.0 # face the bull while engaging
const _TURN_SHARP: float = 20.0 # snap onto a sidestep / roll direction
# Movement tuning. Ramp/turn rates are deliberately snappy so the matador reads as
# a quick, dangerous opponent rather than a sluggish one.
const _ACCEL_FACTOR: float = 13.0 # velocity ramp = speed * factor * delta
const _TURN_MOVE: float = 11.0 # face the travel direction while roaming
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:
@@ -97,7 +104,7 @@ func _ready() -> void:
_anim_player.play(_ANIM_IDLE)
_setup_sword()
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)
_pick_wander_target()
_blood_burst = preload("res://blood_burst.gd").new()
@@ -148,7 +155,7 @@ func _physics_process(delta: float) -> void:
if _resword_timer <= 0.0:
_spawn_sword()
if _state != State.RAGDOLL and _bull != null:
_update_ai_state()
_update_ai_state(delta)
match _state:
State.WANDER: _tick_wander(delta)
@@ -164,42 +171,110 @@ func _physics_process(delta: float) -> void:
_advance_draw(delta)
func _update_ai_state() -> void:
if _state == State.THROW:
# Utility-driven brain. The physically committed states (a brace, a pass, a roll, a
# 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
_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)
if (_state == State.WANDER or _state == State.FLEE or _state == State.ATTACK) \
and _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"):
# Roll for a throw; on a miss, wait before becoming eligible again so the
# matador commits to chasing for a melee Attack instead of throwing early.
if randf() < DP.f("mat_throw_chance"):
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
var bull_speed := bull_flat.length()
# close = 1 right on top of us, 0 at attack range or beyond. `beyond` measures
# how far past throw range the bull is (0 point-blank, 1 at/outside throw range):
# 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()
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
@@ -339,16 +414,21 @@ func _tick_brace(delta: float) -> void:
_brace_timer -= delta
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
_step_timer = DP.f("mat_step_duration")
if _ole_player:
_ole_player.pitch_scale = randf_range(0.88, 1.12)
_ole_player.play()
else:
_state = State.FLEE
else:
# 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")
_start_attack()
# 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
_face_point(_bull.global_position, delta, _TURN_SHARP)
_play_anim(_ANIM_ATTACK)
_try_melee_hit(DP.f("mat_stab_reach"))
move_and_slide()
@@ -406,6 +487,7 @@ func _tick_attack(delta: float) -> void:
else:
_decelerate(22.0, delta)
_play_anim(_ANIM_ATTACK)
_try_melee_hit(DP.f("mat_stab_reach"))
else:
var dir := _steer_clear(to_bull.normalized(), delta)
_accelerate(dir, DP.f("mat_attack_speed"), delta)
@@ -481,6 +563,10 @@ func _tick_throw(delta: float) -> void:
func _end_throw() -> void:
_state = State.FLEE
_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
# (a bare resume() would snap and leave the player on whatever it paused on).
if _anim_player:
@@ -539,15 +625,20 @@ func _release_sword() -> void:
rb.collision_layer = 0
rb.collision_mask = 1
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)
rb.global_transform = gx
sword.reparent(rb, true)
# Physics collider along the blade (GLB +Z), which equals rb-local +Z because
# the mesh kept its global transform and rb adopted it.
# the mesh kept its global transform and rb adopted it. Fattened so a fast throw
# reliably overlaps the bull's collision spheres instead of tunnelling past.
var cap := CapsuleShape3D.new()
cap.radius = 0.06
cap.radius = 0.14
cap.height = 1.4
var cs := CollisionShape3D.new()
cs.shape = cap
@@ -555,19 +646,25 @@ func _release_sword() -> void:
cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
rb.add_child(cs)
# Lead the target: aim where the bull will be when the sword arrives, so a
# moving bull isn't simply behind the throw by the time it lands.
var aim := _throw_dir
# Aim at the bull's BODY (its collision spheres ride low, ~0.5 m below the
# origin), leading a moving target so it arrives where the bull will be. The aim
# keeps its vertical component so the blade drives into the body, not over it.
var speed := maxf(DP.f("mat_throw_speed"), 0.1)
var dir := (_throw_dir + Vector3.UP * 0.0)
if is_instance_valid(_bull):
var speed := maxf(DP.f("mat_throw_speed"), 0.1)
var flat := _bull.global_position - gx.origin
var target := _bull.global_position + Vector3(0.0, DP.f("mat_throw_aim_y"), 0.0)
var flat := target - gx.origin
flat.y = 0.0
var lead := (_bull.global_position + _bull.velocity * (flat.length() / speed)) - gx.origin
lead.y = 0.0
if lead.length() > 0.1:
aim = lead.normalized()
var dir := (aim + Vector3.UP * 0.12).normalized()
rb.linear_velocity = dir * DP.f("mat_throw_speed")
var flight := flat.length() / speed
target += _bull.velocity * flight
var to_target := target - gx.origin
if to_target.length() > 0.1:
dir = to_target.normalized()
# A little aim scatter so throws are a threat to READ and dodge, not a hitscan —
# a bull that keeps moving can slip them, a stationary one gets pinned.
var spread := deg_to_rad(DP.f("mat_throw_spread"))
dir = dir.rotated(Vector3.UP, randf_range(-spread, spread)).normalized()
rb.linear_velocity = dir * speed
rb.angular_velocity = dir.cross(Vector3.UP).normalized() * -DP.f("mat_throw_spin")
var hit_done := [false]
@@ -576,7 +673,7 @@ func _release_sword() -> void:
return
if body.is_in_group(&"player"):
hit_done[0] = true
body.call(&"take_sword_hit")
body.call(&"take_sword_hit", "thrown")
)
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:
if _state != State.ATTACK and _state != State.SIDESTEP:
if _state != State.ATTACK and _state != State.SIDESTEP and _state != State.BRACE:
return
if not body.is_in_group(&"player"):
return
if _blade_hit_cd > 0.0:
return
_blade_hit_cd = 0.5
body.call(&"take_sword_hit")
_blade_hit_cd = DP.f("mat_stab_cooldown")
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:
@@ -833,8 +952,8 @@ func _spawn_sword() -> void:
# 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).
var blade_cap := CapsuleShape3D.new()
blade_cap.radius = 0.10
blade_cap.height = 1.10
blade_cap.radius = 0.18
blade_cap.height = 1.30
var blade_cs := CollisionShape3D.new()
blade_cs.shape = blade_cap
blade_cs.position = Vector3(0.0, 0.0, 0.8)
+33 -49
View File
@@ -1,61 +1,53 @@
extends Node3D
signal matador_killed
signal wave_changed(wave: int)
signal all_defeated
const MATADOR := preload("res://Matador.tscn")
const RESPAWN_COOLDOWN := 8.0
var _respawn_cooldown: float = 0.0
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 _spawn_count: int = 1
var _alive: int = 0
var _resolved: bool = false
func _ready() -> void:
add_to_group(&"matador_spawn")
_spawn_count = maxi(1, int(DP.f("mat_spawn_count")))
_start_wave(1)
# 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)
# Deferred so it's safe from _ready and matches the previous spawn timing.
_spawn.call_deferred()
func _spawn() -> void:
var positions := _spread_positions(_spawn_count)
for pos: Vector3 in positions:
# Bull and matadors start at opposite ends of a random diameter, so they always
# begin as far apart as the arena allows (edge-to-edge across the centre).
var arena_r := DP.f("arena_spawn_radius")
var bull_angle := randf() * TAU
_place_bull(bull_angle, arena_r)
for pos: Vector3 in _spread_positions(_spawn_count, bull_angle + PI, arena_r):
_spawn_at(pos)
func _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 arena_r := DP.f("mat_wander_radius") * 0.72
var min_r := 5.0
if count <= 1:
positions.append(Vector3(cos(center_angle) * arena_r, 1.0, sin(center_angle) * arena_r))
return positions
var arc := TAU * 0.33
for i: int in count:
var sector := TAU / count
var angle := sector * i + randf_range(-sector * 0.35, sector * 0.35)
var radius := randf_range(min_r, arena_r)
var t := float(i) / float(count - 1) - 0.5 # -0.5 .. 0.5 across the arc
var angle := center_angle + t * arc
var radius := randf_range(arena_r * 0.7, arena_r)
positions.append(Vector3(cos(angle) * radius, 1.0, sin(angle) * radius))
return positions
@@ -68,18 +60,10 @@ func _spawn_at(pos: Vector3) -> void:
_alive += 1
# Every matador down = the bull wins. No waves, no respawns — one clean arena.
func _on_matador_killed() -> void:
matador_killed.emit()
_alive -= 1
if _alive <= 0:
_spawn_count *= 2
_start_wave(_wave + 1)
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)
if _alive <= 0 and not _resolved:
_resolved = true
all_defeated.emit()
+132 -267
View File
@@ -22,12 +22,8 @@ const _ANIM_ROLL: StringName = &"Armature|ROLL"
var _hoof_emitters: Array[CPUParticles3D] = []
var _legs: Node
var _kick_timer: float = 0.0
var _was_on_floor: bool = false
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
@@ -38,30 +34,24 @@ var _ability_timer: float = 0.0
var _active_ability: int = -1
var _dash_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 _charge_ready_emitter: CPUParticles3D = null
var _slam_pending: bool = false
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)
const _CHARGE_BAR_WIDTH: float = 1.1
const _CHARGE_BAR_HEIGHT: float = 1.45
var _charge_bar: Node3D = null
var _charge_bar_fill: MeshInstance3D = null
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)
# One clean hit ends the run — see take_sword_hit(). The HUD listens for `died`
# to raise the lose screen (the `cause` tags how it happened, for the death notice);
# _dead latches so a flurry of hits fires it only once.
signal died(cause: String)
var _dead: bool = false
var _huff_player: AudioStreamPlayer = null
var _crowd_player: AudioStreamPlayer = null
var _huff_timer: float = 0.0
var _roll_damping: float = 0.15
enum DustState { NONE, WALK, CHARGE }
var _dust_state: DustState = DustState.NONE
@@ -71,15 +61,15 @@ var _charge_dust_ramp: Gradient = null
func _ready() -> void:
add_to_group(&"player")
_cube_scale = cube_guy.scale
_setup_hoof_dust()
_setup_legs()
_setup_charge_indicators()
_setup_charge_bar()
_setup_charge_trail()
_setup_roll_sparks()
_setup_audio()
_bull_anim = _find_anim_player(cube_guy)
_setup_bull_idle()
DP.any_changed.connect(_on_dp_changed)
_roll_damping = DP.f("roll_damping")
func _setup_legs() -> void:
@@ -129,39 +119,8 @@ func _setup_hoof_dust() -> void:
_hoof_emitters.append(p)
func _setup_charge_indicators() -> void:
var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material())
# 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
func _setup_charge_trail() -> void:
# Blue energy trail — streams backward during a fast run / roll
var trail_sphere := _particle_sphere(0.065, 4, 2, _unshaded_material())
var trail_ramp := Gradient.new()
@@ -194,93 +153,39 @@ func _setup_charge_indicators() -> void:
cube_guy.add_child(_charge_trail_emitter)
func _setup_charge_bar() -> void:
_charge_bar = Node3D.new()
_charge_bar.visible = false
add_child(_charge_bar)
func _setup_roll_sparks() -> void:
# Rammus Powerball ground spray — dirt/sparks flung outward from the spinning
# ball. World-space (local_coords off) so they peel off into a trail behind it.
var spark := _particle_sphere(0.05, 4, 2, _unshaded_material())
var w := _CHARGE_BAR_WIDTH
var thickness := 0.16
var ramp := Gradient.new()
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 track_mat := StandardMaterial3D.new()
track_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
track_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
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)
var sc := Curve.new()
sc.add_point(Vector2(0.0, 0.30))
sc.add_point(Vector2(0.3, 1.00))
sc.add_point(Vector2(1.0, 0.00))
# Glowing fill bar; scaled along X each frame to show progress.
_charge_bar_fill_mat = StandardMaterial3D.new()
_charge_bar_fill_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
_charge_bar_fill_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
_charge_bar_fill_mat.emission_enabled = true
var fill_box := BoxMesh.new()
fill_box.size = Vector3(w, thickness, thickness)
fill_box.material = _charge_bar_fill_mat
_charge_bar_fill = MeshInstance3D.new()
_charge_bar_fill.mesh = fill_box
_charge_bar_fill.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_charge_bar.add_child(_charge_bar_fill)
# Cool → hot colour ramp as the charge fills (matches the spark FX palette).
_charge_bar_ramp = Gradient.new()
_charge_bar_ramp.offsets = PackedFloat32Array([0.0, 0.6, 1.0])
_charge_bar_ramp.colors = PackedColorArray([
Color(0.25, 0.65, 1.0),
Color(1.0, 0.85, 0.2),
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
_roll_spark_emitter = CPUParticles3D.new()
_roll_spark_emitter.position = Vector3(0.0, 0.10, 0.0)
_roll_spark_emitter.emitting = false
_roll_spark_emitter.one_shot = false
_roll_spark_emitter.amount = 46
_roll_spark_emitter.lifetime = 0.45
_roll_spark_emitter.randomness = 0.7
_roll_spark_emitter.local_coords = false
_roll_spark_emitter.direction = Vector3(0.0, 1.0, 0.0)
_roll_spark_emitter.spread = 180.0
_roll_spark_emitter.gravity = Vector3(0.0, -6.0, 0.0)
_roll_spark_emitter.initial_velocity_min = 3.0
_roll_spark_emitter.initial_velocity_max = 8.0
_roll_spark_emitter.scale_amount_min = 0.5
_roll_spark_emitter.scale_amount_max = 1.3
_roll_spark_emitter.mesh = spark
_roll_spark_emitter.color_ramp = ramp
_roll_spark_emitter.scale_amount_curve = sc
cube_guy.add_child(_roll_spark_emitter)
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:
_roll_damping = DP.f("roll_damping")
if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
return
var prev := _dust_state
@@ -460,15 +364,17 @@ func _activate_roll() -> void:
_ability_active = true
_active_ability = 3
_ability_timer = DP.f("roll_duration")
# Combine with any charge already underway: keep the current heading + speed,
# floored to a launch minimum so even a standing roll shoves off like a boulder.
_roll_ramp = 0.0
_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)
if flat.length() > 1.0:
_roll_dir = flat.normalized()
else:
var f := cube_guy.global_transform.basis.z
_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.z = _roll_dir.z * launch
if _bull_anim and _bull_anim.has_animation(_ANIM_ROLL):
@@ -476,6 +382,9 @@ func _activate_roll() -> void:
_bull_anim.play(_ANIM_ROLL)
_legs.set(&"tail_ragdoll", true) # tail streams out behind
_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()
camera_pivot.call(&"trigger_hit", 0.35)
if _huff_player:
@@ -483,27 +392,32 @@ func _activate_roll() -> void:
_huff_player.play()
# One rolling frame: steer, ramp/retain speed, move, then ricochet off walls and
# bowl through matadors. Fully replaces the normal locomotion path while active.
# One rolling frame (Rammus Powerball): steer with momentum, accelerate the longer
# 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:
var steer_flat := Vector3(steer.x, 0.0, steer.z)
if steer_flat.length() > 0.1:
var t := clampf(DP.f("roll_turn") * delta, 0.0, 1.0)
_roll_dir = _roll_dir.slerp(steer_flat.normalized(), t).normalized()
# Ramp toward the cruise top speed; anything above it (from wall boosts) bleeds
# off slowly via roll_momentum instead of snapping back — that's the fun carry.
var speed := Vector2(velocity.x, velocity.z).length()
# Speed builds over time: base → max across roll_rampup_time, so a long roll
# winds up into a runaway boulder while a quick tap barely gets going.
_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")
if speed < top:
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"))
var speed := lerpf(DP.f("roll_base_speed"), top, ramp)
velocity.x = _roll_dir.x * 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):
_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:
_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()
_was_on_floor = is_on_floor()
_roll_wall_ricochet(pre_wall)
# 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"))
_roll_try_pop(speed)
# On hitting a wall mid-roll, mirror the heading and BOOST speed (capped) instead of
# bleeding it — banking off walls is how you build ludicrous momentum.
func _roll_wall_ricochet(pre_wall: Vector3) -> void:
for i in get_slide_collision_count():
var col := get_slide_collision(i)
var normal := col.get_normal()
if absf(normal.y) >= 0.5:
# Pop the ball on ramming a matador: launch every matador in the hit radius up and
# outward, then end the roll (Rammus stops when Powerball connects). Returns true if
# it popped so the caller can stop touching roll state this frame.
func _roll_try_pop(speed: float) -> bool:
var radius := DP.f("roll_hit_radius")
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
to_mat.y = 0.0
if to_mat.length() > radius:
continue
var flat_n := Vector3(normal.x, 0.0, normal.z).normalized()
var refl := pre_wall - 2.0 * pre_wall.dot(flat_n) * flat_n
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))
_hit_matadors_radius(radius, DP.f("roll_hit_strength"), DP.f("roll_knockup"))
camera_pivot.call(&"trigger_hit", clampf(speed / 90.0, 0.35, 1.0))
if _huff_player:
_huff_player.pitch_scale = randf_range(1.05, 1.35)
_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,
@@ -884,36 +804,6 @@ func _make_slam_ring_mesh() -> ArrayMesh:
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:
# ── Gravity ───────────────────────────────────────────────────────────────
if not is_on_floor():
@@ -939,12 +829,15 @@ func _physics_process(delta: float) -> void:
if _ability_active:
_ability_timer -= delta
if _ability_timer <= 0.0:
_ability_active = false
_active_ability = -1
_legs.set(&"charge_pitch_target", 0.0)
_legs.set(&"tail_ragdoll", false)
_slam_pending = false
_play_idle()
if _active_ability == 3:
_end_roll() # tears down roll-specific state (sparks, tumble control)
else:
_ability_active = false
_active_ability = -1
_legs.set(&"charge_pitch_target", 0.0)
_legs.set(&"tail_ragdoll", false)
_slam_pending = false
_play_idle()
# ── Boulder roll ──────────────────────────────────────────────────────────
# 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)
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
# ── Periodic kick ─────────────────────────────────────────────────────────
_kick_timer = maxf(0.0, _kick_timer - delta)
if direction and _kick_timer <= 0.0:
_apply_kick(direction)
_kick_timer = DP.f("kick_interval")
# ── Rolling friction (exponential, frame-rate independent) ────────────────
var retain := pow(_roll_damping, delta)
velocity.x *= retain
velocity.z *= retain
# ── Ground locomotion — smooth accelerate / friction ──────────────────────
# WASD/arrows drive the bull directly: hold a direction to ramp up to max_speed
# and steer, release to coast to a stop. Ability bursts (dash) and wall bounces
# can exceed max_speed — while a direction is held we steer that momentum without
# braking it (cap tracks the current speed); friction only bleeds the excess once
# input is released.
var flat_vel := Vector3(velocity.x, 0.0, velocity.z)
var max_speed := DP.f("max_speed")
if direction.length_squared() > 0.01:
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 ─────────────────────────────
var flat_speed := Vector2(velocity.x, velocity.z).length()
@@ -1065,6 +925,11 @@ func _physics_process(delta: float) -> void:
# ── Hoof sounds ───────────────────────────────────────────────────────────
func take_sword_hit() -> void:
health = maxi(0, health - 1)
health_changed.emit(health)
# A single clean hit — thrown or swung blade — ends the run. `cause` ("gored" /
# "thrown") tags how it happened for the death notice. Latches so the lose screen is
# 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]
config/name="Bullosseum"
config/version="0.1.0"
run/main_scene="res://MainMenu.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")
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)
]
}
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={
"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)
+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