Compare commits
21 Commits
708a35d51f
..
0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 56593c58cf | |||
| ce6f3d7075 | |||
| a74182255b | |||
| ebf5046461 | |||
| 271de1b045 | |||
| 3defc9da95 | |||
| ff75dacfb6 | |||
| bf7a454ad0 | |||
| ebb6ec6335 | |||
| a0b248748c | |||
| 5ef5889979 | |||
| 2e9d62b964 | |||
| 622830a6ee | |||
| 603a674e08 | |||
| 631df017d1 | |||
| 2c0142d208 | |||
| c283eb0927 | |||
| d379b215e2 | |||
| cd73a21ab7 | |||
| 6a67bfea36 | |||
| b929ed2a7a |
@@ -2,3 +2,4 @@ disable:
|
|||||||
- class-definitions-order
|
- class-definitions-order
|
||||||
- no-else-return
|
- no-else-return
|
||||||
max-line-length: 120
|
max-line-length: 120
|
||||||
|
max-file-lines: 1200
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ extends Node3D
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
add_to_group(&"public") # excluded from debug overlays — see publikum_controller.gd
|
||||||
var anim := get_node_or_null(^"AnimationPlayer") as AnimationPlayer
|
var anim := get_node_or_null(^"AnimationPlayer") as AnimationPlayer
|
||||||
if anim:
|
if anim:
|
||||||
anim.play(&"Juubeldab")
|
anim.play(&"Juubeldab")
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cl54dt38yj2x"
|
||||||
|
path="res://.godot/imported/Arena.glb-bf6b057edaab2fa44513d7f705517361.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Arena.glb"
|
||||||
|
dest_files=["res://.godot/imported/Arena.glb-bf6b057edaab2fa44513d7f705517361.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
|
After Width: | Height: | Size: 598 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dk6xuwwa0mfpe"
|
||||||
|
path.s3tc="res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "c8b9b7d6205ce80dfcfaa38152b996fe"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Arena_sand.png"
|
||||||
|
dest_files=["res://.godot/imported/Arena_sand.png-416de2afe03241df088061e1a5f831af.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
|
After Width: | Height: | Size: 412 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b4gkpa7jypcsm"
|
||||||
|
path.s3tc="res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "0e7ed34c3c812d96732bd32f9dca0c93"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Arena_wall.png"
|
||||||
|
dest_files=["res://.godot/imported/Arena_wall.png-e67bd4bbd2f60583bfc14dd8b7687eef.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
|
After Width: | Height: | Size: 180 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://yercpq3y2o0x"
|
||||||
|
path.s3tc="res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
generator_parameters={
|
||||||
|
"md5": "23a7a951553ffe271c47693fd71314ca"
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Arena_wood.png"
|
||||||
|
dest_files=["res://.godot/imported/Arena_wood.png-b4ed20d66b4d375396b0be2f2f863d80.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dxe72ig8gfbcg"
|
||||||
|
path="res://.godot/imported/arena model2026.glb-299db01cae891e5005f8ac1e6c83d0a6.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/arena model2026.glb"
|
||||||
|
dest_files=["res://.godot/imported/arena model2026.glb-299db01cae891e5005f8ac1e6c83d0a6.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
|
After Width: | Height: | Size: 16 KiB |
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://ypxkr3vla3fr"
|
uid="uid://ml0ntjv5injp"
|
||||||
path="res://.godot/imported/HUD_0000_ability_JALG.png-c430df7817ae1f22045a2a27b06f60a8.ctex"
|
path="res://.godot/imported/bull_hud_bg.png-4645abe7114935a3d4a8e0f88ccce15c.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HUD/HUD_0000_ability_JALG.png"
|
source_file="res://Assets/bull_hud_bg.png"
|
||||||
dest_files=["res://.godot/imported/HUD_0000_ability_JALG.png-c430df7817ae1f22045a2a27b06f60a8.ctex"]
|
dest_files=["res://.godot/imported/bull_hud_bg.png-4645abe7114935a3d4a8e0f88ccce15c.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 115 KiB |
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://w4yovxyg0104"
|
uid="uid://d166u18p3wqiy"
|
||||||
path="res://.godot/imported/HUD_0001_ability_SLAM.png-7c5f88306e1af474db2c2073888feab7.ctex"
|
path="res://.godot/imported/crowd_clap_sheet.png-a110e7d663af8fcbe9ebf5de44188c85.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HUD/HUD_0001_ability_SLAM.png"
|
source_file="res://Assets/crowd_clap_sheet.png"
|
||||||
dest_files=["res://.godot/imported/HUD_0001_ability_SLAM.png-7c5f88306e1af474db2c2073888feab7.ctex"]
|
dest_files=["res://.godot/imported/crowd_clap_sheet.png-a110e7d663af8fcbe9ebf5de44188c85.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://csxxlnq74fej4
|
||||||
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 12 MiB |
@@ -0,0 +1,12 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
|
||||||
|
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
animation_player.play("Taunt")
|
||||||
|
animation_player.animation_finished.connect(_on_animation_finished)
|
||||||
|
|
||||||
|
func _on_animation_finished(anim_name):
|
||||||
|
if anim_name == "Taunt":
|
||||||
|
animation_player.play("Taunt")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dycnoiadljbb1
|
||||||
|
After Width: | Height: | Size: 296 KiB |
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
importer="texture"
|
importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://dnv8w5h32ck7m"
|
uid="uid://bxv1uuymonrp6"
|
||||||
path="res://.godot/imported/HUD_0002_ability_DASH.png-011f272dda65c06c407720ec201ad92e.ctex"
|
path="res://.godot/imported/menu_bg.png-3906063022fa3c81090e3c663f8866b0.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"vram_texture": false
|
"vram_texture": false
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://HUD/HUD_0002_ability_DASH.png"
|
source_file="res://Assets/menu_bg.png"
|
||||||
dest_files=["res://.godot/imported/HUD_0002_ability_DASH.png-011f272dda65c06c407720ec201ad92e.ctex"]
|
dest_files=["res://.godot/imported/menu_bg.png-3906063022fa3c81090e3c663f8866b0.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://jepc74w8opbu"
|
||||||
|
path="res://.godot/imported/spear.glb-0139879d219c93f9dd35db2304db7d5d.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/spear.glb"
|
||||||
|
dest_files=["res://.godot/imported/spear.glb-0139879d219c93f9dd35db2304db7d5d.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -90,13 +90,28 @@ bash run_tests.sh
|
|||||||
# Individual tests:
|
# Individual tests:
|
||||||
gdlint *.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
gdlint *.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
||||||
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
||||||
|
godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s
|
||||||
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
|
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
|
||||||
|
|
||||||
# Gameplay test with real rendering (inspect frame strip visually):
|
# Gameplay test with real rendering (inspect frame strip visually):
|
||||||
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
||||||
# Output: tests/output/gameplay/motion_00..04.png, ragdoll_trigger.png, ragdoll_result.png
|
# Output: tests/output/gameplay/motion_00..04.png, ragdoll_trigger.png, ragdoll_result.png
|
||||||
|
|
||||||
|
# Difficulty simulation (balance tool, ~2 min):
|
||||||
|
godot --headless --script tests/difficulty_sim.gd # drives a bot bull vs one matador
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Difficulty simulation (`tests/difficulty_sim.gd`)
|
||||||
|
A balance tool, not a pass/fail test. It drives a kinematic "bot bull" against one
|
||||||
|
real matador across behaviour profiles (CHARGER / KITER / PASSIVE) and reports the
|
||||||
|
matador's win rate (how often it gores the bull). Use it to tune matador difficulty
|
||||||
|
from data. The bot bull has NO abilities (it only kills by ramming), so it can't use
|
||||||
|
roll/slam/kick — real skilled play beats the matador more than these numbers imply;
|
||||||
|
read the rates as "how well the matador threatens each movement pattern." A rough
|
||||||
|
target is CHARGER high (blind charging is punished), KITER moderate (throws are a
|
||||||
|
dodgeable threat), PASSIVE high (standing still dies). Note: as a `--script` entry it
|
||||||
|
reads the `DP` autoload via `/root/DP`, not the global identifier.
|
||||||
|
|
||||||
### What the gameplay test catches
|
### What the gameplay test catches
|
||||||
| Check | What it detects |
|
| Check | What it detects |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -107,6 +122,13 @@ godot --script tests/gameplay_test.gd # opens a window, saves re
|
|||||||
|
|
||||||
Headless screenshots will be blank (Forward Plus has no display). Run without `--headless` to get real frame strips.
|
Headless screenshots will be blank (Forward Plus has no display). Run without `--headless` to get real frame strips.
|
||||||
|
|
||||||
|
### What the performance test catches
|
||||||
|
Loads the full scene with `STRESS_MATADORS` (20) matadors, samples per-frame time
|
||||||
|
during normal play and during a mass-ragdoll spike, and fails if the average
|
||||||
|
exceeds 16 ms or any single frame exceeds 100 ms. Budgets are generous regression
|
||||||
|
guards (not a target frame rate); the measured avg/peak/fps print every run so a
|
||||||
|
gradual creep shows up before it trips the ceiling.
|
||||||
|
|
||||||
## Godot 4.x specifics
|
## Godot 4.x specifics
|
||||||
|
|
||||||
- `wrapf` / `wrap` instead of manual modulo for angles
|
- `wrapf` / `wrap` instead of manual modulo for angles
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bxm7c6ldrdiq5"
|
||||||
|
path.bptc="res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://HDRI/citrus_orchard_road_puresky_4k.hdr"
|
||||||
|
dest_files=["res://.godot/imported/citrus_orchard_road_puresky_4k.hdr-262868e8787abf05f7044da72beefce7.bptc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://3syhacswepyj"
|
||||||
|
path.bptc="res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://HDRI/kloppenheim_06_puresky_4k.hdr"
|
||||||
|
dest_files=["res://.godot/imported/kloppenheim_06_puresky_4k.hdr-026f0eabdfce50035e2f8417ce3e0060.bptc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
|
After Width: | Height: | Size: 144 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cyscxku0nw055"
|
||||||
|
path="res://.godot/imported/DASH.png-02cbc39ec03a0c79526d7dc333659c48.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://HUD/DASH.png"
|
||||||
|
dest_files=["res://.godot/imported/DASH.png-02cbc39ec03a0c79526d7dc333659c48.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
|
||||||
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bdnc3jw1ykydi"
|
||||||
|
path="res://.godot/imported/KICK.png-5482be80e8176ed7a0ae69a48ca1f5ed.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://HUD/KICK.png"
|
||||||
|
dest_files=["res://.godot/imported/KICK.png-5482be80e8176ed7a0ae69a48ca1f5ed.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
|
||||||
|
After Width: | Height: | Size: 204 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d25i88cixy4pn"
|
||||||
|
path="res://.godot/imported/ROLL.png-306dafcb4b6c13eded9a925aa6d2f62a.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://HUD/ROLL.png"
|
||||||
|
dest_files=["res://.godot/imported/ROLL.png-306dafcb4b6c13eded9a925aa6d2f62a.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
|
||||||
|
After Width: | Height: | Size: 80 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bxplqrp4mc5yh"
|
||||||
|
path="res://.godot/imported/SLAM.png-b5036a739251be1240a24080e28d7a8e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://HUD/SLAM.png"
|
||||||
|
dest_files=["res://.godot/imported/SLAM.png-b5036a739251be1240a24080e28d7a8e.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
[gd_scene format=3 uid="uid://c4df4qgpc3d5g"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="1_eicjn"]
|
||||||
|
[ext_resource type="Script" uid="uid://dycnoiadljbb1" path="res://Assets/matador_intro_menu.gd" id="2_5jr0i"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dio67r8ambe7h" path="res://Assets/arena_placeholder_v01.glb" id="3_1v6n4"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dxe72ig8gfbcg" path="res://Assets/arena model2026.glb" id="3_5jr0i"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://crtm0xtiql3r8" path="res://Player.tscn" id="4_eicjn"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://3syhacswepyj" path="res://HDRI/kloppenheim_06_puresky_4k.hdr" id="6_1v6n4"]
|
||||||
|
|
||||||
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_eicjn"]
|
||||||
|
|
||||||
|
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_w7j4y"]
|
||||||
|
panorama = ExtResource("6_1v6n4")
|
||||||
|
energy_multiplier = 5.6
|
||||||
|
|
||||||
|
[sub_resource type="Sky" id="Sky_3el7n"]
|
||||||
|
sky_material = SubResource("PanoramaSkyMaterial_w7j4y")
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Environment_5jr0i"]
|
||||||
|
background_mode = 2
|
||||||
|
background_energy_multiplier = 0.18
|
||||||
|
sky = SubResource("Sky_3el7n")
|
||||||
|
ambient_light_color = Color(0.31061712, 0.31061712, 0.3106171, 1)
|
||||||
|
ambient_light_energy = 16.0
|
||||||
|
ssao_enabled = true
|
||||||
|
ssao_radius = 1.17
|
||||||
|
ssao_intensity = 0.0
|
||||||
|
glow_intensity = 0.34
|
||||||
|
glow_strength = 0.29
|
||||||
|
glow_bloom = 0.1
|
||||||
|
fog_light_color = Color(2.211, 2.211, 2.211, 1)
|
||||||
|
fog_light_energy = 0.01
|
||||||
|
fog_density = 0.0399
|
||||||
|
volumetric_fog_enabled = true
|
||||||
|
volumetric_fog_density = 0.0005
|
||||||
|
volumetric_fog_emission = Color(1, 1, 1, 1)
|
||||||
|
volumetric_fog_emission_energy = 7.85
|
||||||
|
volumetric_fog_anisotropy = 0.9
|
||||||
|
volumetric_fog_length = 69.07
|
||||||
|
|
||||||
|
[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_5jr0i"]
|
||||||
|
dof_blur_far_enabled = true
|
||||||
|
dof_blur_far_distance = 26.01
|
||||||
|
dof_blur_near_enabled = true
|
||||||
|
dof_blur_near_distance = 2.6
|
||||||
|
dof_blur_near_transition = 5.0
|
||||||
|
|
||||||
|
[node name="IntroMenu" type="Node3D" unique_id=2130618263]
|
||||||
|
|
||||||
|
[node name="Matador" parent="." unique_id=166852612 instance=ExtResource("1_eicjn")]
|
||||||
|
transform = Transform3D(-0.56715536, 0, -0.8236109, 0, 1, 0, 0.8236109, 0, -0.56715536, 5.2114463, -0.08998108, 1.8927763)
|
||||||
|
script = ExtResource("2_5jr0i")
|
||||||
|
|
||||||
|
[node name="Skeleton3D" parent="Matador/Armature" parent_id_path=PackedInt32Array(166852612, 1269629432) index="0" unique_id=978366810]
|
||||||
|
bones/0/position = Vector3(0.056660548, 0.6424767, -0.0167365)
|
||||||
|
bones/0/rotation = Quaternion(0.013179442, -0.2660563, 0.073319815, 0.9610747)
|
||||||
|
bones/1/rotation = Quaternion(0.06234612, 0.049236443, 0.08183311, 0.99347484)
|
||||||
|
bones/2/rotation = Quaternion(-0.073883414, -0.049860783, -0.17668208, 0.9802238)
|
||||||
|
bones/3/rotation = Quaternion(0.1986702, -0.17794952, -0.71790075, 0.6430262)
|
||||||
|
bones/4/rotation = Quaternion(0.34491858, -0.4473084, -0.25076818, 0.7861691)
|
||||||
|
bones/5/rotation = Quaternion(0.2577085, 0.103201896, -0.6748433, 0.68375605)
|
||||||
|
bones/6/rotation = Quaternion(1.7417646e-08, -1.0305205e-07, -0.05870078, 0.99827564)
|
||||||
|
bones/7/rotation = Quaternion(0.24033272, 0.3103031, 0.5222128, 0.7571301)
|
||||||
|
bones/8/rotation = Quaternion(5.5530215e-08, 1.2911333e-07, 0.055464897, 0.99846065)
|
||||||
|
bones/9/rotation = Quaternion(-6.3051346e-09, -3.6333955e-07, 0.005667881, 0.999984)
|
||||||
|
bones/10/rotation = Quaternion(2.5839595e-08, 9.468444e-08, 0.058700778, 0.99827564)
|
||||||
|
bones/11/rotation = Quaternion(0.39202607, 0.47060946, 0.57993996, 0.53713304)
|
||||||
|
bones/12/rotation = Quaternion(0.8712108, 0.008231261, 0.48523128, 0.073990606)
|
||||||
|
bones/13/rotation = Quaternion(-0.056263044, 0.0027001738, 0.3080972, 0.9496859)
|
||||||
|
bones/14/rotation = Quaternion(0.016082464, 0.024660494, -0.86586916, 0.4994036)
|
||||||
|
bones/15/rotation = Quaternion(0.6067664, -0.3062479, -0.7163194, 0.15790276)
|
||||||
|
bones/16/rotation = Quaternion(-0.0731876, -0.0023153822, -0.47496143, 0.87695485)
|
||||||
|
bones/17/rotation = Quaternion(-0.11824294, -0.17776872, 0.80557114, 0.55269533)
|
||||||
|
bones/18/rotation = Quaternion(0.5305965, -0.28927496, 0.76720744, -0.21489605)
|
||||||
|
bones/20/position = Vector3(0.22012731, 0.38421977, 0.36399037)
|
||||||
|
bones/21/position = Vector3(-0.23671798, 0.052281797, -0.027087089)
|
||||||
|
bones/21/rotation = Quaternion(0.5078295, -0.4920459, -0.4920459, 0.50782955)
|
||||||
|
bones/22/position = Vector3(-0.39721552, 0.31719273, 0.3595722)
|
||||||
|
bones/23/position = Vector3(-0.9167187, 1.3206767, 0.42621902)
|
||||||
|
bones/23/rotation = Quaternion(0.9993525, -0.0021175682, 0.03502641, -0.00795782)
|
||||||
|
bones/24/rotation = Quaternion(0.08874816, -0.018719967, -0.04671622, 0.99478185)
|
||||||
|
bones/25/rotation = Quaternion(0.09634846, -0.0268489, -0.04050248, 0.9941608)
|
||||||
|
|
||||||
|
[node name="matador" parent="Matador/Armature/Skeleton3D" index="0" unique_id=256196413]
|
||||||
|
transform = Transform3D(0.8642424, 0, 0.50307554, 0, 1, 0, -0.50307554, 0, 0.8642424, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Muleta" parent="Matador/Armature/Skeleton3D" index="1" unique_id=988244930]
|
||||||
|
cast_shadow = 0
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="." unique_id=1531255459]
|
||||||
|
transform = Transform3D(0.42176014, 0.2362673, 0.8753835, -0.10687317, 0.97167695, -0.21076548, -0.900387, -0.0046625203, 0.4350652, 8.41945, 0.17132203, 2.0652685)
|
||||||
|
fov = 41.6
|
||||||
|
near = 0.001
|
||||||
|
|
||||||
|
[node name="arena model2026" parent="." unique_id=179544183 instance=ExtResource("3_5jr0i")]
|
||||||
|
transform = Transform3D(-0.83746713, 0, 0.54648757, 0, 0.99999994, 0, -0.54648757, 0, -0.83746713, 3.8733478, -0.6, -1.1080065)
|
||||||
|
|
||||||
|
[node name="arena_placeholder_v01" parent="." unique_id=1343590504 instance=ExtResource("3_1v6n4")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3344436, 0, -1.5401263)
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="StaticBody3D" type="StaticBody3D" parent="arena_placeholder_v01" unique_id=340133176]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="arena_placeholder_v01/StaticBody3D" unique_id=201484167]
|
||||||
|
transform = Transform3D(-33.265, 4.9526625e-07, -2.9081186e-06, 5.0228755e-06, 3.28, 0, -2.9081186e-06, 4.329755e-14, 33.265, 0, -3.3065948, 0)
|
||||||
|
shape = SubResource("CylinderShape3D_eicjn")
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=660229295]
|
||||||
|
transform = Transform3D(0.9491592, 0.31479403, -0.0014368258, -0.28033537, 0.84731936, 0.45106864, 0.14321098, -0.4277331, 0.89248836, 1.1162033, 7.3956842, -4.480528)
|
||||||
|
light_energy = 1.32
|
||||||
|
shadow_enabled = true
|
||||||
|
shadow_blur = 8.689
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D2" type="DirectionalLight3D" parent="." unique_id=498871251]
|
||||||
|
transform = Transform3D(0.835524, -0.5376153, 0.11344351, -0.48260456, -0.6193575, 0.6192651, -0.26266432, -0.572159, -0.7769435, 1.1162033, 7.3956842, -4.480528)
|
||||||
|
visible = false
|
||||||
|
light_energy = 0.415
|
||||||
|
light_indirect_energy = 16.0
|
||||||
|
light_volumetric_fog_energy = 1.11
|
||||||
|
shadow_blur = 10.0
|
||||||
|
|
||||||
|
[node name="Player" parent="." unique_id=165510044 instance=ExtResource("4_eicjn")]
|
||||||
|
transform = Transform3D(0.9563367, 0, 0.29226705, 0, 1, 0, -0.29226705, 0, 0.9563367, 3.0920272, 0.39692497, -4.302477)
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=114793752]
|
||||||
|
environment = SubResource("Environment_5jr0i")
|
||||||
|
camera_attributes = SubResource("CameraAttributesPractical_5jr0i")
|
||||||
|
|
||||||
|
[node name="SpotLight3D" type="SpotLight3D" parent="." unique_id=1428020323]
|
||||||
|
transform = Transform3D(0.94930077, -0.05846006, -0.30888584, 0.21308771, 0.8420559, 0.4955155, 0.23113126, -0.53621286, 0.81182146, -20.807917, 10.690277, 20.135067)
|
||||||
|
visible = false
|
||||||
|
light_energy = 16.0
|
||||||
|
light_volumetric_fog_energy = 16.0
|
||||||
|
shadow_enabled = true
|
||||||
|
spot_range = 84.316696
|
||||||
|
|
||||||
|
[editable path="Matador"]
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bmnmenu0intr1"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://46y10alai56q" path="res://main_menu.gd" id="1_menu"]
|
||||||
|
[ext_resource type="VideoStream" uid="uid://csxxlnq74fej4" path="res://Assets/intro_bg.ogv" id="2_video"]
|
||||||
|
[ext_resource type="Shader" path="res://menu_blur.gdshader" id="3_blur"]
|
||||||
|
|
||||||
|
[sub_resource type="ShaderMaterial" id="mat_blur"]
|
||||||
|
shader = ExtResource("3_blur")
|
||||||
|
shader_parameter/blur_radius = 3.0
|
||||||
|
shader_parameter/darken = 0.35
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="sb_normal"]
|
||||||
|
content_margin_left = 16.0
|
||||||
|
content_margin_top = 8.0
|
||||||
|
content_margin_right = 16.0
|
||||||
|
content_margin_bottom = 8.0
|
||||||
|
bg_color = Color(0, 0, 0, 0.4)
|
||||||
|
corner_radius_top_left = 8
|
||||||
|
corner_radius_top_right = 8
|
||||||
|
corner_radius_bottom_right = 8
|
||||||
|
corner_radius_bottom_left = 8
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="sb_hover"]
|
||||||
|
content_margin_left = 16.0
|
||||||
|
content_margin_top = 8.0
|
||||||
|
content_margin_right = 16.0
|
||||||
|
content_margin_bottom = 8.0
|
||||||
|
bg_color = Color(0, 0, 0, 0.65)
|
||||||
|
border_width_left = 2
|
||||||
|
border_width_top = 2
|
||||||
|
border_width_right = 2
|
||||||
|
border_width_bottom = 2
|
||||||
|
border_color = Color(1, 0.85, 0.2, 1)
|
||||||
|
corner_radius_top_left = 8
|
||||||
|
corner_radius_top_right = 8
|
||||||
|
corner_radius_bottom_right = 8
|
||||||
|
corner_radius_bottom_left = 8
|
||||||
|
|
||||||
|
[node name="MainMenu" type="Control" unique_id=371457725]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_menu")
|
||||||
|
|
||||||
|
[node name="VideoBlur" type="AspectRatioContainer" parent="." unique_id=374854263]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
ratio = 1.7778
|
||||||
|
stretch_mode = 3
|
||||||
|
|
||||||
|
[node name="BlurVideo" type="VideoStreamPlayer" parent="VideoBlur" unique_id=374854262]
|
||||||
|
material = SubResource("mat_blur")
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
mouse_filter = 2
|
||||||
|
stream = ExtResource("2_video")
|
||||||
|
expand = true
|
||||||
|
|
||||||
|
[node name="VideoAspect" type="AspectRatioContainer" parent="." unique_id=374854264]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
ratio = 1.7778
|
||||||
|
stretch_mode = 2
|
||||||
|
|
||||||
|
[node name="VideoStreamPlayer" type="VideoStreamPlayer" parent="VideoAspect" unique_id=374854265]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
mouse_filter = 2
|
||||||
|
stream = ExtResource("2_video")
|
||||||
|
expand = true
|
||||||
|
|
||||||
|
[node name="CapeLayer" type="Control" parent="VideoAspect" unique_id=374854266]
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="PlayButton" type="Button" parent="VideoAspect/CapeLayer" unique_id=743089847]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.453
|
||||||
|
anchor_top = 0.6513
|
||||||
|
anchor_right = 0.453
|
||||||
|
anchor_bottom = 0.6513
|
||||||
|
offset_left = -120.0
|
||||||
|
offset_top = -55.0
|
||||||
|
offset_right = 120.0
|
||||||
|
offset_bottom = 55.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 8
|
||||||
|
theme_override_font_sizes/font_size = 56
|
||||||
|
theme_override_styles/normal = SubResource("sb_normal")
|
||||||
|
theme_override_styles/pressed = SubResource("sb_hover")
|
||||||
|
theme_override_styles/hover = SubResource("sb_hover")
|
||||||
|
theme_override_styles/focus = SubResource("sb_hover")
|
||||||
|
text = "PLAY"
|
||||||
|
|
||||||
|
[node name="MainPanel" type="Control" parent="." unique_id=1263320918]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="OptionsButton" type="Button" parent="MainPanel" unique_id=673211179]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.11
|
||||||
|
anchor_top = 0.94
|
||||||
|
anchor_right = 0.11
|
||||||
|
anchor_bottom = 0.94
|
||||||
|
offset_left = -100.0
|
||||||
|
offset_top = -28.0
|
||||||
|
offset_right = 100.0
|
||||||
|
offset_bottom = 28.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 6
|
||||||
|
theme_override_font_sizes/font_size = 30
|
||||||
|
theme_override_styles/normal = SubResource("sb_normal")
|
||||||
|
theme_override_styles/pressed = SubResource("sb_hover")
|
||||||
|
theme_override_styles/hover = SubResource("sb_hover")
|
||||||
|
theme_override_styles/focus = SubResource("sb_hover")
|
||||||
|
text = "OPTIONS"
|
||||||
|
|
||||||
|
[node name="CreditsButton" type="Button" parent="MainPanel" unique_id=882339461]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.489
|
||||||
|
anchor_top = 0.951
|
||||||
|
anchor_right = 0.489
|
||||||
|
anchor_bottom = 0.951
|
||||||
|
offset_left = -100.0
|
||||||
|
offset_top = -28.0
|
||||||
|
offset_right = 100.0
|
||||||
|
offset_bottom = 28.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 6
|
||||||
|
theme_override_font_sizes/font_size = 30
|
||||||
|
theme_override_styles/normal = SubResource("sb_normal")
|
||||||
|
theme_override_styles/pressed = SubResource("sb_hover")
|
||||||
|
theme_override_styles/hover = SubResource("sb_hover")
|
||||||
|
theme_override_styles/focus = SubResource("sb_hover")
|
||||||
|
text = "CREDITS"
|
||||||
|
|
||||||
|
[node name="ExitButton" type="Button" parent="MainPanel" unique_id=1064586744]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = -1
|
||||||
|
anchor_left = 0.92
|
||||||
|
anchor_top = 0.951
|
||||||
|
anchor_right = 0.92
|
||||||
|
anchor_bottom = 0.951
|
||||||
|
offset_left = -100.0
|
||||||
|
offset_top = -28.0
|
||||||
|
offset_right = 100.0
|
||||||
|
offset_bottom = 28.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 6
|
||||||
|
theme_override_font_sizes/font_size = 30
|
||||||
|
theme_override_styles/normal = SubResource("sb_normal")
|
||||||
|
theme_override_styles/pressed = SubResource("sb_hover")
|
||||||
|
theme_override_styles/hover = SubResource("sb_hover")
|
||||||
|
theme_override_styles/focus = SubResource("sb_hover")
|
||||||
|
text = "EXIT"
|
||||||
|
|
||||||
|
[node name="OptionsPanel" type="CenterContainer" parent="." unique_id=593823446]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="Backdrop" type="ColorRect" parent="OptionsPanel" unique_id=1811982549]
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
color = Color(0, 0, 0, 0.55)
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="OptionsPanel" unique_id=2125052135]
|
||||||
|
custom_minimum_size = Vector2(560, 480)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="OptionsPanel/VBox" unique_id=853979265]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 8
|
||||||
|
theme_override_font_sizes/font_size = 44
|
||||||
|
text = "Controls"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Hint" type="Label" parent="OptionsPanel/VBox" unique_id=1809504670]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 5
|
||||||
|
theme_override_font_sizes/font_size = 20
|
||||||
|
text = "Click a key, then press a new key. Esc cancels."
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Scroll" type="ScrollContainer" parent="OptionsPanel/VBox" unique_id=683738956]
|
||||||
|
custom_minimum_size = Vector2(0, 340)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
horizontal_scroll_mode = 0
|
||||||
|
|
||||||
|
[node name="RebindList" type="VBoxContainer" parent="OptionsPanel/VBox/Scroll" unique_id=1261520279]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="BackButton" type="Button" parent="OptionsPanel/VBox" unique_id=1587524951]
|
||||||
|
custom_minimum_size = Vector2(0, 48)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
text = "Back"
|
||||||
|
|
||||||
|
[node name="CreditsPanel" type="CenterContainer" parent="." unique_id=175743902]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="Backdrop" type="ColorRect" parent="CreditsPanel" unique_id=1950253735]
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
color = Color(0, 0, 0, 0.55)
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="CreditsPanel" unique_id=901836808]
|
||||||
|
custom_minimum_size = Vector2(520, 360)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 20
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="CreditsPanel/VBox" unique_id=825593160]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 8
|
||||||
|
theme_override_font_sizes/font_size = 44
|
||||||
|
text = "Credits"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Names" type="RichTextLabel" parent="CreditsPanel/VBox" unique_id=1355198629]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_colors/default_color = Color(0.93, 0.88, 0.72, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/line_separation = 6
|
||||||
|
theme_override_constants/outline_size = 5
|
||||||
|
theme_override_font_sizes/normal_font_size = 26
|
||||||
|
theme_override_font_sizes/bold_font_size = 30
|
||||||
|
bbcode_enabled = true
|
||||||
|
text = "[center][font_size=32][color=#d9a833]Bullosseum[/color][/font_size]
|
||||||
|
|
||||||
|
[b][color=#d9a833]Siim Raud[/color][/b] [url=https://instagram.com/siimpressionist]@siimpressionist[/url]
|
||||||
|
[font_size=20]Artist[/font_size]
|
||||||
|
|
||||||
|
[b][color=#d9a833]Richard Aasa[/color][/b] [url=https://instagram.com/aasarichard]@aasarichard[/url]
|
||||||
|
[font_size=20]Programmer[/font_size]
|
||||||
|
|
||||||
|
[b][color=#d9a833]Kadi Rebane[/color][/b] [url=https://x.com/_barkseven]@_barkseven[/url]
|
||||||
|
[font_size=20]Arena asset[/font_size][/center]"
|
||||||
|
fit_content = true
|
||||||
|
scroll_active = false
|
||||||
|
|
||||||
|
[node name="BackButton" type="Button" parent="CreditsPanel/VBox" unique_id=1194791074]
|
||||||
|
custom_minimum_size = Vector2(0, 48)
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
text = "Back"
|
||||||
|
|
||||||
|
[node name="FadeRect" type="ColorRect" parent="." unique_id=15381366]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
color = Color(0, 0, 0, 0)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
extends CPUParticles3D
|
||||||
|
## One-shot blood spray. Add as a child of whatever bleeds, then call burst().
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
mat.vertex_color_use_as_albedo = true
|
||||||
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
|
|
||||||
|
var sphere := SphereMesh.new()
|
||||||
|
sphere.radius = 0.07
|
||||||
|
sphere.height = 0.14
|
||||||
|
sphere.radial_segments = 4
|
||||||
|
sphere.rings = 2
|
||||||
|
sphere.material = mat
|
||||||
|
|
||||||
|
var ramp := Gradient.new()
|
||||||
|
ramp.set_color(0, Color(1.0, 0.02, 0.02, 1.0))
|
||||||
|
ramp.set_color(1, Color(0.25, 0.0, 0.0, 0.0))
|
||||||
|
|
||||||
|
var scale_curve := Curve.new()
|
||||||
|
scale_curve.add_point(Vector2(0.0, 1.0))
|
||||||
|
scale_curve.add_point(Vector2(0.6, 0.5))
|
||||||
|
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||||
|
|
||||||
|
mesh = sphere
|
||||||
|
color_ramp = ramp
|
||||||
|
scale_amount_curve = scale_curve
|
||||||
|
one_shot = true
|
||||||
|
explosiveness = 0.9
|
||||||
|
amount = 25
|
||||||
|
lifetime = 1.1
|
||||||
|
randomness = 0.4
|
||||||
|
local_coords = false
|
||||||
|
spread = 80.0
|
||||||
|
gravity = Vector3(0.0, -7.0, 0.0)
|
||||||
|
initial_velocity_min = 4.0
|
||||||
|
initial_velocity_max = 11.0
|
||||||
|
scale_amount_min = 0.5
|
||||||
|
scale_amount_max = 1.6
|
||||||
|
emitting = false
|
||||||
|
|
||||||
|
|
||||||
|
# Fire a burst at a world position, sprayed along dir (biased upward).
|
||||||
|
func burst(at: Vector3, dir: Vector3) -> void:
|
||||||
|
global_position = at
|
||||||
|
direction = (dir * 0.6 + Vector3.UP * 0.4).normalized()
|
||||||
|
restart()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cnhmhom4w5o3i
|
||||||
@@ -15,6 +15,10 @@ var _aerial_pitch: float = 0.0
|
|||||||
var charge_pitch_target: float = 0.0
|
var charge_pitch_target: float = 0.0
|
||||||
var _charge_pitch_curr: float = 0.0
|
var _charge_pitch_curr: float = 0.0
|
||||||
|
|
||||||
|
# When true, another system (the roll's ball-tumble) owns the bull's full
|
||||||
|
# orientation — skip writing body wobble so we don't fight it.
|
||||||
|
var external_rotation: bool = false
|
||||||
|
|
||||||
# Tail — Verlet chain simulation
|
# Tail — Verlet chain simulation
|
||||||
const TAIL_BONES: Array = [
|
const TAIL_BONES: Array = [
|
||||||
&"tail_1", &"tail_2", &"tail_3", &"tail_4",
|
&"tail_1", &"tail_2", &"tail_3", &"tail_4",
|
||||||
@@ -94,18 +98,14 @@ func _setup_tail() -> void:
|
|||||||
_tail_parent_idx = _skeleton.get_bone_parent(_tail_idx[0])
|
_tail_parent_idx = _skeleton.get_bone_parent(_tail_idx[0])
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventKey and event.pressed and not event.echo:
|
|
||||||
if event.physical_keycode == KEY_QUOTELEFT:
|
|
||||||
_dbg_toggle()
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if not _skeleton:
|
if not _skeleton:
|
||||||
return
|
return
|
||||||
_wobble_body(delta)
|
_wobble_body(delta)
|
||||||
_update_tail(delta)
|
_update_tail(delta)
|
||||||
_apply_tail()
|
_apply_tail()
|
||||||
|
if DP.b("show_tail") != _dbg_on:
|
||||||
|
_dbg_toggle()
|
||||||
if _dbg_on:
|
if _dbg_on:
|
||||||
_dbg_update()
|
_dbg_update()
|
||||||
|
|
||||||
@@ -132,6 +132,8 @@ func _wobble_body(delta: float) -> void:
|
|||||||
_aerial_pitch = lerpf(_aerial_pitch, target_pitch, delta * DP.f("aerial_pitch_speed"))
|
_aerial_pitch = lerpf(_aerial_pitch, target_pitch, delta * DP.f("aerial_pitch_speed"))
|
||||||
|
|
||||||
_charge_pitch_curr = lerpf(_charge_pitch_curr, charge_pitch_target, delta * DP.f("charge_head_speed"))
|
_charge_pitch_curr = lerpf(_charge_pitch_curr, charge_pitch_target, delta * DP.f("charge_head_speed"))
|
||||||
|
if external_rotation:
|
||||||
|
return
|
||||||
_bull.rotation.x = _tilt_x + _aerial_pitch + _charge_pitch_curr
|
_bull.rotation.x = _tilt_x + _aerial_pitch + _charge_pitch_curr
|
||||||
_bull.rotation.z = _tilt_z
|
_bull.rotation.z = _tilt_z
|
||||||
|
|
||||||
@@ -243,12 +245,9 @@ func _dbg_toggle() -> void:
|
|||||||
n.queue_free()
|
n.queue_free()
|
||||||
_dbg_nodes.clear()
|
_dbg_nodes.clear()
|
||||||
_dbg_on = false
|
_dbg_on = false
|
||||||
print("TailDebug OFF")
|
|
||||||
else:
|
else:
|
||||||
_dbg_build()
|
_dbg_build()
|
||||||
_dbg_on = true
|
_dbg_on = true
|
||||||
var pname := _skeleton.get_bone_name(_tail_parent_idx) if _tail_parent_idx >= 0 else "none"
|
|
||||||
print("TailDebug ON parent_bone=", pname, " bull_pos=", _bull.global_position)
|
|
||||||
|
|
||||||
|
|
||||||
func _dbg_build() -> void:
|
func _dbg_build() -> void:
|
||||||
@@ -275,14 +274,6 @@ func _dbg_build() -> void:
|
|||||||
func _dbg_update() -> void:
|
func _dbg_update() -> void:
|
||||||
for i: int in _dbg_nodes.size():
|
for i: int in _dbg_nodes.size():
|
||||||
_dbg_nodes[i].global_position = _tail_world[i]
|
_dbg_nodes[i].global_position = _tail_world[i]
|
||||||
# Print positions every ~60 frames
|
|
||||||
if Engine.get_process_frames() % 60 == 0:
|
|
||||||
var parent_y := 0.0
|
|
||||||
if _tail_parent_idx >= 0:
|
|
||||||
parent_y = _skeleton.to_global(_skeleton.get_bone_global_pose(_tail_parent_idx).origin).y
|
|
||||||
print("anchor_Y=%.2f parent_bone_Y=%.2f player_Y=%.2f on_floor=%s" % [
|
|
||||||
_tail_world[0].y, parent_y, _player.global_position.y,
|
|
||||||
str(_player.is_on_floor())])
|
|
||||||
|
|
||||||
|
|
||||||
# ── Utility ───────────────────────────────────────────────────────────────────
|
# ── Utility ───────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,94 +1,119 @@
|
|||||||
extends Camera3D
|
extends Camera3D
|
||||||
|
|
||||||
@export var height: float = 28.0
|
@export var height: float = 25.0
|
||||||
@export var horizontal_distance: float = 28.0
|
@export var horizontal_distance: float = 25.0
|
||||||
@export var horizontal_angle_deg: float = 45.0
|
@export var horizontal_angle_deg: float = 45.0
|
||||||
|
|
||||||
@export var size_min: float = 5.0
|
# Perspective zoom (FOV)
|
||||||
@export var size_max: float = 30.0
|
@export var fov_min: float = 20.0
|
||||||
@export var default_size: float = 15.0
|
@export var fov_max: float = 75.0
|
||||||
|
@export var default_fov: float = 30.0
|
||||||
|
|
||||||
@export var focus_slice_thickness: float = 6.0
|
@export var focus_slice_thickness: float = 6.0
|
||||||
@export var blur_transition: float = 15.0
|
@export var blur_transition: float = 15.0
|
||||||
|
|
||||||
var _shake_amount: float = 0.0
|
var _shake_amount: float = 0.0
|
||||||
var _shake_decay: float = 0.0
|
var _shake_decay: float = 0.0
|
||||||
var _user_size: float = 0.0
|
var _user_fov: float = 0.0
|
||||||
var _charge_active: bool = false
|
|
||||||
|
# Trailing-camera state: _focus is the smoothed point the camera frames (it lags
|
||||||
|
# the bull), _drag_offset is the velocity-driven trail that slides it behind.
|
||||||
|
var _focus: Vector3 = Vector3.ZERO
|
||||||
|
var _drag_offset: Vector3 = Vector3.ZERO
|
||||||
|
var _focus_init: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
set_as_top_level(true)
|
set_as_top_level(true)
|
||||||
projection = Camera3D.PROJECTION_ORTHOGONAL
|
|
||||||
_user_size = default_size
|
projection = Camera3D.PROJECTION_PERSPECTIVE
|
||||||
size = default_size
|
fov = default_fov
|
||||||
|
_user_fov = default_fov
|
||||||
|
|
||||||
near = 0.1
|
near = 0.1
|
||||||
far = 150.0
|
far = 150.0
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event.is_action_pressed("zoom_in"):
|
if event.is_action_pressed("zoom_in"):
|
||||||
_user_size = clamp(_user_size / 1.2, size_min, size_max)
|
_user_fov = clamp(_user_fov / 1.1, fov_min, fov_max)
|
||||||
|
|
||||||
if event.is_action_pressed("zoom_out"):
|
if event.is_action_pressed("zoom_out"):
|
||||||
_user_size = clamp(_user_size * 1.2, size_min, size_max)
|
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
var zoom_target := _user_size * DP.f("charge_zoom_factor") if _charge_active else _user_size
|
fov = lerpf(fov, _user_fov, delta * 8.0)
|
||||||
size = lerpf(size, zoom_target, delta * 8.0)
|
|
||||||
|
|
||||||
var angle_rad := deg_to_rad(horizontal_angle_deg)
|
var angle_rad := deg_to_rad(horizontal_angle_deg)
|
||||||
|
|
||||||
var offset := Vector3(
|
var offset := Vector3(
|
||||||
sin(angle_rad) * horizontal_distance,
|
sin(angle_rad) * horizontal_distance,
|
||||||
height,
|
height,
|
||||||
cos(angle_rad) * horizontal_distance,
|
cos(angle_rad) * horizontal_distance
|
||||||
)
|
)
|
||||||
var target := get_parent() as Node3D
|
|
||||||
global_position = target.global_position + offset
|
|
||||||
look_at(target.global_position, Vector3.UP)
|
|
||||||
|
|
||||||
_update_ortho_tilt_shift_dof(target.global_position)
|
var target := get_parent() as Node3D
|
||||||
|
var focus := _drag_focus(target, delta)
|
||||||
|
|
||||||
|
global_position = focus + offset
|
||||||
|
look_at(focus, Vector3.UP)
|
||||||
|
|
||||||
|
_update_dof(focus)
|
||||||
|
|
||||||
if _shake_amount > 0.005:
|
if _shake_amount > 0.005:
|
||||||
global_position += Vector3(
|
global_position += Vector3(
|
||||||
randf_range(-_shake_amount, _shake_amount),
|
randf_range(-_shake_amount, _shake_amount),
|
||||||
randf_range(-_shake_amount * 0.25, _shake_amount * 0.25),
|
randf_range(-_shake_amount * 0.25, _shake_amount * 0.25),
|
||||||
randf_range(-_shake_amount, _shake_amount),
|
randf_range(-_shake_amount, _shake_amount)
|
||||||
)
|
)
|
||||||
|
|
||||||
_shake_amount = move_toward(_shake_amount, 0.0, _shake_decay * delta)
|
_shake_amount = move_toward(_shake_amount, 0.0, _shake_decay * delta)
|
||||||
|
|
||||||
|
|
||||||
|
# Smoothed follow point that drags behind the bull. The target trail sits opposite
|
||||||
|
# the bull's flat velocity (cam_drag metres per m/s); the offset and the focus both
|
||||||
|
# ease toward their targets so the camera lags and settles rather than snapping.
|
||||||
|
func _drag_focus(target: Node3D, delta: float) -> Vector3:
|
||||||
|
var pos := target.global_position
|
||||||
|
if not _focus_init:
|
||||||
|
_focus = pos
|
||||||
|
_focus_init = true
|
||||||
|
|
||||||
|
var vel := Vector3.ZERO
|
||||||
|
if target is CharacterBody3D:
|
||||||
|
vel = (target as CharacterBody3D).velocity
|
||||||
|
vel.y = 0.0
|
||||||
|
|
||||||
|
var drag_target := -vel * DP.f("cam_drag")
|
||||||
|
_drag_offset = _drag_offset.lerp(drag_target, clampf(DP.f("cam_drag_speed") * delta, 0.0, 1.0))
|
||||||
|
_focus = _focus.lerp(pos + _drag_offset, clampf(DP.f("cam_follow_lerp") * delta, 0.0, 1.0))
|
||||||
|
return _focus
|
||||||
|
|
||||||
|
|
||||||
func trigger_hit(strength: float) -> void:
|
func trigger_hit(strength: float) -> void:
|
||||||
|
if not Settings.screen_shake:
|
||||||
|
return
|
||||||
_shake_amount = strength * 0.5
|
_shake_amount = strength * 0.5
|
||||||
_shake_decay = _shake_amount / 0.4
|
_shake_decay = _shake_amount / 0.4
|
||||||
|
|
||||||
|
|
||||||
func set_charge_active(active: bool) -> void:
|
func _update_dof(target_pos: Vector3) -> void:
|
||||||
_charge_active = active
|
|
||||||
|
|
||||||
|
|
||||||
func _update_ortho_tilt_shift_dof(target_pos: Vector3) -> void:
|
|
||||||
var cam_attr: CameraAttributesPractical = null
|
var cam_attr: CameraAttributesPractical = null
|
||||||
|
|
||||||
if "attributes" in self and self.attributes is CameraAttributesPractical:
|
if attributes is CameraAttributesPractical:
|
||||||
cam_attr = self.attributes
|
cam_attr = attributes
|
||||||
elif "camera_attributes" in self and self.camera_attributes is CameraAttributesPractical:
|
|
||||||
cam_attr = self.camera_attributes
|
|
||||||
|
|
||||||
if cam_attr != null:
|
if cam_attr == null:
|
||||||
var to_target := target_pos - global_position
|
return
|
||||||
var forward_vector := -global_transform.basis.z
|
|
||||||
var ortho_depth := to_target.dot(forward_vector)
|
|
||||||
|
|
||||||
var zoom_factor := size / default_size
|
var distance := global_position.distance_to(target_pos)
|
||||||
var dynamic_slice := focus_slice_thickness * zoom_factor
|
|
||||||
var half_slice := dynamic_slice * 0.5
|
|
||||||
|
|
||||||
var base_transition := blur_transition * zoom_factor
|
cam_attr.dof_blur_near_distance = max(distance - focus_slice_thickness * 0.5, near + 0.1)
|
||||||
|
cam_attr.dof_blur_far_distance = distance + focus_slice_thickness * 0.5
|
||||||
|
|
||||||
|
cam_attr.dof_blur_near_transition = blur_transition
|
||||||
|
cam_attr.dof_blur_far_transition = blur_transition
|
||||||
|
|
||||||
cam_attr.dof_blur_far_distance = ortho_depth + half_slice
|
|
||||||
cam_attr.dof_blur_near_distance = maxf(near + 0.1, ortho_depth - half_slice)
|
|
||||||
cam_attr.dof_blur_far_transition = maxf(0.1, base_transition * 0.5)
|
|
||||||
cam_attr.dof_blur_near_transition = maxf(1.0, base_transition * 1.5)
|
|
||||||
cam_attr.dof_blur_far_enabled = false
|
|
||||||
cam_attr.dof_blur_near_enabled = false
|
cam_attr.dof_blur_near_enabled = false
|
||||||
|
cam_attr.dof_blur_far_enabled = false
|
||||||
|
|||||||
@@ -0,0 +1,651 @@
|
|||||||
|
extends Node
|
||||||
|
## Quake-style drop-down developer console (autoload "Console"). Toggle with the
|
||||||
|
## backtick/tilde key (`/~) — matched by physical position so it works on any
|
||||||
|
## keyboard layout and with or without Shift.
|
||||||
|
## Query a param: type its name. Set it: "name value".
|
||||||
|
## Tab = complete top match · ↑↓ = browse suggestions / history · Esc = close.
|
||||||
|
|
||||||
|
const CONSOLE_HEIGHT_RATIO := 0.25
|
||||||
|
const SLIDE_DURATION := 0.18
|
||||||
|
const MAX_SUGGESTIONS := 10
|
||||||
|
const LOG_MAX_LINES := 300
|
||||||
|
|
||||||
|
# Built-in commands (name → one-line help). Surfaced in autocomplete alongside
|
||||||
|
# param keys and printed by `help`. None collide with param key names.
|
||||||
|
const _COMMANDS := {
|
||||||
|
"save": "persist values to disk",
|
||||||
|
"reset": "reset all, or reset <name>",
|
||||||
|
"diff": "list params changed from default",
|
||||||
|
"toggle": "flip a bool param",
|
||||||
|
"list": "print params (optional section)",
|
||||||
|
"clear": "clear the log",
|
||||||
|
"help": "show this help",
|
||||||
|
}
|
||||||
|
|
||||||
|
var _canvas: CanvasLayer
|
||||||
|
var _panel: PanelContainer
|
||||||
|
var _log: RichTextLabel
|
||||||
|
var _suggest_panel: PanelContainer
|
||||||
|
var _suggest_box: VBoxContainer
|
||||||
|
var _suggest_labels: Array[Label] = []
|
||||||
|
var _input_field: LineEdit
|
||||||
|
|
||||||
|
var _tween: Tween
|
||||||
|
var _is_open: bool = false
|
||||||
|
var _prev_mouse: Input.MouseMode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
var _suggestions: Array[String] = []
|
||||||
|
var _selected_idx: int = -1
|
||||||
|
var _history: Array[String] = []
|
||||||
|
var _history_idx: int = -1 # -1 = editing the live line
|
||||||
|
var _pending_line: String = "" # live line stashed while browsing history
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Keep the console (and its slide tween/input) alive while the tree is paused.
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
_build_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
var mono := SystemFont.new()
|
||||||
|
mono.font_names = PackedStringArray([
|
||||||
|
"JetBrains Mono", "DejaVu Sans Mono", "Consolas", "Menlo", "monospace",
|
||||||
|
])
|
||||||
|
|
||||||
|
_canvas = CanvasLayer.new()
|
||||||
|
_canvas.layer = 128
|
||||||
|
add_child(_canvas)
|
||||||
|
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
_panel.anchor_left = 0.0
|
||||||
|
_panel.anchor_right = 1.0
|
||||||
|
_panel.anchor_top = 0.0
|
||||||
|
_panel.anchor_bottom = 0.0
|
||||||
|
_panel.offset_bottom = 500.0
|
||||||
|
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
_panel.visible = false
|
||||||
|
|
||||||
|
var bg := StyleBoxFlat.new()
|
||||||
|
bg.bg_color = Color(0.04, 0.06, 0.04, 0.94)
|
||||||
|
bg.border_width_bottom = 2
|
||||||
|
bg.border_color = Color(0.2, 0.8, 0.2, 1.0)
|
||||||
|
_panel.add_theme_stylebox_override("panel", bg)
|
||||||
|
_canvas.add_child(_panel)
|
||||||
|
|
||||||
|
var root_vbox := VBoxContainer.new()
|
||||||
|
root_vbox.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
root_vbox.size_flags_vertical = Control.SIZE_FILL
|
||||||
|
_panel.add_child(root_vbox)
|
||||||
|
|
||||||
|
# ── Log ───────────────────────────────────────────────────────────────────
|
||||||
|
_log = RichTextLabel.new()
|
||||||
|
_log.bbcode_enabled = true
|
||||||
|
_log.scroll_following = true
|
||||||
|
_log.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
_log.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
_log.add_theme_font_size_override("normal_font_size", 13)
|
||||||
|
_log.add_theme_constant_override("line_separation", 1)
|
||||||
|
# Monospace so the rpad-aligned suggestion/list columns actually line up.
|
||||||
|
_log.add_theme_font_override("normal_font", mono)
|
||||||
|
_log.add_theme_font_override("bold_font", mono)
|
||||||
|
_log.add_theme_font_override("mono_font", mono)
|
||||||
|
root_vbox.add_child(_log)
|
||||||
|
|
||||||
|
# ── Suggestion panel ──────────────────────────────────────────────────────
|
||||||
|
_suggest_panel = PanelContainer.new()
|
||||||
|
_suggest_panel.visible = false
|
||||||
|
_suggest_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
var sg_bg := StyleBoxFlat.new()
|
||||||
|
sg_bg.bg_color = Color(0.06, 0.10, 0.06, 0.97)
|
||||||
|
sg_bg.border_width_top = 1
|
||||||
|
sg_bg.border_color = Color(0.15, 0.55, 0.15, 0.8)
|
||||||
|
_suggest_panel.add_theme_stylebox_override("panel", sg_bg)
|
||||||
|
root_vbox.add_child(_suggest_panel)
|
||||||
|
|
||||||
|
_suggest_box = VBoxContainer.new()
|
||||||
|
_suggest_box.add_theme_constant_override("separation", 0)
|
||||||
|
_suggest_panel.add_child(_suggest_box)
|
||||||
|
|
||||||
|
for _i in MAX_SUGGESTIONS:
|
||||||
|
var lbl := Label.new()
|
||||||
|
lbl.visible = false
|
||||||
|
lbl.add_theme_font_size_override("font_size", 13)
|
||||||
|
lbl.add_theme_font_override("font", mono)
|
||||||
|
_suggest_box.add_child(lbl)
|
||||||
|
_suggest_labels.append(lbl)
|
||||||
|
|
||||||
|
root_vbox.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
# ── Input row ─────────────────────────────────────────────────────────────
|
||||||
|
var in_margin := MarginContainer.new()
|
||||||
|
in_margin.add_theme_constant_override("margin_left", 10)
|
||||||
|
in_margin.add_theme_constant_override("margin_right", 10)
|
||||||
|
in_margin.add_theme_constant_override("margin_top", 4)
|
||||||
|
in_margin.add_theme_constant_override("margin_bottom", 6)
|
||||||
|
root_vbox.add_child(in_margin)
|
||||||
|
|
||||||
|
var in_row := HBoxContainer.new()
|
||||||
|
in_row.add_theme_constant_override("separation", 6)
|
||||||
|
in_margin.add_child(in_row)
|
||||||
|
|
||||||
|
var prompt_lbl := Label.new()
|
||||||
|
prompt_lbl.text = ">"
|
||||||
|
prompt_lbl.modulate = Color(0.35, 1.0, 0.35)
|
||||||
|
prompt_lbl.add_theme_font_override("font", mono)
|
||||||
|
in_row.add_child(prompt_lbl)
|
||||||
|
|
||||||
|
_input_field = LineEdit.new()
|
||||||
|
_input_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_input_field.placeholder_text = "param · param value · toggle <bool> · save reset diff list clear help"
|
||||||
|
_input_field.clear_button_enabled = true
|
||||||
|
_input_field.add_theme_font_size_override("font_size", 14)
|
||||||
|
_input_field.add_theme_font_override("font", mono)
|
||||||
|
# Drop the default LineEdit chrome (the white box border) — the panel is the frame.
|
||||||
|
var flat := StyleBoxFlat.new()
|
||||||
|
flat.bg_color = Color(0, 0, 0, 0)
|
||||||
|
flat.set_border_width_all(0)
|
||||||
|
flat.set_content_margin_all(2)
|
||||||
|
_input_field.add_theme_stylebox_override("normal", flat)
|
||||||
|
_input_field.add_theme_stylebox_override("focus", flat)
|
||||||
|
in_row.add_child(_input_field)
|
||||||
|
|
||||||
|
_input_field.text_changed.connect(_on_text_changed)
|
||||||
|
_input_field.text_submitted.connect(_on_submitted)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Input handling ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not (event is InputEventKey and (event as InputEventKey).pressed
|
||||||
|
and not (event as InputEventKey).echo):
|
||||||
|
return
|
||||||
|
var ke := event as InputEventKey
|
||||||
|
|
||||||
|
# Backtick/tilde — toggle regardless of focus. Match by physical position so a
|
||||||
|
# non-US layout or a Shift-produced ~ both open the console.
|
||||||
|
if ke.physical_keycode == KEY_QUOTELEFT:
|
||||||
|
_toggle()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not _is_open:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ctrl+C clears the input line, Ctrl+L clears the log — familiar shell reflexes.
|
||||||
|
if ke.ctrl_pressed:
|
||||||
|
match ke.keycode:
|
||||||
|
KEY_C:
|
||||||
|
_clear_input()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
KEY_L:
|
||||||
|
_log.clear()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
|
||||||
|
match ke.keycode:
|
||||||
|
KEY_TAB:
|
||||||
|
_accept_completion()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
KEY_UP:
|
||||||
|
_arrow(-1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
KEY_DOWN:
|
||||||
|
_arrow(1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
KEY_ESCAPE:
|
||||||
|
_toggle()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
# Focus can be stolen a frame or more after we re-grab it (submit, arrows, the tree
|
||||||
|
# unpausing), so a one-shot deferred grab loses the race. Enforce it every frame
|
||||||
|
# while the console is open — cheap, and it never lets the command line go dark.
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _is_open and not _input_field.has_focus():
|
||||||
|
# Diagnostic: name whatever currently holds focus before we snatch it back, so
|
||||||
|
# a persistent thief (a HUD button, a menu control) shows up in the log instead
|
||||||
|
# of us guessing. Remove once the culprit is identified.
|
||||||
|
var owner: Control = get_viewport().gui_get_focus_owner()
|
||||||
|
var who: String = String(owner.name) if owner != null else "<nothing>"
|
||||||
|
_log_warn("focus stolen by %s — reclaiming" % who)
|
||||||
|
_input_field.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle() -> void:
|
||||||
|
_is_open = not _is_open
|
||||||
|
get_tree().paused = _is_open
|
||||||
|
var h := get_viewport().get_visible_rect().size.y * CONSOLE_HEIGHT_RATIO
|
||||||
|
_panel.offset_bottom = h
|
||||||
|
|
||||||
|
if _tween:
|
||||||
|
_tween.kill()
|
||||||
|
_tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUART)
|
||||||
|
|
||||||
|
if _is_open:
|
||||||
|
_prev_mouse = Input.mouse_mode
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
_panel.position.y = -h
|
||||||
|
_panel.visible = true
|
||||||
|
_tween.tween_property(_panel, "position:y", 0.0, SLIDE_DURATION)
|
||||||
|
_tween.tween_callback(_input_field.grab_focus)
|
||||||
|
else:
|
||||||
|
_tween.tween_property(_panel, "position:y", -h, SLIDE_DURATION)
|
||||||
|
_tween.tween_callback(func() -> void: _panel.visible = false)
|
||||||
|
Input.mouse_mode = _prev_mouse
|
||||||
|
_clear_suggestions()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Fuzzy autocomplete ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _on_text_changed(text: String) -> void:
|
||||||
|
_selected_idx = -1
|
||||||
|
_history_idx = -1
|
||||||
|
var query := text.get_slice(" ", 0).strip_edges()
|
||||||
|
if query.is_empty():
|
||||||
|
_clear_suggestions()
|
||||||
|
return
|
||||||
|
_refresh_suggestions(query)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_suggestions(query: String) -> void:
|
||||||
|
var ql := query.to_lower()
|
||||||
|
var scored : Array = []
|
||||||
|
# Commands rank above params on an equal score so a leading "re" surfaces the
|
||||||
|
# `reset` verb, not just params that happen to contain those letters.
|
||||||
|
for name: String in _COMMANDS:
|
||||||
|
var cs := _fuzzy_score(ql, name)
|
||||||
|
if cs > 0:
|
||||||
|
scored.append([cs + 1, name])
|
||||||
|
for key: String in DP.get_all():
|
||||||
|
var s := _fuzzy_score(ql, key.to_lower())
|
||||||
|
if s > 0:
|
||||||
|
scored.append([s, key])
|
||||||
|
scored.sort_custom(func(a: Array, b: Array) -> bool: return a[0] > b[0])
|
||||||
|
|
||||||
|
_suggestions.clear()
|
||||||
|
for i in mini(scored.size(), MAX_SUGGESTIONS):
|
||||||
|
_suggestions.append(scored[i][1])
|
||||||
|
|
||||||
|
for i in MAX_SUGGESTIONS:
|
||||||
|
if i < _suggestions.size():
|
||||||
|
_suggest_labels[i].text = _suggestion_text(_suggestions[i])
|
||||||
|
_suggest_labels[i].modulate = _suggestion_color(i)
|
||||||
|
_suggest_labels[i].visible = true
|
||||||
|
else:
|
||||||
|
_suggest_labels[i].visible = false
|
||||||
|
_suggest_panel.visible = not _suggestions.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func _is_command(name: String) -> bool:
|
||||||
|
return _COMMANDS.has(name)
|
||||||
|
|
||||||
|
|
||||||
|
func _suggestion_text(name: String) -> String:
|
||||||
|
if _is_command(name):
|
||||||
|
return " %s [cmd] %s" % [name.rpad(10), _COMMANDS[name]]
|
||||||
|
var meta: Dictionary = DP.get_all()[name]
|
||||||
|
return " %s %s [%s]" % [name.rpad(28), _fmt_val(name, meta).rpad(8), meta["section"]]
|
||||||
|
|
||||||
|
|
||||||
|
func _suggestion_color(idx: int) -> Color:
|
||||||
|
if idx == _selected_idx:
|
||||||
|
return Color(1.0, 1.0, 0.4) # selected — yellow
|
||||||
|
if idx == 0:
|
||||||
|
return Color(0.85, 1.0, 0.70) # top match — bright green
|
||||||
|
return Color(0.60, 0.78, 0.50) # rest — muted green
|
||||||
|
|
||||||
|
|
||||||
|
func _fuzzy_score(query: String, target: String) -> int:
|
||||||
|
var qi := 0
|
||||||
|
var score := 0
|
||||||
|
var last := -1
|
||||||
|
for ti in target.length():
|
||||||
|
if qi >= query.length():
|
||||||
|
break
|
||||||
|
if target[ti] == query[qi]:
|
||||||
|
score += 10 + (5 if ti == last + 1 else 0)
|
||||||
|
last = ti
|
||||||
|
qi += 1
|
||||||
|
if qi < query.length():
|
||||||
|
return 0 # not all query chars matched
|
||||||
|
if target.begins_with(query):
|
||||||
|
score += 50
|
||||||
|
if target == query:
|
||||||
|
score += 100
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
func _fmt_val(_key: String, meta: Dictionary) -> String:
|
||||||
|
if meta["type"] == TYPE_FLOAT:
|
||||||
|
return _fmt_num(meta["value"] as float)
|
||||||
|
if meta["type"] == TYPE_BOOL:
|
||||||
|
var bval: bool = meta["value"]
|
||||||
|
return "true" if bval else "false"
|
||||||
|
return str(meta["value"])
|
||||||
|
|
||||||
|
|
||||||
|
# Render a float as a plain decimal — trailing zeros trimmed but always at least one
|
||||||
|
# place, so it always reads as a float (4.0, 0.4, 0.005) never an int (4).
|
||||||
|
func _fmt_num(v: float) -> String:
|
||||||
|
var s := ("%.4f" % v).rstrip("0")
|
||||||
|
if s.ends_with("."):
|
||||||
|
s += "0"
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_suggestions() -> void:
|
||||||
|
_suggestions.clear()
|
||||||
|
_selected_idx = -1
|
||||||
|
for lbl in _suggest_labels:
|
||||||
|
lbl.visible = false
|
||||||
|
_suggest_panel.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
# ↑/↓ move through the suggestion list while it's open; once it's gone they recall
|
||||||
|
# command history instead. Same keys, context decides — no separate binding to learn.
|
||||||
|
func _arrow(dir: int) -> void:
|
||||||
|
if _suggest_panel.visible and not _suggestions.is_empty():
|
||||||
|
_move_selection(dir)
|
||||||
|
else:
|
||||||
|
_history_step(-dir)
|
||||||
|
|
||||||
|
|
||||||
|
# Highlight the next/prev suggestion and drop "<name> <value>" into the field with the
|
||||||
|
# value pre-selected, so you just type your number over it.
|
||||||
|
func _move_selection(dir: int) -> void:
|
||||||
|
if _selected_idx < 0:
|
||||||
|
_selected_idx = 0 if dir > 0 else _suggestions.size() - 1
|
||||||
|
else:
|
||||||
|
_selected_idx = wrapi(_selected_idx + dir, 0, _suggestions.size())
|
||||||
|
_recolor_suggestions()
|
||||||
|
_fill_selected()
|
||||||
|
|
||||||
|
|
||||||
|
# Tab commits the highlighted suggestion (top match if none yet) and closes the list.
|
||||||
|
func _accept_completion() -> void:
|
||||||
|
if _suggestions.is_empty():
|
||||||
|
return
|
||||||
|
_selected_idx = maxi(_selected_idx, 0)
|
||||||
|
_fill_selected()
|
||||||
|
_clear_suggestions()
|
||||||
|
|
||||||
|
|
||||||
|
func _fill_selected() -> void:
|
||||||
|
var key := _suggestions[_selected_idx]
|
||||||
|
# A command drops in as "verb " ready for its argument (or a bare Enter); a
|
||||||
|
# param drops in as "key value" with the value pre-selected to type over.
|
||||||
|
# Suggestions stay open either way so ↑↓ keeps cycling.
|
||||||
|
if _is_command(key):
|
||||||
|
_input_field.text = key + " "
|
||||||
|
_input_field.caret_column = _input_field.text.length()
|
||||||
|
return
|
||||||
|
var meta: Dictionary = DP.get_all()[key]
|
||||||
|
_input_field.text = "%s %s" % [key, _fmt_val(key, meta)]
|
||||||
|
_input_field.caret_column = _input_field.text.length()
|
||||||
|
_input_field.select(key.length() + 1)
|
||||||
|
|
||||||
|
|
||||||
|
func _recolor_suggestions() -> void:
|
||||||
|
for i in MAX_SUGGESTIONS:
|
||||||
|
if _suggest_labels[i].visible:
|
||||||
|
_suggest_labels[i].modulate = _suggestion_color(i)
|
||||||
|
|
||||||
|
|
||||||
|
# ↑ recalls older commands, ↓ newer ones; index -1 is the live (in-progress) line,
|
||||||
|
# which is stashed on the first ↑ and restored when you step back down to it.
|
||||||
|
func _history_step(dir: int) -> void:
|
||||||
|
if _history.is_empty():
|
||||||
|
return
|
||||||
|
var target := _history_idx + dir
|
||||||
|
if target < -1 or target >= _history.size():
|
||||||
|
return
|
||||||
|
if _history_idx == -1 and dir > 0:
|
||||||
|
_pending_line = _input_field.text
|
||||||
|
_history_idx = target
|
||||||
|
_set_input(_pending_line if _history_idx == -1 else _history[_history_idx])
|
||||||
|
|
||||||
|
|
||||||
|
# Wipe the live line (Ctrl+C) without submitting — also drops any suggestion popup
|
||||||
|
# and resets history browsing so the next ↑ starts fresh.
|
||||||
|
func _clear_input() -> void:
|
||||||
|
_input_field.clear()
|
||||||
|
_history_idx = -1
|
||||||
|
_pending_line = ""
|
||||||
|
_clear_suggestions()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_input(text: String) -> void:
|
||||||
|
_input_field.text = text
|
||||||
|
_input_field.caret_column = text.length()
|
||||||
|
_clear_suggestions()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Command execution ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _on_submitted(raw: String) -> void:
|
||||||
|
raw = raw.strip_edges()
|
||||||
|
if raw.is_empty():
|
||||||
|
return
|
||||||
|
if _history.is_empty() or _history[0] != raw:
|
||||||
|
_history.push_front(raw)
|
||||||
|
_history_idx = -1
|
||||||
|
_pending_line = ""
|
||||||
|
_input_field.clear()
|
||||||
|
_clear_suggestions()
|
||||||
|
_execute(raw)
|
||||||
|
|
||||||
|
|
||||||
|
func _execute(cmd: String) -> void:
|
||||||
|
_log_raw("[color=#3a5a3a]> %s[/color]" % _esc(cmd))
|
||||||
|
var parts := cmd.split(" ", false)
|
||||||
|
if parts.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
if _run_command(parts):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Not a command — treat as a param key, with fuzzy fall-back for partial names.
|
||||||
|
var key := _resolve_key(parts[0])
|
||||||
|
if key.is_empty():
|
||||||
|
return
|
||||||
|
var meta: Dictionary = DP.get_all()[key]
|
||||||
|
if parts.size() == 1:
|
||||||
|
_read_param(key, meta)
|
||||||
|
else:
|
||||||
|
_set_param(key, meta, parts[1])
|
||||||
|
|
||||||
|
|
||||||
|
# Run a built-in command; returns false if parts[0] isn't one (caller falls through
|
||||||
|
# to param handling). Kept separate from _execute to keep each dispatch shallow.
|
||||||
|
func _run_command(parts: PackedStringArray) -> bool:
|
||||||
|
match parts[0].to_lower():
|
||||||
|
"save":
|
||||||
|
DP.save()
|
||||||
|
_log_ok("Saved.")
|
||||||
|
"reset":
|
||||||
|
_cmd_reset(parts)
|
||||||
|
"toggle":
|
||||||
|
_cmd_toggle(parts)
|
||||||
|
"diff":
|
||||||
|
_cmd_diff()
|
||||||
|
"list":
|
||||||
|
_cmd_list(parts)
|
||||||
|
"clear":
|
||||||
|
_log.clear()
|
||||||
|
"help":
|
||||||
|
_cmd_help()
|
||||||
|
_:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
# Resolve a user-typed token to a param key: exact match, else unique fuzzy match.
|
||||||
|
# Logs an ambiguity / unknown warning (and an arrow on a fuzzy hit) and returns
|
||||||
|
# "" when it can't narrow to a single key.
|
||||||
|
func _resolve_key(raw: String) -> String:
|
||||||
|
if DP.get_all().has(raw):
|
||||||
|
return raw
|
||||||
|
var matches := _fuzzy_find_all(raw)
|
||||||
|
if matches.size() == 1:
|
||||||
|
_log_raw("[color=#8888ff]→ %s[/color]" % matches[0])
|
||||||
|
return matches[0]
|
||||||
|
if matches.size() > 1:
|
||||||
|
_log_warn("Ambiguous '%s': %s" % [_esc(raw), ", ".join(matches.slice(0, 6))])
|
||||||
|
else:
|
||||||
|
_log_warn("Unknown: %s" % _esc(raw))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_reset(parts: PackedStringArray) -> void:
|
||||||
|
if parts.size() == 1:
|
||||||
|
DP.reset_all()
|
||||||
|
_log_ok("All params reset to defaults.")
|
||||||
|
return
|
||||||
|
var key := _resolve_key(parts[1])
|
||||||
|
if key.is_empty():
|
||||||
|
return
|
||||||
|
DP.reset(key)
|
||||||
|
_log_ok("[b]%s[/b] reset to [color=#ffe080]%s[/color]" % [key, _fmt_val(key, DP.get_all()[key])])
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_toggle(parts: PackedStringArray) -> void:
|
||||||
|
if parts.size() < 2:
|
||||||
|
_log_warn("Usage: toggle <bool param>")
|
||||||
|
return
|
||||||
|
var key := _resolve_key(parts[1])
|
||||||
|
if key.is_empty():
|
||||||
|
return
|
||||||
|
var meta: Dictionary = DP.get_all()[key]
|
||||||
|
if meta["type"] != TYPE_BOOL:
|
||||||
|
_log_warn("%s is not a bool — use '%s <value>'" % [key, key])
|
||||||
|
return
|
||||||
|
var v := not (meta["value"] as bool)
|
||||||
|
DP.set_value(key, v)
|
||||||
|
_log_ok("[b]%s[/b] ← [color=#ffe080]%s[/color]" % [key, "true" if v else "false"])
|
||||||
|
|
||||||
|
|
||||||
|
# List only params that currently differ from their registered default, grouped by
|
||||||
|
# section — the "what have I actually changed?" view before a save.
|
||||||
|
func _cmd_diff() -> void:
|
||||||
|
var all := DP.get_all()
|
||||||
|
var sections: Dictionary = {}
|
||||||
|
for key: String in all:
|
||||||
|
if not DP.is_modified(key):
|
||||||
|
continue
|
||||||
|
var sec: String = all[key]["section"]
|
||||||
|
if not sections.has(sec):
|
||||||
|
sections[sec] = []
|
||||||
|
sections[sec].append(key)
|
||||||
|
if sections.is_empty():
|
||||||
|
_log_ok("All params at defaults.")
|
||||||
|
return
|
||||||
|
for sec: String in sections:
|
||||||
|
_log_raw("[color=#6699ff][b]%s[/b][/color]" % sec)
|
||||||
|
for key: String in sections[sec]:
|
||||||
|
var meta: Dictionary = all[key]
|
||||||
|
_log_raw(" [color=#777777]%s[/color] = [color=#ffe080]%s[/color] [color=#484848](def %s)[/color]" % [
|
||||||
|
key, _fmt_val(key, meta), _fmt_default(meta)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_help() -> void:
|
||||||
|
_log_raw("[color=#888888] <name>[/color] — read current value + range")
|
||||||
|
_log_raw("[color=#888888] <name> <value>[/color] — set it (number, or true/false)")
|
||||||
|
for name: String in _COMMANDS:
|
||||||
|
_log_raw("[color=#888888] %s[/color] — %s" % [name.rpad(20), _COMMANDS[name]])
|
||||||
|
_log_raw(" [color=#555555]↑↓ pick suggestion · Tab complete · ↑↓ history when empty · ` close[/color]")
|
||||||
|
|
||||||
|
|
||||||
|
func _fmt_default(meta: Dictionary) -> String:
|
||||||
|
if meta["type"] == TYPE_FLOAT:
|
||||||
|
return _fmt_num(meta["default"] as float)
|
||||||
|
return "true" if (meta["default"] as bool) else "false"
|
||||||
|
|
||||||
|
|
||||||
|
func _read_param(key: String, meta: Dictionary) -> void:
|
||||||
|
if meta["type"] == TYPE_FLOAT:
|
||||||
|
_log_raw("[b]%s[/b] = [color=#ffe080]%s[/color] [color=#484848](%s … %s)[/color]" % [
|
||||||
|
key, _fmt_val(key, meta),
|
||||||
|
_fmt_num(meta["min"] as float), _fmt_num(meta["max"] as float),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
_log_raw("[b]%s[/b] = [color=#ffe080]%s[/color] [color=#484848](true / false)[/color]" % [
|
||||||
|
key, _fmt_val(key, meta),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
func _set_param(key: String, meta: Dictionary, raw: String) -> void:
|
||||||
|
if meta["type"] == TYPE_FLOAT:
|
||||||
|
var lo := meta["min"] as float
|
||||||
|
var hi := meta["max"] as float
|
||||||
|
if not raw.is_valid_float() or not is_finite(raw.to_float()):
|
||||||
|
_log_warn("%s wants a number %s … %s" % [key, _fmt_num(lo), _fmt_num(hi)])
|
||||||
|
return
|
||||||
|
var v := snappedf(clampf(raw.to_float(), lo, hi), meta["step"] as float)
|
||||||
|
DP.set_value(key, v)
|
||||||
|
_log_ok("[b]%s[/b] ← [color=#ffe080]%s[/color]" % [key, _fmt_num(v)])
|
||||||
|
elif meta["type"] == TYPE_BOOL:
|
||||||
|
match raw.to_lower():
|
||||||
|
"1", "true", "yes", "on":
|
||||||
|
DP.set_value(key, true)
|
||||||
|
_log_ok("[b]%s[/b] ← [color=#ffe080]true[/color]" % key)
|
||||||
|
"0", "false", "no", "off":
|
||||||
|
DP.set_value(key, false)
|
||||||
|
_log_ok("[b]%s[/b] ← [color=#ffe080]false[/color]" % key)
|
||||||
|
_:
|
||||||
|
_log_warn("%s wants true or false" % key)
|
||||||
|
else:
|
||||||
|
_log_warn("Cannot set param of type %d" % meta["type"])
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_list(parts: PackedStringArray) -> void:
|
||||||
|
var filter := parts[1].to_lower() if parts.size() > 1 else ""
|
||||||
|
var all := DP.get_all()
|
||||||
|
var sections: Dictionary = {}
|
||||||
|
for key: String in all:
|
||||||
|
var sec: String = all[key]["section"]
|
||||||
|
if filter.length() > 0:
|
||||||
|
if not sec.to_lower().begins_with(filter) and not key.to_lower().contains(filter):
|
||||||
|
continue
|
||||||
|
if not sections.has(sec):
|
||||||
|
sections[sec] = []
|
||||||
|
sections[sec].append(key)
|
||||||
|
for sec: String in sections:
|
||||||
|
_log_raw("[color=#6699ff][b]%s[/b][/color]" % sec)
|
||||||
|
for key: String in sections[sec]:
|
||||||
|
_log_raw(" [color=#777777]%s[/color] = [color=#ffe080]%s[/color]" % [
|
||||||
|
key, _fmt_val(key, all[key])
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
func _fuzzy_find_all(query: String) -> Array[String]:
|
||||||
|
var result: Array[String] = []
|
||||||
|
var ql := query.to_lower()
|
||||||
|
for key: String in DP.get_all():
|
||||||
|
if _fuzzy_score(ql, key.to_lower()) > 0:
|
||||||
|
result.append(key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
## Neutralise BBCode in user-supplied text before it reaches the RichTextLabel,
|
||||||
|
## so malformed or hostile tags (e.g. [img]) render literally instead of running.
|
||||||
|
func _esc(text: String) -> String:
|
||||||
|
return text.replace("[", "[lb]")
|
||||||
|
|
||||||
|
|
||||||
|
func _log_raw(text: String) -> void:
|
||||||
|
if _log.get_line_count() > LOG_MAX_LINES:
|
||||||
|
_log.clear()
|
||||||
|
_log.append_text(text + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
func _log_ok(text: String) -> void:
|
||||||
|
_log_raw("[color=#66ee88]%s[/color]" % text)
|
||||||
|
|
||||||
|
|
||||||
|
func _log_warn(text: String) -> void:
|
||||||
|
_log_raw("[color=#ff7744]%s[/color]" % text)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
## Global keybinding manager (autoload `Controls`).
|
||||||
|
## Loads saved keyboard bindings on startup and applies them to InputMap so
|
||||||
|
## rebinds made in the intro menu persist into the game scene. Only the
|
||||||
|
## keyboard event of each action is overridden; joypad/mouse events are kept.
|
||||||
|
|
||||||
|
const SAVE_PATH := "user://controls.cfg"
|
||||||
|
const SECTION := "keybinds"
|
||||||
|
|
||||||
|
## Actions the options menu lets the player rebind (keyboard only).
|
||||||
|
const REBINDABLE_ACTIONS: PackedStringArray = [
|
||||||
|
&"move_forward",
|
||||||
|
&"move_back",
|
||||||
|
&"move_left",
|
||||||
|
&"move_right",
|
||||||
|
&"ability_kick",
|
||||||
|
&"ability_dash",
|
||||||
|
&"ability_slam",
|
||||||
|
&"ability_roll",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
load_saved()
|
||||||
|
|
||||||
|
|
||||||
|
## Whether to drive the game with the on-screen touch UI (joystick + buttons)
|
||||||
|
## instead of keyboard hints. DisplayServer.is_touchscreen_available() can't be
|
||||||
|
## used here: `emulate_touch_from_mouse` (on for desktop testing) makes it report
|
||||||
|
## true on every desktop. Gate on the actual runtime platform instead — real
|
||||||
|
## mobile/native and mobile web browsers — plus the DP force flag for testing.
|
||||||
|
func use_touch_ui() -> bool:
|
||||||
|
if DP.b("force_touch_controls"):
|
||||||
|
return true
|
||||||
|
return (
|
||||||
|
OS.has_feature("mobile")
|
||||||
|
or OS.has_feature("web_android")
|
||||||
|
or OS.has_feature("web_ios")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the first keyboard event bound to an action, or null.
|
||||||
|
func get_key_event(action: StringName) -> InputEventKey:
|
||||||
|
for event: InputEvent in InputMap.action_get_events(action):
|
||||||
|
if event is InputEventKey:
|
||||||
|
return event
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## Human-readable label for the action's current key (e.g. "W", "Space").
|
||||||
|
func get_key_label(action: StringName) -> String:
|
||||||
|
var event := get_key_event(action)
|
||||||
|
if event == null:
|
||||||
|
return "—"
|
||||||
|
return event.as_text_physical_keycode()
|
||||||
|
|
||||||
|
|
||||||
|
## Replaces the primary keyboard event of `action` with `event`, keeping
|
||||||
|
## every other event (e.g. arrow-key alternates, joypad) in its place.
|
||||||
|
func rebind(action: StringName, event: InputEventKey) -> void:
|
||||||
|
rebind_no_save(action, event)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func save() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
for action: StringName in REBINDABLE_ACTIONS:
|
||||||
|
var event := get_key_event(action)
|
||||||
|
if event != null:
|
||||||
|
cfg.set_value(SECTION, String(action), event.physical_keycode)
|
||||||
|
cfg.save(SAVE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
func load_saved() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(SAVE_PATH) != OK:
|
||||||
|
return
|
||||||
|
for action: StringName in REBINDABLE_ACTIONS:
|
||||||
|
var key := String(action)
|
||||||
|
if not cfg.has_section_key(SECTION, key):
|
||||||
|
continue
|
||||||
|
var keycode: int = cfg.get_value(SECTION, key)
|
||||||
|
var event := InputEventKey.new()
|
||||||
|
event.physical_keycode = keycode as Key
|
||||||
|
rebind_no_save(action, event)
|
||||||
|
|
||||||
|
|
||||||
|
## Rebuilds the action's event list, swapping the first keyboard event for
|
||||||
|
## `event` in place so ordering (and thus the "primary" key) is preserved.
|
||||||
|
func rebind_no_save(action: StringName, event: InputEventKey) -> void:
|
||||||
|
var events := InputMap.action_get_events(action)
|
||||||
|
InputMap.action_erase_events(action)
|
||||||
|
var replaced := false
|
||||||
|
for existing: InputEvent in events:
|
||||||
|
if not replaced and existing is InputEventKey:
|
||||||
|
InputMap.action_add_event(action, event)
|
||||||
|
replaced = true
|
||||||
|
else:
|
||||||
|
InputMap.action_add_event(action, existing)
|
||||||
|
if not replaced:
|
||||||
|
InputMap.action_add_event(action, event)
|
||||||
|
|
||||||
|
|
||||||
|
func reset_defaults() -> void:
|
||||||
|
InputMap.load_from_project_settings()
|
||||||
|
if FileAccess.file_exists(SAVE_PATH):
|
||||||
|
DirAccess.remove_absolute(SAVE_PATH)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dvxx75l5re2mf
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
shader_type spatial;
|
||||||
|
// Flat crowd sprite. Each instance is oriented at build time to face the arena centre
|
||||||
|
// (see crowd_billboards.gd) — NOT the camera — so spectators on different segments of
|
||||||
|
// the ring face inward toward the action, like a real stand. Unshaded (the sheet was
|
||||||
|
// baked with lighting), alpha-tested (no transparency sorting across hundreds of
|
||||||
|
// quads), nearest-filtered for a crisp PS1 look that matches the ps1_filter effect.
|
||||||
|
render_mode unshaded, cull_disabled, shadows_disabled, depth_draw_opaque;
|
||||||
|
|
||||||
|
uniform sampler2D sheet : source_color, filter_nearest;
|
||||||
|
uniform int hframes = 8;
|
||||||
|
uniform float fps = 10.0;
|
||||||
|
uniform float alpha_clip = 0.4;
|
||||||
|
|
||||||
|
varying flat float v_frame;
|
||||||
|
varying flat vec3 v_tint;
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
// Per-instance clap phase (0..1) and tint are packed into the MultiMesh custom data.
|
||||||
|
// The instance transform already carries the inward facing, so no billboard maths.
|
||||||
|
float phase = INSTANCE_CUSTOM.x;
|
||||||
|
v_tint = INSTANCE_CUSTOM.yzw;
|
||||||
|
v_frame = floor(mod(TIME * fps + phase * float(hframes), float(hframes)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
// Sheet is two rows: top = front, bottom = back. Show the back when the camera is
|
||||||
|
// behind the spectator (they're facing into the arena, away from us).
|
||||||
|
float u = (UV.x + v_frame) / float(hframes);
|
||||||
|
float v = UV.y * 0.5 + (FRONT_FACING ? 0.0 : 0.5);
|
||||||
|
vec4 c = texture(sheet, vec2(u, v));
|
||||||
|
if (c.a < alpha_clip) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
ALBEDO = c.rgb * v_tint;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://hkdivctavovb
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
extends MultiMeshInstance3D
|
||||||
|
## A whole stand of cheering spectators rendered as camera-facing billboards in a
|
||||||
|
## single draw call — the performant replacement for dozens of skinned crowd skeletons.
|
||||||
|
## The sprite sheet is baked from the existing crowd figure by tools/bake_crowd_sheet.gd.
|
||||||
|
##
|
||||||
|
## Placement mirrors an existing crowd node (default: the sibling PublikNode) so the
|
||||||
|
## billboards land exactly where the crowd was authored; if that node is missing it
|
||||||
|
## falls back to a generated tribune so the scene still populates.
|
||||||
|
|
||||||
|
const SHEET_PATH := "res://Assets/crowd_clap_sheet.png"
|
||||||
|
const SHADER_PATH := "res://crowd_billboard.gdshader"
|
||||||
|
|
||||||
|
## Node whose descendant figures (names starting with `figure_prefix`) donate their
|
||||||
|
## world positions. Empty = use the generated fallback stand.
|
||||||
|
@export var source_path: NodePath = ^"../PublikNode"
|
||||||
|
@export var figure_prefix: String = "mees"
|
||||||
|
## World height of a spectator billboard, in metres. Sheet aspect sets the width.
|
||||||
|
@export var figure_height: float = 2.0
|
||||||
|
@export var frames: int = 8
|
||||||
|
@export var clap_fps: float = 10.0
|
||||||
|
## Subtle per-spectator brightness spread so the crowd doesn't read as clones.
|
||||||
|
@export_range(0.0, 0.5) var tint_variation: float = 0.18
|
||||||
|
|
||||||
|
# Fallback tribune (used only when source_path resolves to nothing) — concentric arcs
|
||||||
|
# of seats ringing the arena.
|
||||||
|
@export_group("Fallback stand")
|
||||||
|
@export var fallback_count: int = 120
|
||||||
|
@export var fallback_inner_radius: float = 26.0
|
||||||
|
@export var fallback_rows: int = 6
|
||||||
|
@export var fallback_row_rise: float = 1.6
|
||||||
|
@export var fallback_row_step: float = 2.2
|
||||||
|
@export var fallback_seat_step: float = 2.4
|
||||||
|
|
||||||
|
var _rng := RandomNumberGenerator.new()
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_rng.seed = hash("bullosseum-crowd") # deterministic layout/phases across runs
|
||||||
|
_build()
|
||||||
|
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
var sheet := load(SHEET_PATH) as Texture2D
|
||||||
|
if sheet == null:
|
||||||
|
push_warning("crowd_billboards: missing sheet %s — run tools/bake_crowd_sheet.gd" % SHEET_PATH)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sheet holds `frames` columns and 2 rows (front / back), so a cell is half-height.
|
||||||
|
var aspect := (float(sheet.get_width()) / float(frames)) / (float(sheet.get_height()) / 2.0)
|
||||||
|
var quad := QuadMesh.new()
|
||||||
|
quad.size = Vector2(figure_height * aspect, figure_height)
|
||||||
|
quad.center_offset = Vector3(0.0, figure_height * 0.5, 0.0) # pivot at the feet
|
||||||
|
|
||||||
|
var mat := ShaderMaterial.new()
|
||||||
|
mat.shader = load(SHADER_PATH) as Shader
|
||||||
|
mat.set_shader_parameter("sheet", sheet)
|
||||||
|
mat.set_shader_parameter("hframes", frames)
|
||||||
|
mat.set_shader_parameter("fps", clap_fps)
|
||||||
|
material_override = mat
|
||||||
|
|
||||||
|
var seats := _seat_positions()
|
||||||
|
var center := _arena_center(seats)
|
||||||
|
|
||||||
|
var mm := MultiMesh.new()
|
||||||
|
mm.transform_format = MultiMesh.TRANSFORM_3D
|
||||||
|
mm.use_custom_data = true
|
||||||
|
mm.mesh = quad
|
||||||
|
mm.instance_count = seats.size()
|
||||||
|
|
||||||
|
for i in seats.size():
|
||||||
|
var s := 1.0 + _rng.randf_range(-0.06, 0.06) # slight height variety
|
||||||
|
mm.set_instance_transform(i, Transform3D(_facing_basis(seats[i], center, s), seats[i]))
|
||||||
|
var b := 1.0 + _rng.randf_range(-tint_variation, tint_variation)
|
||||||
|
mm.set_instance_custom_data(i, Color(_rng.randf(), b, b * 0.99, b * 0.98))
|
||||||
|
multimesh = mm
|
||||||
|
|
||||||
|
|
||||||
|
# Orientation for a seat: the sprite's forward (+Z, the quad's face) points horizontally
|
||||||
|
# inward at the arena centre, so each ring segment faces the action instead of the
|
||||||
|
# camera. Upright (world +Y), uniformly scaled by `s`.
|
||||||
|
func _facing_basis(seat: Vector3, center: Vector3, s: float) -> Basis:
|
||||||
|
var look := center - seat
|
||||||
|
look.y = 0.0
|
||||||
|
look = look.normalized() if look.length() > 0.001 else Vector3(0.0, 0.0, -1.0)
|
||||||
|
var up := Vector3.UP
|
||||||
|
var right := up.cross(look).normalized()
|
||||||
|
var basis := Basis()
|
||||||
|
basis.x = right
|
||||||
|
basis.y = up
|
||||||
|
basis.z = look
|
||||||
|
return basis.scaled(Vector3.ONE * s)
|
||||||
|
|
||||||
|
|
||||||
|
# Horizontal centre the crowd faces — the mean of the seat positions (the ring/stand
|
||||||
|
# centroid), which for an arena crowd is the middle of the pitch.
|
||||||
|
func _arena_center(seats: PackedVector3Array) -> Vector3:
|
||||||
|
if seats.is_empty():
|
||||||
|
return global_position
|
||||||
|
var sum := Vector3.ZERO
|
||||||
|
for p in seats:
|
||||||
|
sum += p
|
||||||
|
return sum / float(seats.size())
|
||||||
|
|
||||||
|
|
||||||
|
# Global feet-positions for every spectator: harvested from the source crowd if present,
|
||||||
|
# otherwise the generated fallback tribune.
|
||||||
|
func _seat_positions() -> PackedVector3Array:
|
||||||
|
var out := PackedVector3Array()
|
||||||
|
var src := get_node_or_null(source_path)
|
||||||
|
if src != null:
|
||||||
|
_collect_figures(src, out)
|
||||||
|
if out.is_empty():
|
||||||
|
_fallback_stand(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_figures(node: Node, out: PackedVector3Array) -> void:
|
||||||
|
if node is Node3D and node != self and (node as Node3D).name.begins_with(figure_prefix):
|
||||||
|
out.append((node as Node3D).global_position)
|
||||||
|
return # don't descend into a figure's own rig
|
||||||
|
for c in node.get_children():
|
||||||
|
_collect_figures(c, out)
|
||||||
|
|
||||||
|
|
||||||
|
func _fallback_stand(out: PackedVector3Array) -> void:
|
||||||
|
var placed := 0
|
||||||
|
for row in fallback_rows:
|
||||||
|
var radius := fallback_inner_radius + float(row) * fallback_row_step
|
||||||
|
var y := float(row) * fallback_row_rise
|
||||||
|
var circumference := TAU * radius
|
||||||
|
var seats_in_row := int(circumference / fallback_seat_step)
|
||||||
|
for s in seats_in_row:
|
||||||
|
if placed >= fallback_count:
|
||||||
|
return
|
||||||
|
var ang := TAU * float(s) / float(seats_in_row)
|
||||||
|
out.append(Vector3(cos(ang) * radius, y, sin(ang) * radius))
|
||||||
|
placed += 1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cj0ytlkuavmf4
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
shader_type canvas_item;
|
||||||
|
|
||||||
|
// Faint red gradient hugging the screen edges, flashed on a hit. `flash` is tweened
|
||||||
|
// 0.9 → 0 by the HUD; the vignette is transparent in the centre and reddens toward
|
||||||
|
// the corners so the middle of the screen stays clear.
|
||||||
|
uniform float flash : hint_range(0.0, 1.0) = 0.0;
|
||||||
|
uniform vec3 tint : source_color = vec3(0.75, 0.0, 0.0);
|
||||||
|
uniform float inner : hint_range(0.0, 1.0) = 0.45;
|
||||||
|
uniform float outer : hint_range(0.0, 1.0) = 1.0;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
// Distance from centre normalised so the corners land at ~1.0.
|
||||||
|
float dist = length(UV - vec2(0.5)) / 0.70710678;
|
||||||
|
float v = smoothstep(inner, outer, dist);
|
||||||
|
COLOR = vec4(tint, v * flash);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dys23pu1kw5fl
|
||||||
@@ -0,0 +1,509 @@
|
|||||||
|
extends Node
|
||||||
|
## Runtime debug overlays (autoload "DebugDraw").
|
||||||
|
##
|
||||||
|
## Two mechanisms, both gated by DP "Debug" bool params so they cost nothing when off:
|
||||||
|
## • Immediate-mode 3D lines — call DebugDraw.line()/ray() each frame; flushed once
|
||||||
|
## per frame into a single ImmediateMesh. Used for raycasts and the bone overlay.
|
||||||
|
## • Shape overlays — a MeshInstance3D attached to each CollisionShape3D, reconciled
|
||||||
|
## incrementally so static geometry is never re-instanced and freshly spawned
|
||||||
|
## bodies get picked up within one interval.
|
||||||
|
##
|
||||||
|
## Toggle from the console:
|
||||||
|
## show_collisions · show_hitboxes · show_bones · show_raycasts (true / false)
|
||||||
|
## show_states — floating AI-state tags + velocity vectors over each matador
|
||||||
|
## show_animation_name — floating current-clip tag over every AnimationPlayer host
|
||||||
|
## show_stats — 2D corner readout: fps / frame ms / draw calls / matador tally
|
||||||
|
## show_grid — 1 m reference grid on the ground plane around the origin
|
||||||
|
## show_axes — RGB world-axis gizmo at the origin
|
||||||
|
## show_wireframe — render the whole viewport in wireframe
|
||||||
|
|
||||||
|
const REBUILD_INTERVAL := 0.5
|
||||||
|
const STATS_INTERVAL := 0.2 # stats text is re-composed 5×/sec, not every frame
|
||||||
|
|
||||||
|
const COLOR_BODY := Color(0.30, 1.0, 0.45) # collision wireframes (green)
|
||||||
|
const COLOR_AREA := Color(0.0, 0.9, 1.0) # hit-area volumes (cyan, translucent)
|
||||||
|
const COLOR_BONE := Color(1.0, 0.35, 0.9) # skeleton bones (magenta)
|
||||||
|
const COLOR_VEL := Color(1.0, 0.85, 0.2) # matador velocity vectors (amber)
|
||||||
|
const COLOR_ANIM := Color(0.55, 0.85, 1.0) # animation-name tags (sky blue)
|
||||||
|
const COLOR_GRID := Color(0.45, 0.45, 0.45, 0.5)
|
||||||
|
const AREA_ALPHA := 0.25
|
||||||
|
|
||||||
|
const GRID_HALF := 20 # grid extends ±GRID_HALF cells from the origin
|
||||||
|
const GRID_STEP := 1.0 # metres between grid lines
|
||||||
|
const AXIS_LEN := 3.0 # length of each world-axis gizmo arm
|
||||||
|
|
||||||
|
var _line_mesh: ImmediateMesh
|
||||||
|
var _lines: PackedVector3Array = PackedVector3Array()
|
||||||
|
var _colors: PackedColorArray = PackedColorArray()
|
||||||
|
|
||||||
|
var _overlays: Dictionary = {} # CollisionShape3D instance id -> MeshInstance3D
|
||||||
|
var _skeletons: Array[Skeleton3D] = []
|
||||||
|
var _anim_players: Array[AnimationPlayer] = []
|
||||||
|
var _reconcile_t: float = 0.0
|
||||||
|
var _prev_flags: String = ""
|
||||||
|
|
||||||
|
var _state_labels: Dictionary = {} # matador instance id -> Label3D
|
||||||
|
var _anim_labels: Dictionary = {} # AnimationPlayer instance id -> Label3D
|
||||||
|
|
||||||
|
var _stats_layer: CanvasLayer = null
|
||||||
|
var _stats_label: Label = null
|
||||||
|
var _stats_t: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS # keep drawing while the console pauses the game
|
||||||
|
process_priority = 1000 # flush after everyone has queued their lines
|
||||||
|
# One-time: wireframe debug-draw needs the wireframe index buffers generated.
|
||||||
|
RenderingServer.set_debug_generate_wireframes(true)
|
||||||
|
_line_mesh = ImmediateMesh.new()
|
||||||
|
var inst := MeshInstance3D.new()
|
||||||
|
inst.mesh = _line_mesh
|
||||||
|
inst.material_override = _line_material()
|
||||||
|
inst.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
add_child(inst)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Public immediate-mode API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func line(from: Vector3, to: Vector3, color: Color = Color.YELLOW) -> void:
|
||||||
|
_lines.append(from)
|
||||||
|
_lines.append(to)
|
||||||
|
_colors.append(color)
|
||||||
|
_colors.append(color)
|
||||||
|
|
||||||
|
|
||||||
|
func ray(from: Vector3, to: Vector3, color: Color = Color.YELLOW) -> void:
|
||||||
|
line(from, to, color)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Frame flush ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_reconcile(delta)
|
||||||
|
if DP.b("show_bones"):
|
||||||
|
_draw_bones()
|
||||||
|
if DP.b("show_states"):
|
||||||
|
_update_state_labels()
|
||||||
|
elif not _state_labels.is_empty():
|
||||||
|
_clear_state_labels()
|
||||||
|
if DP.b("show_animation_name"):
|
||||||
|
_update_anim_labels()
|
||||||
|
elif not _anim_labels.is_empty():
|
||||||
|
_clear_anim_labels()
|
||||||
|
if DP.b("show_grid"):
|
||||||
|
_draw_grid()
|
||||||
|
if DP.b("show_axes"):
|
||||||
|
_draw_axes()
|
||||||
|
_update_wireframe()
|
||||||
|
_update_stats(delta)
|
||||||
|
_flush_lines()
|
||||||
|
|
||||||
|
|
||||||
|
func _flush_lines() -> void:
|
||||||
|
_line_mesh.clear_surfaces()
|
||||||
|
if not _lines.is_empty():
|
||||||
|
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINES)
|
||||||
|
for i in _lines.size():
|
||||||
|
_line_mesh.surface_set_color(_colors[i])
|
||||||
|
_line_mesh.surface_add_vertex(_lines[i])
|
||||||
|
_line_mesh.surface_end()
|
||||||
|
_lines.clear()
|
||||||
|
_colors.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Bone overlay ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_bones() -> void:
|
||||||
|
for sk in _skeletons:
|
||||||
|
if not is_instance_valid(sk):
|
||||||
|
continue
|
||||||
|
var gx := sk.global_transform
|
||||||
|
for b in sk.get_bone_count():
|
||||||
|
var parent := sk.get_bone_parent(b)
|
||||||
|
if parent < 0:
|
||||||
|
continue
|
||||||
|
var a := gx * sk.get_bone_global_pose(parent).origin
|
||||||
|
var c := gx * sk.get_bone_global_pose(b).origin
|
||||||
|
line(a, c, COLOR_BONE)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Matador AI-state overlay ───────────────────────────────────────────────────
|
||||||
|
# A billboarded state tag hovers over each matador, coloured by state, with an
|
||||||
|
# amber velocity vector — reads the whole arena's AI at a glance while tuning combat.
|
||||||
|
|
||||||
|
func _update_state_labels() -> void:
|
||||||
|
var seen: Dictionary = {}
|
||||||
|
for node: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||||
|
var m := node as Node3D
|
||||||
|
if m == null:
|
||||||
|
continue
|
||||||
|
var id := m.get_instance_id()
|
||||||
|
seen[id] = true
|
||||||
|
var lbl: Label3D = _state_labels.get(id)
|
||||||
|
if lbl == null:
|
||||||
|
lbl = _new_state_label()
|
||||||
|
_state_labels[id] = lbl
|
||||||
|
var state_name := "?"
|
||||||
|
if m.has_method(&"ai_state_name"):
|
||||||
|
state_name = m.call(&"ai_state_name")
|
||||||
|
lbl.text = state_name
|
||||||
|
lbl.modulate = _state_color(state_name)
|
||||||
|
lbl.global_position = m.global_position + Vector3.UP * 2.2
|
||||||
|
if m is CharacterBody3D:
|
||||||
|
var v: Vector3 = (m as CharacterBody3D).velocity
|
||||||
|
v.y = 0.0
|
||||||
|
if v.length() > 0.5:
|
||||||
|
var base := m.global_position + Vector3.UP * 0.15
|
||||||
|
line(base, base + v * 0.18, COLOR_VEL)
|
||||||
|
for id: int in _state_labels.keys():
|
||||||
|
if not seen.has(id):
|
||||||
|
var lbl: Label3D = _state_labels[id]
|
||||||
|
if is_instance_valid(lbl):
|
||||||
|
lbl.queue_free()
|
||||||
|
_state_labels.erase(id)
|
||||||
|
|
||||||
|
|
||||||
|
func _new_state_label() -> Label3D:
|
||||||
|
var lbl := Label3D.new()
|
||||||
|
lbl.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
|
lbl.no_depth_test = true
|
||||||
|
lbl.fixed_size = true
|
||||||
|
lbl.pixel_size = 0.0006
|
||||||
|
lbl.font_size = 64
|
||||||
|
lbl.outline_size = 12
|
||||||
|
lbl.outline_modulate = Color(0.0, 0.0, 0.0, 0.9)
|
||||||
|
add_child(lbl)
|
||||||
|
return lbl
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_state_labels() -> void:
|
||||||
|
for id: int in _state_labels:
|
||||||
|
var lbl: Label3D = _state_labels[id]
|
||||||
|
if is_instance_valid(lbl):
|
||||||
|
lbl.queue_free()
|
||||||
|
_state_labels.clear()
|
||||||
|
|
||||||
|
|
||||||
|
const _STATE_COLORS := {
|
||||||
|
"WANDER": Color(0.70, 0.70, 0.70),
|
||||||
|
"FLEE": Color(0.40, 0.80, 1.00),
|
||||||
|
"BRACE": Color(1.00, 0.90, 0.30),
|
||||||
|
"SIDESTEP": Color(1.00, 0.60, 0.10),
|
||||||
|
"ATTACK": Color(1.00, 0.30, 0.30),
|
||||||
|
"ROLL": Color(0.60, 1.00, 0.60),
|
||||||
|
"THROW": Color(1.00, 0.40, 0.90),
|
||||||
|
"RAGDOLL": Color(0.40, 0.40, 0.40),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _state_color(state_name: String) -> Color:
|
||||||
|
return _STATE_COLORS.get(state_name, Color.WHITE)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Animation-name overlay ─────────────────────────────────────────────────────
|
||||||
|
# A billboarded tag showing the clip each AnimationPlayer is currently playing,
|
||||||
|
# hovering above its nearest Node3D host — catches wrong / stuck animation states
|
||||||
|
# at a glance. Players are re-collected on the reconcile tick (see _collect).
|
||||||
|
|
||||||
|
func _update_anim_labels() -> void:
|
||||||
|
var seen: Dictionary = {}
|
||||||
|
for ap in _anim_players:
|
||||||
|
if not is_instance_valid(ap):
|
||||||
|
continue
|
||||||
|
var clip := ap.current_animation
|
||||||
|
# Skip finished / dormant sub-players so a host with several AnimationPlayers
|
||||||
|
# (e.g. a matador's main + olé + death players) doesn't stack empty tags.
|
||||||
|
if clip.is_empty() and not ap.is_playing():
|
||||||
|
continue
|
||||||
|
var host := _node3d_host(ap)
|
||||||
|
if host == null:
|
||||||
|
continue
|
||||||
|
var id := ap.get_instance_id()
|
||||||
|
seen[id] = true
|
||||||
|
var lbl: Label3D = _anim_labels.get(id)
|
||||||
|
if lbl == null:
|
||||||
|
lbl = _new_state_label()
|
||||||
|
lbl.modulate = COLOR_ANIM
|
||||||
|
_anim_labels[id] = lbl
|
||||||
|
lbl.text = clip if not clip.is_empty() else "—"
|
||||||
|
lbl.global_position = host.global_position + Vector3.UP * 2.6
|
||||||
|
for id: int in _anim_labels.keys():
|
||||||
|
if not seen.has(id):
|
||||||
|
var lbl: Label3D = _anim_labels[id]
|
||||||
|
if is_instance_valid(lbl):
|
||||||
|
lbl.queue_free()
|
||||||
|
_anim_labels.erase(id)
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_anim_labels() -> void:
|
||||||
|
for id: int in _anim_labels:
|
||||||
|
var lbl: Label3D = _anim_labels[id]
|
||||||
|
if is_instance_valid(lbl):
|
||||||
|
lbl.queue_free()
|
||||||
|
_anim_labels.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# Nearest Node3D at or above `node` — the anchor an AnimationPlayer's tag hovers over.
|
||||||
|
func _node3d_host(node: Node) -> Node3D:
|
||||||
|
var n := node
|
||||||
|
while n != null:
|
||||||
|
if n is Node3D:
|
||||||
|
return n as Node3D
|
||||||
|
n = n.get_parent()
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
# ── World reference overlays ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_grid() -> void:
|
||||||
|
var extent := GRID_HALF * GRID_STEP
|
||||||
|
for i in range(-GRID_HALF, GRID_HALF + 1):
|
||||||
|
var o := i * GRID_STEP
|
||||||
|
line(Vector3(o, 0.01, -extent), Vector3(o, 0.01, extent), COLOR_GRID)
|
||||||
|
line(Vector3(-extent, 0.01, o), Vector3(extent, 0.01, o), COLOR_GRID)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_axes() -> void:
|
||||||
|
line(Vector3.ZERO, Vector3.RIGHT * AXIS_LEN, Color.RED) # +X
|
||||||
|
line(Vector3.ZERO, Vector3.UP * AXIS_LEN, Color.GREEN) # +Y
|
||||||
|
line(Vector3.ZERO, Vector3(0.0, 0.0, 1.0) * AXIS_LEN, Color.DODGER_BLUE) # +Z
|
||||||
|
|
||||||
|
|
||||||
|
func _update_wireframe() -> void:
|
||||||
|
var vp := get_viewport()
|
||||||
|
var want := Viewport.DEBUG_DRAW_WIREFRAME if DP.b("show_wireframe") else Viewport.DEBUG_DRAW_DISABLED
|
||||||
|
if vp.debug_draw != want:
|
||||||
|
vp.debug_draw = want
|
||||||
|
|
||||||
|
|
||||||
|
# ── Stats overlay ──────────────────────────────────────────────────────────────
|
||||||
|
# A 2D corner readout of frame cost + scene load, plus a per-state matador tally so
|
||||||
|
# a combat slowdown or a stuck AI swarm shows up immediately.
|
||||||
|
|
||||||
|
func _update_stats(delta: float) -> void:
|
||||||
|
if not DP.b("show_stats"):
|
||||||
|
if _stats_layer != null:
|
||||||
|
_stats_layer.visible = false
|
||||||
|
return
|
||||||
|
_ensure_stats_ui()
|
||||||
|
_stats_layer.visible = true
|
||||||
|
_stats_t -= delta
|
||||||
|
if _stats_t > 0.0:
|
||||||
|
return
|
||||||
|
_stats_t = STATS_INTERVAL
|
||||||
|
_stats_label.text = _compose_stats()
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_stats_ui() -> void:
|
||||||
|
if _stats_layer != null:
|
||||||
|
return
|
||||||
|
var mono := SystemFont.new()
|
||||||
|
mono.font_names = PackedStringArray(["JetBrains Mono", "DejaVu Sans Mono", "monospace"])
|
||||||
|
|
||||||
|
_stats_layer = CanvasLayer.new()
|
||||||
|
_stats_layer.layer = 64
|
||||||
|
add_child(_stats_layer)
|
||||||
|
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.position = Vector2(8.0, 8.0)
|
||||||
|
var bg := StyleBoxFlat.new()
|
||||||
|
bg.bg_color = Color(0.0, 0.0, 0.0, 0.55)
|
||||||
|
bg.set_content_margin_all(6.0)
|
||||||
|
panel.add_theme_stylebox_override("panel", bg)
|
||||||
|
_stats_layer.add_child(panel)
|
||||||
|
|
||||||
|
_stats_label = Label.new()
|
||||||
|
_stats_label.add_theme_font_override("font", mono)
|
||||||
|
_stats_label.add_theme_font_size_override("font_size", 13)
|
||||||
|
_stats_label.add_theme_color_override("font_color", Color(0.60, 1.0, 0.60))
|
||||||
|
panel.add_child(_stats_label)
|
||||||
|
|
||||||
|
|
||||||
|
func _compose_stats() -> String:
|
||||||
|
var fps := int(Performance.get_monitor(Performance.TIME_FPS))
|
||||||
|
var proc := Performance.get_monitor(Performance.TIME_PROCESS) * 1000.0
|
||||||
|
var phys := Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000.0
|
||||||
|
var draws := int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
|
||||||
|
var prims := int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME))
|
||||||
|
var vmem := int(Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED) / 1048576.0)
|
||||||
|
var nodes := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||||
|
var phys3d := int(Performance.get_monitor(Performance.PHYSICS_3D_ACTIVE_OBJECTS))
|
||||||
|
return "fps %d proc %.1fms phys %.1fms\ndraws %d prims %s vmem %dMB\nnodes %d phys3d %d\n%s" % [
|
||||||
|
fps, proc, phys, draws, _si(prims), vmem, nodes, phys3d, _matador_tally()]
|
||||||
|
|
||||||
|
|
||||||
|
func _matador_tally() -> String:
|
||||||
|
var mats := get_tree().get_nodes_in_group(&"matador")
|
||||||
|
if mats.is_empty():
|
||||||
|
return "matadors 0"
|
||||||
|
var counts: Dictionary = {}
|
||||||
|
for node: Node in mats:
|
||||||
|
var s := "?"
|
||||||
|
if node.has_method(&"ai_state_name"):
|
||||||
|
s = node.call(&"ai_state_name")
|
||||||
|
counts[s] = int(counts.get(s, 0)) + 1
|
||||||
|
var parts: PackedStringArray = PackedStringArray()
|
||||||
|
for s: String in counts:
|
||||||
|
parts.append("%s:%d" % [s.substr(0, 2), counts[s]])
|
||||||
|
return "matadors %d %s" % [mats.size(), " ".join(parts)]
|
||||||
|
|
||||||
|
|
||||||
|
func _si(n: int) -> String:
|
||||||
|
if n >= 1000000:
|
||||||
|
return "%.1fM" % (n / 1000000.0)
|
||||||
|
if n >= 1000:
|
||||||
|
return "%.0fk" % (n / 1000.0)
|
||||||
|
return str(n)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Collision / hit-area overlays (incremental) ────────────────────────────────
|
||||||
|
|
||||||
|
func _reconcile(delta: float) -> void:
|
||||||
|
var col := DP.b("show_collisions")
|
||||||
|
var hit := DP.b("show_hitboxes")
|
||||||
|
var bone := DP.b("show_bones")
|
||||||
|
var anim := DP.b("show_animation_name")
|
||||||
|
if not (col or hit or bone or anim):
|
||||||
|
if not _overlays.is_empty():
|
||||||
|
_clear_overlays()
|
||||||
|
_skeletons.clear()
|
||||||
|
_anim_players.clear()
|
||||||
|
_prev_flags = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
var flags := "%d%d%d%d" % [int(col), int(hit), int(bone), int(anim)]
|
||||||
|
_reconcile_t -= delta
|
||||||
|
if flags != _prev_flags: # a toggle just changed — refresh immediately
|
||||||
|
_prev_flags = flags
|
||||||
|
_reconcile_t = 0.0
|
||||||
|
if _reconcile_t > 0.0:
|
||||||
|
return
|
||||||
|
_reconcile_t = REBUILD_INTERVAL
|
||||||
|
|
||||||
|
_skeletons.clear()
|
||||||
|
_anim_players.clear()
|
||||||
|
var wanted: Dictionary = {}
|
||||||
|
var scene := get_tree().current_scene
|
||||||
|
if scene != null:
|
||||||
|
_collect(scene, col, hit, bone, anim, 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: Variant = _overlays[id]
|
||||||
|
if not wanted.has(id) or not is_instance_valid(mi):
|
||||||
|
if is_instance_valid(mi):
|
||||||
|
(mi as Node).queue_free()
|
||||||
|
_overlays.erase(id)
|
||||||
|
# Add overlays for newly seen shapes.
|
||||||
|
for id: int in wanted:
|
||||||
|
if not _overlays.has(id):
|
||||||
|
var entry: Array = wanted[id]
|
||||||
|
_overlays[id] = _attach(entry[0] as CollisionShape3D, entry[1] as bool)
|
||||||
|
|
||||||
|
|
||||||
|
func _collect(node: Node, col: bool, hit: bool, bone: bool, anim: bool, wanted: Dictionary) -> void:
|
||||||
|
# The audience ("public") is ambient decoration, not a debug target — skip the whole
|
||||||
|
# crowd subtree so its clapping AnimationPlayers/skeletons don't flood the overlays.
|
||||||
|
if node.is_in_group(&"public"):
|
||||||
|
return
|
||||||
|
if bone and node is Skeleton3D and (node as Skeleton3D).is_visible_in_tree():
|
||||||
|
_skeletons.append(node as Skeleton3D)
|
||||||
|
if anim and node is AnimationPlayer:
|
||||||
|
_anim_players.append(node as AnimationPlayer)
|
||||||
|
if node is CollisionShape3D:
|
||||||
|
var cs := node as CollisionShape3D
|
||||||
|
if cs.shape != null and not cs.disabled and _drawable(cs.shape):
|
||||||
|
var parent := cs.get_parent()
|
||||||
|
var is_area := parent is Area3D
|
||||||
|
# Ragdoll bone colliders (PhysicalBone3D) are excluded — use show_bones for
|
||||||
|
# the skeleton; otherwise every corpse floods the view with capsules.
|
||||||
|
var is_body := parent is PhysicsBody3D and not (parent is PhysicalBone3D)
|
||||||
|
if (is_area and hit) or (is_body and col):
|
||||||
|
wanted[cs.get_instance_id()] = [cs, is_area]
|
||||||
|
for child in node.get_children():
|
||||||
|
_collect(child, col, hit, bone, anim, wanted)
|
||||||
|
|
||||||
|
|
||||||
|
# Skip only the unbounded shapes whose debug mesh is meaningless/huge; concave arena
|
||||||
|
# geometry is fine — Godot caches each shape's debug mesh, so it's built at most once.
|
||||||
|
func _drawable(shape: Shape3D) -> bool:
|
||||||
|
return not (shape is WorldBoundaryShape3D or shape is HeightMapShape3D)
|
||||||
|
|
||||||
|
|
||||||
|
func _attach(cs: CollisionShape3D, is_area: bool) -> MeshInstance3D:
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
if is_area:
|
||||||
|
var solid := _solid_mesh(cs.shape)
|
||||||
|
mi.mesh = solid if solid != null else cs.shape.get_debug_mesh()
|
||||||
|
mi.material_override = _translucent_material(COLOR_AREA)
|
||||||
|
else:
|
||||||
|
mi.mesh = cs.shape.get_debug_mesh()
|
||||||
|
mi.material_override = _wire_material(COLOR_BODY)
|
||||||
|
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
cs.add_child(mi)
|
||||||
|
return mi
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_overlays() -> void:
|
||||||
|
for id: int in _overlays:
|
||||||
|
var mi: Variant = _overlays[id]
|
||||||
|
if is_instance_valid(mi):
|
||||||
|
(mi as Node).queue_free()
|
||||||
|
_overlays.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Meshes & materials ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# A filled mesh matching a primitive shape, for translucent volume overlays. Returns
|
||||||
|
# null for shapes without a clean solid equivalent (caller falls back to wireframe).
|
||||||
|
func _solid_mesh(shape: Shape3D) -> Mesh:
|
||||||
|
if shape is BoxShape3D:
|
||||||
|
var m := BoxMesh.new()
|
||||||
|
m.size = (shape as BoxShape3D).size
|
||||||
|
return m
|
||||||
|
if shape is SphereShape3D:
|
||||||
|
var m := SphereMesh.new()
|
||||||
|
m.radius = (shape as SphereShape3D).radius
|
||||||
|
m.height = m.radius * 2.0
|
||||||
|
return m
|
||||||
|
if shape is CapsuleShape3D:
|
||||||
|
var m := CapsuleMesh.new()
|
||||||
|
m.radius = (shape as CapsuleShape3D).radius
|
||||||
|
m.height = (shape as CapsuleShape3D).height
|
||||||
|
return m
|
||||||
|
if shape is CylinderShape3D:
|
||||||
|
var m := CylinderMesh.new()
|
||||||
|
m.top_radius = (shape as CylinderShape3D).radius
|
||||||
|
m.bottom_radius = (shape as CylinderShape3D).radius
|
||||||
|
m.height = (shape as CylinderShape3D).height
|
||||||
|
return m
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _line_material() -> StandardMaterial3D:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.vertex_color_use_as_albedo = true
|
||||||
|
mat.no_depth_test = true
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
func _wire_material(color: Color) -> StandardMaterial3D:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.albedo_color = color
|
||||||
|
mat.no_depth_test = true
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
func _translucent_material(color: Color) -> StandardMaterial3D:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
mat.albedo_color = Color(color.r, color.g, color.b, AREA_ALPHA)
|
||||||
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
|
mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||||
|
return mat
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cgty4al6aqey3
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
extends Node
|
|
||||||
## Runtime debug panel. Toggle with F1.
|
|
||||||
## Reads DP.get_all() to build controls — adding a param to DP is all that's needed to show it here.
|
|
||||||
|
|
||||||
var _panel: Control
|
|
||||||
var _status_label: Label
|
|
||||||
var _prev_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_CAPTURED
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
_build_ui()
|
|
||||||
|
|
||||||
|
|
||||||
func _build_ui() -> void:
|
|
||||||
var canvas := CanvasLayer.new()
|
|
||||||
canvas.layer = 128
|
|
||||||
add_child(canvas)
|
|
||||||
|
|
||||||
_panel = PanelContainer.new()
|
|
||||||
_panel.anchor_left = 1.0
|
|
||||||
_panel.anchor_right = 1.0
|
|
||||||
_panel.anchor_top = 0.0
|
|
||||||
_panel.anchor_bottom = 1.0
|
|
||||||
_panel.offset_left = -390.0
|
|
||||||
_panel.offset_right = 0.0
|
|
||||||
_panel.offset_top = 0.0
|
|
||||||
_panel.offset_bottom = 0.0
|
|
||||||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
||||||
_panel.visible = false
|
|
||||||
|
|
||||||
var bg := StyleBoxFlat.new()
|
|
||||||
bg.bg_color = Color(0.08, 0.08, 0.10, 0.95)
|
|
||||||
_panel.add_theme_stylebox_override("panel", bg)
|
|
||||||
canvas.add_child(_panel)
|
|
||||||
|
|
||||||
var root_vbox := VBoxContainer.new()
|
|
||||||
root_vbox.size_flags_horizontal = Control.SIZE_FILL
|
|
||||||
_panel.add_child(root_vbox)
|
|
||||||
|
|
||||||
# ── Toolbar ───────────────────────────────────────────────────────────────
|
|
||||||
var toolbar_margin := MarginContainer.new()
|
|
||||||
toolbar_margin.add_theme_constant_override("margin_left", 8)
|
|
||||||
toolbar_margin.add_theme_constant_override("margin_right", 8)
|
|
||||||
toolbar_margin.add_theme_constant_override("margin_top", 6)
|
|
||||||
toolbar_margin.add_theme_constant_override("margin_bottom", 4)
|
|
||||||
root_vbox.add_child(toolbar_margin)
|
|
||||||
|
|
||||||
var toolbar := HBoxContainer.new()
|
|
||||||
toolbar.add_theme_constant_override("separation", 6)
|
|
||||||
toolbar_margin.add_child(toolbar)
|
|
||||||
|
|
||||||
var title := Label.new()
|
|
||||||
title.text = "DEBUG [F1]"
|
|
||||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
toolbar.add_child(title)
|
|
||||||
|
|
||||||
_status_label = Label.new()
|
|
||||||
_status_label.modulate = Color(0.4, 1.0, 0.5)
|
|
||||||
toolbar.add_child(_status_label)
|
|
||||||
|
|
||||||
var save_btn := Button.new()
|
|
||||||
save_btn.text = "Save"
|
|
||||||
save_btn.pressed.connect(_on_save)
|
|
||||||
toolbar.add_child(save_btn)
|
|
||||||
|
|
||||||
var rand_all_btn := Button.new()
|
|
||||||
rand_all_btn.text = "Randomize All"
|
|
||||||
rand_all_btn.pressed.connect(_on_randomize_all)
|
|
||||||
toolbar.add_child(rand_all_btn)
|
|
||||||
|
|
||||||
var reset_btn := Button.new()
|
|
||||||
reset_btn.text = "Reset All"
|
|
||||||
reset_btn.pressed.connect(DP.reset_all)
|
|
||||||
toolbar.add_child(reset_btn)
|
|
||||||
|
|
||||||
root_vbox.add_child(HSeparator.new())
|
|
||||||
|
|
||||||
# ── Scrollable content ────────────────────────────────────────────────────
|
|
||||||
var scroll := ScrollContainer.new()
|
|
||||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
||||||
scroll.size_flags_horizontal = Control.SIZE_FILL
|
|
||||||
root_vbox.add_child(scroll)
|
|
||||||
|
|
||||||
var content_margin := MarginContainer.new()
|
|
||||||
content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
content_margin.add_theme_constant_override("margin_left", 8)
|
|
||||||
content_margin.add_theme_constant_override("margin_right", 8)
|
|
||||||
content_margin.add_theme_constant_override("margin_top", 4)
|
|
||||||
content_margin.add_theme_constant_override("margin_bottom", 8)
|
|
||||||
scroll.add_child(content_margin)
|
|
||||||
|
|
||||||
var content := VBoxContainer.new()
|
|
||||||
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
content_margin.add_child(content)
|
|
||||||
|
|
||||||
_build_params(content)
|
|
||||||
|
|
||||||
|
|
||||||
func _build_params(content: VBoxContainer) -> void:
|
|
||||||
# Group keys by section preserving registration order
|
|
||||||
var sections: Dictionary = {}
|
|
||||||
for key: String in DP.get_all():
|
|
||||||
var sec: String = DP.get_all()[key]["section"]
|
|
||||||
if not sections.has(sec):
|
|
||||||
sections[sec] = []
|
|
||||||
sections[sec].append(key)
|
|
||||||
|
|
||||||
for section: String in sections:
|
|
||||||
content.add_child(HSeparator.new())
|
|
||||||
|
|
||||||
var header := Label.new()
|
|
||||||
header.text = section.to_upper()
|
|
||||||
header.modulate = Color(0.75, 0.85, 1.0)
|
|
||||||
content.add_child(header)
|
|
||||||
|
|
||||||
for key: String in sections[section]:
|
|
||||||
var meta: Dictionary = DP.get_all()[key]
|
|
||||||
if meta["type"] == TYPE_FLOAT:
|
|
||||||
_add_float_row(content, key, meta)
|
|
||||||
|
|
||||||
|
|
||||||
func _add_float_row(parent: VBoxContainer, key: String, meta: Dictionary) -> void:
|
|
||||||
var row := HBoxContainer.new()
|
|
||||||
row.size_flags_horizontal = Control.SIZE_FILL
|
|
||||||
parent.add_child(row)
|
|
||||||
|
|
||||||
var lbl := Label.new()
|
|
||||||
lbl.text = key.replace("_", " ")
|
|
||||||
lbl.custom_minimum_size.x = 140
|
|
||||||
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
||||||
row.add_child(lbl)
|
|
||||||
|
|
||||||
var slider := HSlider.new()
|
|
||||||
slider.min_value = meta["min"]
|
|
||||||
slider.max_value = meta["max"]
|
|
||||||
slider.step = meta["step"]
|
|
||||||
slider.value = meta["value"]
|
|
||||||
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
slider.custom_minimum_size.x = 60
|
|
||||||
row.add_child(slider)
|
|
||||||
|
|
||||||
var spin := SpinBox.new()
|
|
||||||
spin.min_value = meta["min"]
|
|
||||||
spin.max_value = meta["max"]
|
|
||||||
spin.step = meta["step"]
|
|
||||||
spin.value = meta["value"]
|
|
||||||
spin.custom_minimum_size.x = 85
|
|
||||||
row.add_child(spin)
|
|
||||||
|
|
||||||
var rand_btn := Button.new()
|
|
||||||
rand_btn.text = "🎲"
|
|
||||||
rand_btn.tooltip_text = "Randomize"
|
|
||||||
rand_btn.pressed.connect(func() -> void:
|
|
||||||
var v := randf_range(meta["min"], meta["max"])
|
|
||||||
v = snappedf(v, meta["step"])
|
|
||||||
slider.value = v
|
|
||||||
)
|
|
||||||
row.add_child(rand_btn)
|
|
||||||
|
|
||||||
# Slider → SpinBox + DP (use no_signal to avoid echo)
|
|
||||||
slider.value_changed.connect(func(v: float) -> void:
|
|
||||||
spin.set_value_no_signal(v)
|
|
||||||
DP.set_value(key, v)
|
|
||||||
)
|
|
||||||
# SpinBox → Slider + DP
|
|
||||||
spin.value_changed.connect(func(v: float) -> void:
|
|
||||||
slider.set_value_no_signal(v)
|
|
||||||
DP.set_value(key, v)
|
|
||||||
)
|
|
||||||
# External changes (reset, load) → sync both controls
|
|
||||||
DP.any_changed.connect(func(changed_key: String, _val: Variant) -> void:
|
|
||||||
if changed_key != key and changed_key != "__all__":
|
|
||||||
return
|
|
||||||
var v := DP.f(key)
|
|
||||||
slider.set_value_no_signal(v)
|
|
||||||
spin.set_value_no_signal(v)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _on_save() -> void:
|
|
||||||
DP.save()
|
|
||||||
_status_label.text = "Saved!"
|
|
||||||
get_tree().create_timer(1.5).timeout.connect(func() -> void:
|
|
||||||
_status_label.text = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if _panel.visible and Input.is_key_pressed(KEY_CTRL) and event is InputEventMouseMotion:
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
|
||||||
if not (event is InputEventKey and event.pressed and not event.echo):
|
|
||||||
return
|
|
||||||
if (event as InputEventKey).keycode != KEY_F1:
|
|
||||||
return
|
|
||||||
_panel.visible = not _panel.visible
|
|
||||||
if _panel.visible:
|
|
||||||
_prev_mouse_mode = Input.mouse_mode
|
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
||||||
else:
|
|
||||||
Input.mouse_mode = _prev_mouse_mode
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func _on_randomize_all() -> void:
|
|
||||||
for key: String in DP.get_all():
|
|
||||||
var meta: Dictionary = DP.get_all()[key]
|
|
||||||
if meta["type"] == TYPE_FLOAT:
|
|
||||||
var v := snappedf(randf_range(meta["min"], meta["max"]), meta["step"])
|
|
||||||
DP.set_value(key, v)
|
|
||||||
@@ -7,7 +7,12 @@ signal any_changed(key: String, value: Variant)
|
|||||||
|
|
||||||
const SAVE_PATH := "user://debug_params.cfg"
|
const SAVE_PATH := "user://debug_params.cfg"
|
||||||
|
|
||||||
|
# _params holds the full metadata (min/max/step/default/section/type) and is the
|
||||||
|
# authority the debug menu reads. _values is a flat key→value mirror so the hot
|
||||||
|
# f()/b() reads — called dozens of times per matador per physics frame — cost a
|
||||||
|
# single hash lookup instead of a nested dict access. Every write updates both.
|
||||||
var _params: Dictionary = {}
|
var _params: Dictionary = {}
|
||||||
|
var _values: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -17,25 +22,22 @@ func _ready() -> void:
|
|||||||
|
|
||||||
func _register_all() -> void:
|
func _register_all() -> void:
|
||||||
# ── Movement ──────────────────────────────────────────────────────────────
|
# ── Movement ──────────────────────────────────────────────────────────────
|
||||||
_reg_f("Movement", "thrust_impulse", 18.0, 1.0, 200.0)
|
# WASD/arrows drive the bull: move_accel ramps flat velocity toward max_speed in
|
||||||
_reg_f("Movement", "thrust_vertical", 2.0, 0.0, 20.0)
|
# the input direction; move_friction bleeds it off when no direction is held.
|
||||||
_reg_f("Movement", "thrust_min_angle", 15.0, 0.0, 90.0, 1.0)
|
_reg_f("Movement", "max_speed", 10.0, 2.0, 1000.0)
|
||||||
_reg_f("Movement", "thrust_max_angle", 90.0, 0.0, 180.0, 1.0)
|
_reg_f("Movement", "move_accel", 120.0, 5.0, 600.0)
|
||||||
_reg_f("Movement", "thrust_charge_time", 0.5, 0.05, 2.0, 0.05)
|
_reg_f("Movement", "move_friction", 90.0, 5.0, 600.0)
|
||||||
_reg_f("Movement", "max_speed", 50.0, 2.0, 1000.0)
|
|
||||||
_reg_f("Movement", "roll_damping", 0.15, 0.01, 0.99, 0.01)
|
|
||||||
_reg_f("Movement", "jump_velocity", 4.5, 1.0, 200.0)
|
_reg_f("Movement", "jump_velocity", 4.5, 1.0, 200.0)
|
||||||
_reg_f("Movement", "visual_turn_speed", 12.0, 1.0, 200.0)
|
_reg_f("Movement", "visual_turn_speed", 12.0, 1.0, 200.0)
|
||||||
_reg_f("Movement", "bounce_restitution", 0.55, 0.0, 2.0, 0.05)
|
_reg_f("Movement", "bounce_restitution", 0.55, 0.0, 2.0, 0.05)
|
||||||
_reg_f("Movement", "bounce_min_impact", 1.0, 0.0, 20.0)
|
_reg_f("Movement", "bounce_min_impact", 1.0, 0.0, 20.0)
|
||||||
_reg_f("Movement", "wall_bounce_restitution", 0.6, 0.0, 1.5, 0.05)
|
_reg_f("Movement", "wall_bounce_restitution", 0.6, 0.0, 1.5, 0.05)
|
||||||
_reg_f("Movement", "wall_min_bounce_speed", 6.0, 0.0, 30.0)
|
_reg_f("Movement", "wall_min_bounce_speed", 6.0, 0.0, 30.0)
|
||||||
_reg_f("Movement", "kick_impulse", 8.0, 1.0, 50.0)
|
# ── Bull health ───────────────────────────────────────────────────────────
|
||||||
_reg_f("Movement", "kick_interval", 0.25, 0.05, 1.0, 0.01)
|
# The bull soaks bull_max_hp clean hits before it dies; each hit grants
|
||||||
_reg_f("Movement", "kick_vertical", 0.0, 0.0, 15.0)
|
# bull_hit_iframes seconds of invulnerability so a single pass can't drain it.
|
||||||
_reg_f("Movement", "kick_force_var", 0.2, 0.0, 1.0, 0.01)
|
_reg_f("Bull", "bull_max_hp", 10.0, 1.0, 100.0, 1.0)
|
||||||
_reg_f("Movement", "kick_spread", 10.0, 0.0, 45.0, 1.0)
|
_reg_f("Bull", "bull_hit_iframes", 0.8, 0.0, 3.0, 0.1)
|
||||||
_reg_f("Movement", "charge_kick_mult", 2.0, 1.0, 5.0)
|
|
||||||
# ── Dust speed thresholds ─────────────────────────────────────────────────
|
# ── Dust speed thresholds ─────────────────────────────────────────────────
|
||||||
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
|
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
|
||||||
_reg_f("Dust", "dust_charge_spd", 7.0, 1.0, 20.0)
|
_reg_f("Dust", "dust_charge_spd", 7.0, 1.0, 20.0)
|
||||||
@@ -58,10 +60,7 @@ func _register_all() -> void:
|
|||||||
_reg_f("Tail", "tail_body_radius", 0.35, 0.05, 2.0, 0.05)
|
_reg_f("Tail", "tail_body_radius", 0.35, 0.05, 2.0, 0.05)
|
||||||
_reg_f("Tail", "tail_wag_freq", 4.0, 0.5, 15.0)
|
_reg_f("Tail", "tail_wag_freq", 4.0, 0.5, 15.0)
|
||||||
_reg_f("Tail", "tail_wag_idle", 0.3, 0.0, 2.0, 0.05)
|
_reg_f("Tail", "tail_wag_idle", 0.3, 0.0, 2.0, 0.05)
|
||||||
# ── Charge (both mouse buttons) ──────────────────────────────────────────
|
# ── Charge pose (horns-down lean during dash / roll) ─────────────────────
|
||||||
_reg_f("Charge", "bull_charge_accel", 80.0, 1.0, 400.0)
|
|
||||||
_reg_f("Charge", "bull_charge_max_speed", 76.0, 5.0, 200.0)
|
|
||||||
_reg_f("Charge", "charge_ramp_time", 0.75, 0.1, 5.0, 0.05)
|
|
||||||
_reg_f("Charge", "charge_head_pitch", 0.3, 0.0, 1.2, 0.01)
|
_reg_f("Charge", "charge_head_pitch", 0.3, 0.0, 1.2, 0.01)
|
||||||
_reg_f("Charge", "charge_head_speed", 5.0, 0.5, 20.0)
|
_reg_f("Charge", "charge_head_speed", 5.0, 0.5, 20.0)
|
||||||
# ── Body ──────────────────────────────────────────────────────────────────
|
# ── Body ──────────────────────────────────────────────────────────────────
|
||||||
@@ -91,38 +90,109 @@ func _register_all() -> void:
|
|||||||
_reg_f("Camera", "cam_spring_length", 7.0, 1.0, 30.0)
|
_reg_f("Camera", "cam_spring_length", 7.0, 1.0, 30.0)
|
||||||
_reg_f("Camera", "cam_min_vert_angle", -60.0, -90.0, 0.0, 1.0)
|
_reg_f("Camera", "cam_min_vert_angle", -60.0, -90.0, 0.0, 1.0)
|
||||||
_reg_f("Camera", "cam_max_vert_angle", 45.0, 0.0, 90.0, 1.0)
|
_reg_f("Camera", "cam_max_vert_angle", 45.0, 0.0, 90.0, 1.0)
|
||||||
|
# Trailing "drag" camera: the focus point lags the bull and slides opposite to
|
||||||
|
# its velocity, so the camera visibly drags behind during a fast charge.
|
||||||
|
# cam_drag = metres of trail per m/s of bull speed (0 = rigid follow)
|
||||||
|
# cam_drag_speed = how fast the trail offset eases toward its target
|
||||||
|
# cam_follow_lerp = how tightly the focus point chases the bull (higher = snappier)
|
||||||
|
_reg_f("Camera", "cam_drag", 0.12, 0.0, 1.0, 0.01)
|
||||||
|
_reg_f("Camera", "cam_drag_speed", 3.5, 0.5, 20.0)
|
||||||
|
_reg_f("Camera", "cam_follow_lerp", 10.0, 1.0, 40.0)
|
||||||
# ── Matador ───────────────────────────────────────────────────────────────
|
# ── Matador ───────────────────────────────────────────────────────────────
|
||||||
_reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0)
|
_reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0)
|
||||||
_reg_f("Matador", "mat_walk_speed", 4.0, 0.5, 10.0)
|
# Hydra split: a slain matador is replaced on the spot by mat_split_factor fresh
|
||||||
|
# matadors, so the horde doubles each generation (1 → 2 → 4 → 8 …) at the default
|
||||||
|
# 2. Capped at mat_split_generations deep so the arena stays winnable (factor <2 or
|
||||||
|
# generations 0 = no splitting, the classic one-and-done arena).
|
||||||
|
_reg_f("Matador", "mat_split_factor", 2.0, 1.0, 6.0, 1.0)
|
||||||
|
_reg_f("Matador", "mat_split_generations", 3.0, 0.0, 6.0, 1.0)
|
||||||
|
# The bull wins outright once it has slain this many matadors, even if the hydra
|
||||||
|
# still has bodies on the sand. 0 = disabled (win only when the horde dies out)
|
||||||
|
_reg_f("Matador", "mat_win_kills", 21.0, 0.0, 200.0, 1.0)
|
||||||
|
_reg_f("Matador", "mat_walk_speed", 5.5, 0.5, 10.0)
|
||||||
_reg_f("Matador", "mat_wander_radius", 22.0, 5.0, 50.0)
|
_reg_f("Matador", "mat_wander_radius", 22.0, 5.0, 50.0)
|
||||||
_reg_f("Matador", "mat_idle_min", 0.5, 0.0, 5.0, 0.1)
|
# Radius of the inner fighting floor: bull + matadors spawn on this ring (so
|
||||||
_reg_f("Matador", "mat_idle_max", 2.5, 0.5, 10.0, 0.1)
|
# 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", 1.2, 0.0, 5.0, 0.1)
|
||||||
|
_reg_f("Matador", "mat_idle_max", 4.0, 0.5, 10.0, 0.1)
|
||||||
_reg_f("Matador", "mat_hit_threshold", 4.0, 1.0, 30.0)
|
_reg_f("Matador", "mat_hit_threshold", 4.0, 1.0, 30.0)
|
||||||
_reg_f("Matador", "mat_ragdoll_impulse", 6.0, 0.5, 20.0)
|
_reg_f("Matador", "mat_ragdoll_impulse", 6.0, 0.5, 20.0)
|
||||||
|
# Ragdoll joint limits — lower swing/twist = stiffer corpse that keeps its
|
||||||
|
# shape; higher angular damp settles the flail instead of flopping flat.
|
||||||
|
_reg_f("Matador", "mat_joint_swing", 35.0, 0.0, 180.0, 1.0)
|
||||||
|
_reg_f("Matador", "mat_joint_twist", 40.0, 0.0, 180.0, 1.0)
|
||||||
|
_reg_f("Matador", "mat_ragdoll_damp", 1.5, 0.0, 10.0, 0.1)
|
||||||
_reg_f("Matador", "mat_flee_range", 3.5, 2.0, 40.0)
|
_reg_f("Matador", "mat_flee_range", 3.5, 2.0, 40.0)
|
||||||
_reg_f("Matador", "mat_flee_speed", 5.5, 0.5, 12.0)
|
_reg_f("Matador", "mat_flee_speed", 7.0, 0.5, 12.0)
|
||||||
_reg_f("Matador", "mat_charge_speed", 10.0, 1.0, 30.0)
|
_reg_f("Matador", "mat_charge_speed", 10.0, 1.0, 30.0)
|
||||||
_reg_f("Matador", "mat_brace_range", 5.0, 2.0, 20.0)
|
_reg_f("Matador", "mat_brace_range", 5.0, 2.0, 20.0)
|
||||||
_reg_f("Matador", "mat_brace_duration", 0.7, 0.05, 1.5, 0.05)
|
_reg_f("Matador", "mat_brace_duration", 0.7, 0.05, 1.5, 0.05)
|
||||||
_reg_f("Matador", "mat_commit_dist", 2.5, 0.5, 6.0, 0.1)
|
_reg_f("Matador", "mat_commit_dist", 2.5, 0.5, 6.0, 0.1)
|
||||||
_reg_f("Matador", "mat_step_speed", 3.5, 2.0, 20.0)
|
_reg_f("Matador", "mat_step_speed", 6.5, 2.0, 20.0)
|
||||||
_reg_f("Matador", "mat_step_duration", 0.35, 0.1, 1.0, 0.05)
|
_reg_f("Matador", "mat_step_duration", 0.35, 0.1, 1.0, 0.05)
|
||||||
_reg_f("Matador", "mat_dodge_cooldown", 5.0, 0.5, 12.0, 0.25)
|
# How strongly the sidestep biases toward the bull so the blade sweeps through
|
||||||
_reg_f("Matador", "mat_dodge_chance", 0.25, 0.0, 1.0, 0.05)
|
# 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.72, 0.0, 1.0, 0.05)
|
||||||
_reg_f("Matador", "mat_attack_range", 8.0, 2.0, 20.0)
|
_reg_f("Matador", "mat_attack_range", 8.0, 2.0, 20.0)
|
||||||
_reg_f("Matador", "mat_attack_speed", 5.5, 1.0, 12.0)
|
_reg_f("Matador", "mat_attack_speed", 7.5, 1.0, 12.0)
|
||||||
_reg_f("Matador", "mat_attack_duration", 1.8, 0.5, 5.0, 0.1)
|
_reg_f("Matador", "mat_attack_duration", 1.8, 0.5, 5.0, 0.1)
|
||||||
_reg_f("Matador", "mat_attack_min_dist", 2.5, 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", 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_chance", 0.35, 0.0, 1.0, 0.05)
|
||||||
_reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05)
|
_reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05)
|
||||||
# Draw the sword from the holster once the bull closes to within this range;
|
# Playback rate of the Draw_weapon clip used for both drawing and sheathing —
|
||||||
# re-sheathe when it retreats well beyond it (hysteresis in _update_sword_carry).
|
# higher = snappier unholster/holster.
|
||||||
_reg_f("Matador", "mat_draw_range", 12.0, 2.0, 40.0)
|
_reg_f("Matador", "mat_draw_speed", 1.9, 0.25, 3.0, 0.05)
|
||||||
# Cross-fade between matador animation clips — smooths Run/Attack/Taunt cuts.
|
# Cross-fade between matador animation clips — smooths Run/Attack/Taunt cuts.
|
||||||
_reg_f("Matador", "mat_anim_blend", 0.12, 0.0, 0.5, 0.01)
|
_reg_f("Matador", "mat_anim_blend", 0.12, 0.0, 0.5, 0.01)
|
||||||
|
# ── Matador heuristics (utility AI) ───────────────────────────────────────
|
||||||
|
# Every mat_decide_interval seconds the matador scores its options — dodge the
|
||||||
|
# pass, close for a strike, retreat, throw the blade, or strut/taunt — and picks
|
||||||
|
# the highest. These weights bias the personality: crank aggression for a reckless
|
||||||
|
# bullfighter, caution for a slippery one. mat_w_commit is a stickiness bonus for
|
||||||
|
# the current choice so it doesn't dither between two near-tied options.
|
||||||
|
_reg_f("Matador", "mat_decide_interval", 0.2, 0.05, 1.0, 0.05)
|
||||||
|
_reg_f("Matador", "mat_w_aggression", 1.1, 0.0, 3.0, 0.05)
|
||||||
|
_reg_f("Matador", "mat_w_caution", 1.0, 0.0, 3.0, 0.05)
|
||||||
|
_reg_f("Matador", "mat_w_flee", 0.8, 0.0, 3.0, 0.05)
|
||||||
|
# Showmanship (strut, circle the bull, taunt) — the supporting matadors lean on
|
||||||
|
# this to spread out and give the lead room, so a whole crowd reads as a bullring,
|
||||||
|
# not a swarm. Crank it for more posturing, drop it for a relentless mob.
|
||||||
|
_reg_f("Matador", "mat_w_showmanship", 1.6, 0.0, 3.0, 0.05)
|
||||||
|
# Weight on hurling the blade. Kept low — a thrown sword is an occasional flourish,
|
||||||
|
# not a barrage; raising this brings back the pelting.
|
||||||
|
_reg_f("Matador", "mat_w_throw", 0.35, 0.0, 3.0, 0.05)
|
||||||
|
# How many matadors press the bull at once. The nearest this-many engage (close,
|
||||||
|
# throw); everyone else keeps their distance and taunts, taking turns as the bull
|
||||||
|
# moves. 1 = strict one-on-one, higher = a bolder pack.
|
||||||
|
_reg_f("Matador", "mat_engage_slots", 2.0, 1.0, 8.0, 1.0)
|
||||||
|
# Preferred distance (m) a taunting matador circles the bull at while waiting its
|
||||||
|
# turn — the showmanship ring the crowd spreads out onto.
|
||||||
|
_reg_f("Matador", "mat_taunt_ring", 7.0, 3.0, 15.0, 0.5)
|
||||||
|
# How long (s) a matador safely out of the action holds a taunt before drifting to
|
||||||
|
# a new spot — much longer than the between-passes idle, so bystanders mostly pose
|
||||||
|
# for the crowd instead of milling about. Actual hold is randomised ±30%.
|
||||||
|
_reg_f("Matador", "mat_taunt_hold", 8.0, 1.0, 20.0, 0.5)
|
||||||
|
# Small so it smooths dithering without locking the matador into idling.
|
||||||
|
_reg_f("Matador", "mat_w_commit", 0.15, 0.0, 2.0, 0.05)
|
||||||
# ── Sword ─────────────────────────────────────────────────────────────────
|
# ── Sword ─────────────────────────────────────────────────────────────────
|
||||||
# Local seating of the sword on its weapon bones (hand grip + hip holster).
|
# Local seating of the sword in the right hand (drawn / fighting).
|
||||||
# Defaults are identity — the bones are oriented in Blender to seat the sword
|
# Defaults are identity — the bone is oriented in Blender to seat the sword
|
||||||
# directly; these exist to fine-tune in-game without re-exporting.
|
# directly; these exist to fine-tune in-game without re-exporting.
|
||||||
_reg_f("Sword", "sword_pos_x", 0.0, -1.0, 1.0, 0.01)
|
_reg_f("Sword", "sword_pos_x", 0.0, -1.0, 1.0, 0.01)
|
||||||
_reg_f("Sword", "sword_pos_y", 0.0, -1.0, 1.0, 0.01)
|
_reg_f("Sword", "sword_pos_y", 0.0, -1.0, 1.0, 0.01)
|
||||||
@@ -130,33 +200,105 @@ func _register_all() -> void:
|
|||||||
_reg_f("Sword", "sword_rot_x", 0.0, -180.0, 180.0, 1.0)
|
_reg_f("Sword", "sword_rot_x", 0.0, -180.0, 180.0, 1.0)
|
||||||
_reg_f("Sword", "sword_rot_y", 0.0, -180.0, 180.0, 1.0)
|
_reg_f("Sword", "sword_rot_y", 0.0, -180.0, 180.0, 1.0)
|
||||||
_reg_f("Sword", "sword_rot_z", 0.0, -180.0, 180.0, 1.0)
|
_reg_f("Sword", "sword_rot_z", 0.0, -180.0, 180.0, 1.0)
|
||||||
# ── Sword throw (matador ranged attack) ──────────────────────────────────
|
# Local seating of the sword in the hip holster (sheathed / wandering).
|
||||||
# Probability of committing to a throw each time the matador becomes eligible;
|
# Independent of the hand grip so each pose can be tuned without affecting the
|
||||||
# kept low so they favour closing in for a melee Attack over throwing.
|
# other. Defaults angle the blade down along the matador's side.
|
||||||
_reg_f("Sword", "mat_throw_chance", 0.15, 0.0, 1.0, 0.05)
|
_reg_f("Sword", "sword_rest_pos_x", 0.0, -1.0, 1.0, 0.01)
|
||||||
# Delay before a fresh sword appears in the holster after one is thrown.
|
_reg_f("Sword", "sword_rest_pos_y", 0.0, -1.0, 1.0, 0.01)
|
||||||
|
_reg_f("Sword", "sword_rest_pos_z", 0.0, -1.0, 1.0, 0.01)
|
||||||
|
_reg_f("Sword", "sword_rest_rot_x", -120.0, -180.0, 180.0, 1.0)
|
||||||
|
_reg_f("Sword", "sword_rest_rot_y", 0.0, -180.0, 180.0, 1.0)
|
||||||
|
_reg_f("Sword", "sword_rest_rot_z", 0.0, -180.0, 180.0, 1.0)
|
||||||
|
# ── Spear throw (matador ranged attack) ──────────────────────────────────
|
||||||
|
# Local seating of the spear in the throwing hand during the wind-up. Tune to
|
||||||
|
# seat the shaft in the grip without re-exporting; flight orientation is derived
|
||||||
|
# from the aim direction at release, not from this pose.
|
||||||
|
_reg_f("Sword", "spear_pos_x", 0.0, -1.0, 1.0, 0.01)
|
||||||
|
_reg_f("Sword", "spear_pos_y", 0.0, -1.0, 1.0, 0.01)
|
||||||
|
_reg_f("Sword", "spear_pos_z", 0.0, -1.0, 1.0, 0.01)
|
||||||
|
_reg_f("Sword", "spear_rot_x", 0.0, -180.0, 180.0, 1.0)
|
||||||
|
_reg_f("Sword", "spear_rot_y", 0.0, -180.0, 180.0, 1.0)
|
||||||
|
_reg_f("Sword", "spear_rot_z", 0.0, -180.0, 180.0, 1.0)
|
||||||
|
# Delay before a fresh sword reappears in the holster if one is ever lost.
|
||||||
_reg_f("Sword", "mat_resword_delay", 2.0, 0.0, 10.0, 0.5)
|
_reg_f("Sword", "mat_resword_delay", 2.0, 0.0, 10.0, 0.5)
|
||||||
_reg_f("Sword", "mat_throw_range", 18.0, 5.0, 40.0)
|
_reg_f("Sword", "mat_throw_range", 18.0, 5.0, 40.0)
|
||||||
_reg_f("Sword", "mat_throw_min_dist", 5.0, 2.0, 15.0, 0.1)
|
_reg_f("Sword", "mat_throw_min_dist", 5.0, 2.0, 15.0, 0.1)
|
||||||
_reg_f("Sword", "mat_throw_windup", 0.7, 0.2, 2.0, 0.05)
|
_reg_f("Sword", "mat_throw_windup", 0.5, 0.2, 2.0, 0.05)
|
||||||
_reg_f("Sword", "mat_throw_release", 0.55, 0.1, 0.95, 0.01)
|
_reg_f("Sword", "mat_throw_release", 0.55, 0.1, 0.95, 0.01)
|
||||||
_reg_f("Sword", "mat_throw_speed", 18.0, 5.0, 60.0)
|
# Horizontal speed of the thrown spear; the vertical launch is solved from this
|
||||||
_reg_f("Sword", "mat_throw_spin", 14.0, 0.0, 40.0)
|
# and the gravity below so the arc lands on the bull. Lower = a higher, slower lob.
|
||||||
|
_reg_f("Sword", "mat_throw_speed", 11.0, 5.0, 60.0)
|
||||||
|
# Gravity scale on the spear (higher = a more pronounced arc) and the vertical aim
|
||||||
|
# offset from the bull origin down onto its low collision body.
|
||||||
|
_reg_f("Sword", "mat_throw_gravity", 1.0, 0.1, 3.0, 0.05)
|
||||||
|
_reg_f("Sword", "mat_throw_aim_y", -0.45,-1.5, 0.5, 0.05)
|
||||||
|
# Aim scatter (± degrees) so thrown blades can be read and dodged on the move.
|
||||||
|
_reg_f("Sword", "mat_throw_spread", 11.0, 0.0, 30.0, 0.5)
|
||||||
_reg_f("Sword", "mat_throw_arm_cock", 70.0,-180.0, 180.0, 1.0)
|
_reg_f("Sword", "mat_throw_arm_cock", 70.0,-180.0, 180.0, 1.0)
|
||||||
_reg_f("Sword", "mat_throw_arm_release", -90.0,-180.0, 180.0, 1.0)
|
_reg_f("Sword", "mat_throw_arm_release", -90.0,-180.0, 180.0, 1.0)
|
||||||
_reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0)
|
_reg_f("Sword", "mat_throw_elbow", 80.0,-180.0, 180.0, 1.0)
|
||||||
# ── Abilities ─────────────────────────────────────────────────────────────
|
# ── Abilities ─────────────────────────────────────────────────────────────
|
||||||
_reg_f("Abilities", "kick_cooldown", 1.5, 0.2, 10.0, 0.1)
|
_reg_f("Abilities", "kick_cooldown", 1.5, 0.2, 10.0, 0.1)
|
||||||
_reg_f("Abilities", "kick_range", 3.5, 0.5, 8.0, 0.1)
|
_reg_f("Abilities", "kick_range", 6.0, 0.5, 8.0, 0.1)
|
||||||
_reg_f("Abilities", "kick_strength", 12.0, 1.0, 30.0)
|
_reg_f("Abilities", "kick_strength", 12.0, 1.0, 30.0)
|
||||||
_reg_f("Abilities", "slam_cooldown", 4.0, 0.5, 15.0, 0.1)
|
_reg_f("Abilities", "slam_cooldown", 4.0, 0.5, 15.0, 0.1)
|
||||||
_reg_f("Abilities", "slam_jump_velocity", 7.0, 2.0, 20.0)
|
_reg_f("Abilities", "slam_jump_velocity", 7.0, 2.0, 20.0)
|
||||||
|
# Descent gravity multiplier — higher = faster crash, less airtime.
|
||||||
|
_reg_f("Abilities", "slam_fall_mult", 3.0, 1.0, 8.0, 0.5)
|
||||||
|
# Upward launch velocity (m/s) given to a slammed matador's ragdoll — pops them
|
||||||
|
# skyward. ~7 ≈ a 2.5 m hop; 0 disables the launch.
|
||||||
|
_reg_f("Abilities", "slam_launch_up", 7.0, 0.0, 15.0, 0.5)
|
||||||
_reg_f("Abilities", "slam_range", 4.0, 1.0, 10.0, 0.1)
|
_reg_f("Abilities", "slam_range", 4.0, 1.0, 10.0, 0.1)
|
||||||
_reg_f("Abilities", "slam_strength", 14.0, 1.0, 30.0)
|
_reg_f("Abilities", "slam_strength", 14.0, 1.0, 30.0)
|
||||||
_reg_f("Abilities", "dash_cooldown", 2.0, 0.5, 10.0, 0.1)
|
_reg_f("Abilities", "dash_cooldown", 0.5, 0.1, 10.0, 0.1)
|
||||||
_reg_f("Abilities", "dash_speed", 66.0, 5.0, 200.0)
|
_reg_f("Abilities", "dash_speed", 66.0, 5.0, 200.0)
|
||||||
# ── Debug ─────────────────────────────────────────────────────────────────
|
# Exponential rate the dash bleeds from dash_speed back to cruise (max_speed).
|
||||||
|
# Higher = a snappier bullet that dies off quicker; lower = a longer glide.
|
||||||
|
_reg_f("Abilities", "dash_decel", 7.0, 0.5, 30.0, 0.5)
|
||||||
|
# ── Roll (homing chain ball) ──────────────────────────────────────────────
|
||||||
|
# The bull curls into a ball and ACCELERATES the longer it rolls, from
|
||||||
|
# roll_base_speed up to roll_max_speed over roll_rampup_time. It HOMES on the
|
||||||
|
# closest matador; on slamming one the ball POPS it (launched skyward via
|
||||||
|
# roll_knockup) then CHAINS to the next closest, up to roll_chain_count times.
|
||||||
|
# roll_turn is the homing turn rate. When no chain targets remain, the roll ends.
|
||||||
|
_reg_f("Roll", "roll_cooldown", 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_chain_count", 2.0, 0.0, 8.0, 1.0) # extra matadors chained after the first
|
||||||
|
_reg_f("Roll", "roll_base_speed", 28.0, 5.0, 120.0) # speed at the start of the roll
|
||||||
|
_reg_f("Roll", "roll_max_speed", 95.0, 10.0, 200.0) # top speed once fully ramped
|
||||||
|
_reg_f("Roll", "roll_rampup_time", 2.0, 0.2, 6.0, 0.1) # time to reach top speed
|
||||||
|
_reg_f("Roll", "roll_turn", 7.0, 0.2, 20.0) # homing turn rate (higher = tighter tracking)
|
||||||
|
_reg_f("Roll", "roll_spin_radius", 1.4, 0.3, 4.0, 0.1) # ball radius for tumble spin (smaller = faster)
|
||||||
|
_reg_f("Roll", "roll_hit_radius", 2.4, 0.5, 6.0, 0.1) # contact radius that pops the ball
|
||||||
|
_reg_f("Roll", "roll_hit_strength", 22.0, 1.0, 40.0) # horizontal launch on the pop
|
||||||
|
_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_collisions", false)
|
||||||
|
# PS1 post-process filter (see ps1_filter.gd on the fullscreen ColorRect):
|
||||||
|
# screen-covering CRT/PS1 shader — pixelation, colour-banding + dither, and a
|
||||||
|
# rolling scanline. Keys map 1:1 onto the shader uniforms; show_ps1 toggles the
|
||||||
|
# whole overlay. Ranges mirror the shader's hint_range so the console can't push
|
||||||
|
# a uniform out of bounds.
|
||||||
|
_reg_b("PS1", "show_ps1", true)
|
||||||
|
_reg_f("PS1", "ps1_pixel_scale", 4.0, 1.0, 8.0, 1.0)
|
||||||
|
_reg_f("PS1", "ps1_color_depth", 5.0, 2.0, 8.0, 1.0)
|
||||||
|
_reg_f("PS1", "ps1_dither_strength", 0.8, 0.0, 2.0, 0.05)
|
||||||
|
_reg_f("PS1", "ps1_scanline_strength", 0.2, 0.0, 1.0, 0.05)
|
||||||
|
_reg_f("PS1", "ps1_roll_speed", 3.0, 0.0, 10.0, 0.1)
|
||||||
|
_reg_f("PS1", "ps1_roll_strength", 1.0, 0.0, 1.0, 0.05)
|
||||||
|
_reg_b("Debug", "show_hitboxes", false)
|
||||||
|
_reg_b("Debug", "show_bones", false)
|
||||||
|
_reg_b("Debug", "show_raycasts", false)
|
||||||
|
_reg_b("Debug", "show_tail", false)
|
||||||
|
_reg_b("Debug", "show_stats", false)
|
||||||
|
_reg_b("Debug", "show_states", false)
|
||||||
|
_reg_b("Debug", "show_animation_name", false)
|
||||||
|
_reg_b("Debug", "show_wireframe", false)
|
||||||
|
_reg_b("Debug", "show_grid", false)
|
||||||
|
_reg_b("Debug", "show_axes", false)
|
||||||
|
# Force the on-screen touch controls (joystick + ability buttons) on even on a
|
||||||
|
# desktop build — otherwise they appear only when a touchscreen is detected.
|
||||||
|
_reg_b("Touch", "force_touch_controls", false)
|
||||||
|
|
||||||
|
|
||||||
func _reg_f(section: String, key: String, default: float,
|
func _reg_f(section: String, key: String, default: float,
|
||||||
@@ -170,6 +312,7 @@ func _reg_f(section: String, key: String, default: float,
|
|||||||
"max": max_val,
|
"max": max_val,
|
||||||
"step": step,
|
"step": step,
|
||||||
}
|
}
|
||||||
|
_values[key] = default
|
||||||
|
|
||||||
|
|
||||||
func _reg_b(section: String, key: String, default: bool) -> void:
|
func _reg_b(section: String, key: String, default: bool) -> void:
|
||||||
@@ -179,26 +322,28 @@ func _reg_b(section: String, key: String, default: bool) -> void:
|
|||||||
"section": section,
|
"section": section,
|
||||||
"type": TYPE_BOOL,
|
"type": TYPE_BOOL,
|
||||||
}
|
}
|
||||||
|
_values[key] = default
|
||||||
|
|
||||||
|
|
||||||
## Read a float param. Panics on unknown key — intentional: typos should be loud.
|
## Read a float param. Panics on unknown key — intentional: typos should be loud.
|
||||||
func f(key: String) -> float:
|
func f(key: String) -> float:
|
||||||
return _params[key]["value"] as float
|
return _values[key] as float
|
||||||
|
|
||||||
|
|
||||||
## Read a bool param.
|
## Read a bool param.
|
||||||
func b(key: String) -> bool:
|
func b(key: String) -> bool:
|
||||||
return _params[key]["value"] as bool
|
return _values[key] as bool
|
||||||
|
|
||||||
|
|
||||||
func set_value(key: String, value: Variant) -> void:
|
func set_value(key: String, value: Variant) -> void:
|
||||||
if not _params.has(key):
|
if not _params.has(key):
|
||||||
return
|
return
|
||||||
_params[key]["value"] = value
|
_params[key]["value"] = value
|
||||||
|
_values[key] = value
|
||||||
any_changed.emit(key, value)
|
any_changed.emit(key, value)
|
||||||
|
|
||||||
|
|
||||||
## Returns the full metadata dict — used by DebugMenu to build UI.
|
## Returns the full metadata dict — used by Console to build UI.
|
||||||
func get_all() -> Dictionary:
|
func get_all() -> Dictionary:
|
||||||
return _params
|
return _params
|
||||||
|
|
||||||
@@ -224,9 +369,27 @@ func load_saved() -> void:
|
|||||||
if p["type"] == TYPE_FLOAT:
|
if p["type"] == TYPE_FLOAT:
|
||||||
val = clampf(val as float, p["min"] as float, p["max"] as float)
|
val = clampf(val as float, p["min"] as float, p["max"] as float)
|
||||||
_params[key]["value"] = val
|
_params[key]["value"] = val
|
||||||
|
_values[key] = val
|
||||||
|
|
||||||
|
|
||||||
func reset_all() -> void:
|
func reset_all() -> void:
|
||||||
for key: String in _params:
|
for key: String in _params:
|
||||||
_params[key]["value"] = _params[key]["default"]
|
var default: Variant = _params[key]["default"]
|
||||||
|
_params[key]["value"] = default
|
||||||
|
_values[key] = default
|
||||||
any_changed.emit("__all__", null)
|
any_changed.emit("__all__", null)
|
||||||
|
|
||||||
|
|
||||||
|
## Reset a single param to its default. No-op on unknown key.
|
||||||
|
func reset(key: String) -> void:
|
||||||
|
if not _params.has(key):
|
||||||
|
return
|
||||||
|
var default: Variant = _params[key]["default"]
|
||||||
|
_params[key]["value"] = default
|
||||||
|
_values[key] = default
|
||||||
|
any_changed.emit(key, default)
|
||||||
|
|
||||||
|
|
||||||
|
## True when the param currently differs from its registered default.
|
||||||
|
func is_modified(key: String) -> bool:
|
||||||
|
return _params.has(key) and _params[key]["value"] != _params[key]["default"]
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Run from the project root regardless of where the script is invoked.
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
SERVER_USER="richard"
|
SERVER_USER="richard"
|
||||||
SERVER_HOST="192.168.1.168"
|
SERVER_HOST="192.168.1.168"
|
||||||
SERVER_PATH="/opt/bulloseum/data"
|
SERVER_PATH="/opt/bulloseum/data"
|
||||||
|
|
||||||
BUILD_DIR="build/web"
|
BUILD_DIR="build/web"
|
||||||
|
|
||||||
|
# Derive the version the same way GameVersion does (single source of truth):
|
||||||
|
# numeric version from project.godot, pre-release channel from game_version.gd.
|
||||||
|
VERSION_NUMBER="$(grep -oP 'config/version="\K[^"]+' project.godot)"
|
||||||
|
CHANNEL="$(grep -oP 'const CHANNEL: String = "\K[^"]*' game_version.gd || true)"
|
||||||
|
VERSION="v${VERSION_NUMBER}"
|
||||||
|
[ -n "$CHANNEL" ] && VERSION="${VERSION}-${CHANNEL}"
|
||||||
|
|
||||||
|
GIT_COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||||
|
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
|
||||||
|
echo "building Bullosseum ${VERSION} (${GIT_COMMIT})"
|
||||||
|
|
||||||
mkdir -p "$BUILD_DIR"
|
mkdir -p "$BUILD_DIR"
|
||||||
godot --headless --export-release "Web" "$BUILD_DIR/index.html"
|
godot --headless --export-release "Web" "$BUILD_DIR/index.html"
|
||||||
|
|
||||||
gzip -kf "$BUILD_DIR/index.wasm"
|
gzip -kf "$BUILD_DIR/index.wasm"
|
||||||
|
|
||||||
|
# Version stamp shipped alongside the flat build so the server records what's live.
|
||||||
|
cat > "$BUILD_DIR/version.txt" <<EOF
|
||||||
|
version=${VERSION}
|
||||||
|
commit=${GIT_COMMIT}
|
||||||
|
built=${BUILD_DATE}
|
||||||
|
EOF
|
||||||
|
|
||||||
scp "$BUILD_DIR/index.html" \
|
scp "$BUILD_DIR/index.html" \
|
||||||
"$BUILD_DIR/index.js" \
|
"$BUILD_DIR/index.js" \
|
||||||
"$BUILD_DIR/index.wasm.gz" \
|
"$BUILD_DIR/index.wasm.gz" \
|
||||||
@@ -21,6 +43,7 @@ scp "$BUILD_DIR/index.html" \
|
|||||||
"$BUILD_DIR/index.icon.png" \
|
"$BUILD_DIR/index.icon.png" \
|
||||||
"$BUILD_DIR/index.apple-touch-icon.png" \
|
"$BUILD_DIR/index.apple-touch-icon.png" \
|
||||||
"$BUILD_DIR/index.png" \
|
"$BUILD_DIR/index.png" \
|
||||||
|
"$BUILD_DIR/version.txt" \
|
||||||
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
|
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
|
||||||
|
|
||||||
echo "deployed to $SERVER_HOST:$SERVER_PATH"
|
echo "deployed ${VERSION} to $SERVER_HOST:$SERVER_PATH"
|
||||||
|
|||||||
@@ -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={}
|
||||||
@@ -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={}
|
||||||
@@ -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={}
|
||||||
@@ -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.
|
||||||
@@ -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]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://f4bvu1mnvpiu
|
||||||
@@ -1,171 +1,510 @@
|
|||||||
extends CanvasLayer
|
extends CanvasLayer
|
||||||
|
|
||||||
const _GOLD := Color(1.00, 0.78, 0.22)
|
const _GOLD := Color(1.00, 0.78, 0.22)
|
||||||
const _CREAM := Color(0.88, 0.83, 0.72)
|
const _CREAM := Color(0.93, 0.88, 0.72)
|
||||||
const _MUTED := Color(0.65, 0.60, 0.48)
|
const _MUTED := Color(0.66, 0.60, 0.46)
|
||||||
const _BG := Color(0.06, 0.04, 0.02, 0.78)
|
|
||||||
const _BADGE := Color(0.16, 0.10, 0.03, 0.90)
|
# Per-ability trim colour, tying each tile (and its key cap) to that ability's FX.
|
||||||
const _BORDER := Color(0.65, 0.48, 0.15, 0.55)
|
# Order matches _ABILITY_ICONS: kick, dash, slam, roll.
|
||||||
|
const _ABILITY_ACCENT: Array[Color] = [
|
||||||
|
Color(1.00, 0.62, 0.16), # Kick — ember orange
|
||||||
|
Color(0.45, 0.75, 1.00), # Dash — sky blue
|
||||||
|
Color(0.98, 0.34, 0.20), # Slam — crimson
|
||||||
|
Color(1.00, 0.82, 0.32), # Roll — gold
|
||||||
|
]
|
||||||
|
|
||||||
const _ABILITY_ICONS: Array[String] = [
|
const _ABILITY_ICONS: Array[String] = [
|
||||||
"res://HUD/HUD_0000_ability_JALG.png",
|
"res://HUD/KICK.png",
|
||||||
"res://HUD/HUD_0001_ability_SLAM.png",
|
"res://HUD/DASH.png",
|
||||||
"res://HUD/HUD_0002_ability_DASH.png",
|
"res://HUD/SLAM.png",
|
||||||
|
"res://HUD/ROLL.png",
|
||||||
]
|
]
|
||||||
const _ABILITY_KEYS: Array[String] = ["F", "G", "H"]
|
const _ABILITY_ACTIONS: Array[StringName] = [
|
||||||
|
&"ability_kick", &"ability_dash", &"ability_slam", &"ability_roll",
|
||||||
|
]
|
||||||
|
# Fallback label shown in a slot whose icon file fails to load.
|
||||||
|
const _ABILITY_NAMES: Array[String] = ["Kick", "Dash", "Slam", "Roll"]
|
||||||
|
|
||||||
var _controls_visible: bool = true
|
var _controls_visible: bool = false
|
||||||
var _controls_panel: PanelContainer
|
var _controls_panel: PanelContainer
|
||||||
var _toggle_btn: Button
|
var _toggle_btn: Button
|
||||||
|
var _tab_hint: Label
|
||||||
var _spawner: Node
|
var _spawner: Node
|
||||||
var _player: Node = null
|
var _player: Node = null
|
||||||
|
var _died_connected: bool = false
|
||||||
var _cd_overlays: Array[ColorRect] = []
|
var _cd_overlays: Array[ColorRect] = []
|
||||||
var _health_segments: Array[ColorRect] = []
|
var _cd_labels: Array[Label] = []
|
||||||
var _health_connected: bool = false
|
var _hp_label: Label
|
||||||
|
var _hp_segments: HBoxContainer
|
||||||
|
var _hp_seg_fills: Array[ColorRect] = []
|
||||||
|
var _bottom_col: VBoxContainer
|
||||||
|
var _ability_bar: HBoxContainer
|
||||||
|
var _ability_panels: Array[PanelContainer] = []
|
||||||
|
var _bull_bg: TextureRect
|
||||||
|
|
||||||
|
# Red damage vignette — a full-screen shader ColorRect flashed on every hit.
|
||||||
|
const _VIGNETTE_SHADER := "res://damage_vignette.gdshader"
|
||||||
|
var _vignette: ColorRect
|
||||||
|
var _vignette_mat: ShaderMaterial
|
||||||
|
|
||||||
|
# Bull-head backdrop for the gameplay HUD. The image is a wide horned silhouette; its
|
||||||
|
# solid "face" occupies the centred middle band, with horns flaring above. We size the
|
||||||
|
# image so that face band wraps the ability+HP cluster (horns crowning it above).
|
||||||
|
const _BULL_BG := "res://Assets/bull_hud_bg.png"
|
||||||
|
const _BULL_FACE_W: float = 0.529 # face width as a fraction of image width
|
||||||
|
const _BULL_ASPECT: float = 0.2741 # image height / width (267 / 974)
|
||||||
|
const _BULL_FACE_PAD: float = 1.15 # breathing room around the cluster within the face
|
||||||
|
|
||||||
|
# Win/lose screen. _game_over latches so the result is shown once; _result_win
|
||||||
|
# records which way it went (read by the gameplay test).
|
||||||
|
const _WIN_VIDEO := "res://videos/bull_win.ogv" # the bull triumphs
|
||||||
|
const _LOSS_VIDEO := "res://videos/matador_win.ogv" # the matador triumphs
|
||||||
|
|
||||||
|
# Beat between the run ending and the outcome screen. A loss is a short freeze on the
|
||||||
|
# dead bull; a win gives the player a longer stretch to strut a victory lap around the
|
||||||
|
# arena before the screen (and the pause) takes over.
|
||||||
|
const _LOSS_DELAY := 2.0
|
||||||
|
const _WIN_DELAY := 3.0
|
||||||
|
|
||||||
|
var _game_over: bool = false
|
||||||
|
var _result_win: bool = false
|
||||||
|
var _over_root: Control
|
||||||
|
var _over_video: VideoStreamPlayer
|
||||||
|
var _over_label: Label
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
layer = 11
|
layer = 11
|
||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
_build_controls_panel()
|
_build_controls_panel()
|
||||||
|
_build_tab_hint()
|
||||||
|
_build_bottom_hud()
|
||||||
|
_build_bull_hud_bg()
|
||||||
_build_ability_bar()
|
_build_ability_bar()
|
||||||
_build_health_bar()
|
_build_health()
|
||||||
|
_build_version_label()
|
||||||
|
_build_damage_vignette()
|
||||||
|
_build_game_over()
|
||||||
|
_build_touch_controls()
|
||||||
|
_update_control_hints()
|
||||||
_find_spawner.call_deferred()
|
_find_spawner.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
# On-screen joystick + ability buttons for touch devices. Self-gating: the overlay
|
||||||
|
# only shows itself when a touchscreen is present (or DP force_touch_controls is on),
|
||||||
|
# so this is a no-op on desktop.
|
||||||
|
func _build_touch_controls() -> void:
|
||||||
|
add_child((load("res://touch_controls.gd") as Script).new())
|
||||||
|
|
||||||
|
|
||||||
|
# Bottom-centre column that groups the gameplay HUD (abilities on top, bull health
|
||||||
|
# directly beneath) so both read as one cluster instead of scattered corners.
|
||||||
|
func _build_bottom_hud() -> void:
|
||||||
|
_bottom_col = VBoxContainer.new()
|
||||||
|
_bottom_col.add_theme_constant_override("separation", 8)
|
||||||
|
_bottom_col.alignment = BoxContainer.ALIGNMENT_END
|
||||||
|
# Collapsed horizontal anchors so the column shrinks to its content and stays
|
||||||
|
# bottom-centred; the bull backdrop is sized against this content rect.
|
||||||
|
_bottom_col.anchor_left = 0.5
|
||||||
|
_bottom_col.anchor_right = 0.5
|
||||||
|
_bottom_col.anchor_top = 1.0
|
||||||
|
_bottom_col.anchor_bottom = 1.0
|
||||||
|
_bottom_col.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||||
|
_bottom_col.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||||
|
_bottom_col.offset_bottom = -16.0
|
||||||
|
_bottom_col.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(_bottom_col)
|
||||||
|
|
||||||
|
|
||||||
|
# Bull-head plate that sits behind the ability + health cluster. It's a sibling of the
|
||||||
|
# cluster (not a parent) so it never constrains the cluster's layout; we size and place
|
||||||
|
# it against the cluster's rect in _position_bull_bg.
|
||||||
|
func _build_bull_hud_bg() -> void:
|
||||||
|
_bull_bg = TextureRect.new()
|
||||||
|
if ResourceLoader.exists(_BULL_BG):
|
||||||
|
_bull_bg.texture = load(_BULL_BG)
|
||||||
|
# We size the rect ourselves to the image's aspect, so STRETCH_SCALE fills it
|
||||||
|
# exactly with no letterboxing.
|
||||||
|
_bull_bg.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
_bull_bg.stretch_mode = TextureRect.STRETCH_SCALE
|
||||||
|
_bull_bg.modulate.a = 0.95
|
||||||
|
_bull_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(_bull_bg)
|
||||||
|
# Draw the plate behind the cluster.
|
||||||
|
move_child(_bull_bg, _bottom_col.get_index())
|
||||||
|
|
||||||
|
_bottom_col.resized.connect(_position_bull_bg)
|
||||||
|
get_viewport().size_changed.connect(_position_bull_bg)
|
||||||
|
_position_bull_bg.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
# Wrap the bull's solid "face" band around the current cluster rect: scale the image up
|
||||||
|
# so its face spans the cluster (plus padding), centre it horizontally on the cluster,
|
||||||
|
# and pin its bottom edge to the bottom of the screen so the plate is always seated flush
|
||||||
|
# against the screen edge on any resolution (horns crowning the cluster above).
|
||||||
|
func _position_bull_bg() -> void:
|
||||||
|
if _bull_bg == null or _bottom_col == null:
|
||||||
|
return
|
||||||
|
var r := _bottom_col.get_global_rect()
|
||||||
|
if r.size.x <= 1.0:
|
||||||
|
return
|
||||||
|
var vp_h := get_viewport().get_visible_rect().size.y
|
||||||
|
var bw := (r.size.x * _BULL_FACE_PAD) / _BULL_FACE_W
|
||||||
|
var bh := bw * _BULL_ASPECT
|
||||||
|
_bull_bg.size = Vector2(bw, bh)
|
||||||
|
_bull_bg.position = Vector2(
|
||||||
|
r.position.x + r.size.x * 0.5 - bw * 0.5,
|
||||||
|
vp_h - bh)
|
||||||
|
|
||||||
|
|
||||||
|
# Unobtrusive early-build tag in the bottom-right corner.
|
||||||
|
func _build_version_label() -> void:
|
||||||
|
var lbl := Label.new()
|
||||||
|
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:
|
func _process(_delta: float) -> void:
|
||||||
|
_update_control_hints()
|
||||||
if _player == null:
|
if _player == null:
|
||||||
var players := get_tree().get_nodes_in_group(&"player")
|
var players := get_tree().get_nodes_in_group(&"player")
|
||||||
if not players.is_empty():
|
if not players.is_empty():
|
||||||
_player = players[0]
|
_player = players[0]
|
||||||
return
|
return
|
||||||
if not _health_connected:
|
if not _died_connected:
|
||||||
_health_connected = true
|
_died_connected = true
|
||||||
|
_player.died.connect(_on_player_died)
|
||||||
_player.health_changed.connect(_on_health_changed)
|
_player.health_changed.connect(_on_health_changed)
|
||||||
_on_health_changed(_player.health)
|
_player.hit_taken.connect(_on_hit_taken)
|
||||||
|
_on_health_changed(_player.call(&"get_hp"), _player.call(&"get_max_hp"))
|
||||||
for i: int in _cd_overlays.size():
|
for i: int in _cd_overlays.size():
|
||||||
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
|
var fraction: float = _player.call(&"ability_cooldown_fraction", i)
|
||||||
var ov := _cd_overlays[i]
|
var ov := _cd_overlays[i]
|
||||||
ov.anchor_top = 1.0 - fraction
|
ov.anchor_top = 1.0 - fraction
|
||||||
ov.anchor_bottom = 1.0
|
ov.anchor_bottom = 1.0
|
||||||
|
var remaining: float = _player.call(&"ability_cooldown_remaining", i)
|
||||||
|
var lbl := _cd_labels[i]
|
||||||
|
if remaining > 0.0:
|
||||||
|
lbl.text = "%.1fs" % remaining
|
||||||
|
lbl.visible = true
|
||||||
|
else:
|
||||||
|
lbl.visible = false
|
||||||
|
|
||||||
|
|
||||||
func _find_spawner() -> void:
|
func _find_spawner() -> void:
|
||||||
var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
|
var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
|
||||||
if not spawners.is_empty():
|
if spawners.is_empty():
|
||||||
|
return
|
||||||
_spawner = spawners[0]
|
_spawner = spawners[0]
|
||||||
|
_spawner.all_defeated.connect(_on_all_defeated)
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.pressed and not event.echo:
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
if event.physical_keycode == KEY_TAB:
|
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
||||||
|
_restart()
|
||||||
|
elif event.physical_keycode == KEY_TAB and not _game_over:
|
||||||
_toggle_controls()
|
_toggle_controls()
|
||||||
|
elif event.physical_keycode == KEY_ESCAPE:
|
||||||
|
_return_to_menu()
|
||||||
|
|
||||||
|
|
||||||
|
func _return_to_menu() -> void:
|
||||||
|
get_tree().paused = false
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
get_tree().change_scene_to_file("res://MainMenu.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
func _restart() -> void:
|
||||||
|
get_tree().paused = false
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
get_tree().reload_current_scene()
|
||||||
|
|
||||||
|
|
||||||
func _toggle_controls() -> void:
|
func _toggle_controls() -> void:
|
||||||
_controls_visible = not _controls_visible
|
_controls_visible = not _controls_visible
|
||||||
_controls_panel.visible = _controls_visible
|
|
||||||
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
|
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
|
||||||
|
_update_control_hints()
|
||||||
|
|
||||||
|
|
||||||
func _on_reset_pressed() -> void:
|
# The Tab hint and the keyboard controls plaque only make sense with a keyboard, so
|
||||||
if get_tree().paused:
|
# they stand down on touch devices — where touch_controls.gd shows the on-screen
|
||||||
get_tree().paused = false
|
# joystick and ability buttons instead. Shares Controls.use_touch_ui() with that
|
||||||
if _spawner:
|
# overlay so exactly one control scheme shows on any given device.
|
||||||
_spawner.reset_spawn()
|
func _update_control_hints() -> void:
|
||||||
|
if _game_over or Controls.use_touch_ui():
|
||||||
|
_controls_panel.visible = false
|
||||||
|
_tab_hint.visible = false
|
||||||
|
else:
|
||||||
|
_controls_panel.visible = _controls_visible
|
||||||
|
_tab_hint.visible = not _controls_visible
|
||||||
|
|
||||||
|
|
||||||
func _build_health_bar() -> void:
|
# ── Damage vignette ───────────────────────────────────────────────────────────
|
||||||
var bar := HBoxContainer.new()
|
# A full-screen ColorRect driven by damage_vignette.gdshader: transparent centre, red
|
||||||
bar.add_theme_constant_override("separation", 4)
|
# toward the edges, alpha ("flash") tweened up on a hit and eased back to nothing. Built
|
||||||
bar.anchor_left = 0.5
|
# before the game-over layer so the outcome screen always draws over it.
|
||||||
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 bg_style := StyleBoxFlat.new()
|
func _build_damage_vignette() -> void:
|
||||||
bg_style.bg_color = _BG
|
_vignette = ColorRect.new()
|
||||||
bg_style.corner_radius_top_left = 4
|
_vignette.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
bg_style.corner_radius_top_right = 4
|
_vignette.color = Color.WHITE # the shader writes COLOR outright; this is just a base
|
||||||
bg_style.corner_radius_bottom_left = 4
|
_vignette.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
bg_style.corner_radius_bottom_right = 4
|
if ResourceLoader.exists(_VIGNETTE_SHADER):
|
||||||
bg_style.border_width_left = 1
|
_vignette_mat = ShaderMaterial.new()
|
||||||
bg_style.border_width_right = 1
|
_vignette_mat.shader = load(_VIGNETTE_SHADER)
|
||||||
bg_style.border_width_top = 1
|
_vignette_mat.set_shader_parameter("flash", 0.0)
|
||||||
bg_style.border_width_bottom = 1
|
_vignette.material = _vignette_mat
|
||||||
bg_style.border_color = _BORDER
|
add_child(_vignette)
|
||||||
bg_style.content_margin_left = 6.0
|
|
||||||
bg_style.content_margin_right = 6.0
|
|
||||||
bg_style.content_margin_top = 6.0
|
|
||||||
bg_style.content_margin_bottom = 6.0
|
|
||||||
|
|
||||||
|
|
||||||
|
func _on_hit_taken(_cause: String) -> void:
|
||||||
|
if _vignette_mat == null:
|
||||||
|
return
|
||||||
|
var set_flash := func(v: float) -> void:
|
||||||
|
_vignette_mat.set_shader_parameter("flash", v)
|
||||||
|
create_tween().tween_method(set_flash, 0.9, 0.0, 0.5)\
|
||||||
|
.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Win / lose ──────────────────────────────────────────────────────────────
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_all_defeated() -> void:
|
||||||
|
_show_game_over(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _show_game_over(win: bool, _cause: String = "") -> void:
|
||||||
|
if _game_over:
|
||||||
|
return
|
||||||
|
_game_over = true
|
||||||
|
_result_win = win
|
||||||
|
|
||||||
|
if _controls_panel:
|
||||||
|
_controls_panel.visible = false
|
||||||
|
if _tab_hint:
|
||||||
|
_tab_hint.visible = false
|
||||||
|
|
||||||
|
await get_tree().create_timer(_WIN_DELAY if win else _LOSS_DELAY, false, false, true).timeout
|
||||||
|
|
||||||
|
_over_label.text = "Bull Won!" if _result_win else "Matadors Won!"
|
||||||
|
|
||||||
|
var path := _WIN_VIDEO if _result_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 _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.
|
||||||
|
# AspectRatioContainer keeps the 16:9 video's ratio and crops overflow (COVER),
|
||||||
|
# so it fills any screen shape without stretching or leaving dead bars.
|
||||||
|
var arc := AspectRatioContainer.new()
|
||||||
|
arc.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
arc.ratio = 16.0 / 9.0
|
||||||
|
arc.stretch_mode = AspectRatioContainer.STRETCH_COVER
|
||||||
|
arc.alignment_horizontal = AspectRatioContainer.ALIGNMENT_CENTER
|
||||||
|
arc.alignment_vertical = AspectRatioContainer.ALIGNMENT_CENTER
|
||||||
|
arc.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
arc.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_over_root.add_child(arc)
|
||||||
|
|
||||||
|
_over_video = VideoStreamPlayer.new()
|
||||||
|
_over_video.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_over_video.expand = true
|
||||||
|
_over_video.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
_over_video.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_over_video.finished.connect(_over_video.play) # loop the outcome clip
|
||||||
|
arc.add_child(_over_video)
|
||||||
|
|
||||||
|
# Outcome text centred over the video.
|
||||||
|
_over_label = Label.new()
|
||||||
|
_over_label.set_anchors_preset(Control.PRESET_CENTER_TOP)
|
||||||
|
_over_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||||
|
_over_label.grow_vertical = Control.GROW_DIRECTION_END
|
||||||
|
_over_label.offset_top = 80.0
|
||||||
|
_over_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_over_label.add_theme_font_override("font", UiFonts.title())
|
||||||
|
_over_label.add_theme_font_size_override("font_size", 64)
|
||||||
|
_over_label.add_theme_color_override("font_color", _GOLD)
|
||||||
|
_over_label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.9))
|
||||||
|
_over_label.add_theme_constant_override("outline_size", 6)
|
||||||
|
_over_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_over_root.add_child(_over_label)
|
||||||
|
|
||||||
|
# Buttons overlaid at the bottom of the clip.
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ── Bull health ───────────────────────────────────────────────────────────────
|
||||||
|
# A row of gold-socketed pips beneath the ability bar — one pip per HP, so raising or
|
||||||
|
# lowering max HP visibly grows or shrinks the row. Lit pips run red and shade toward
|
||||||
|
# crimson as the bull is worn down; spent pips read as empty dark sockets.
|
||||||
|
|
||||||
|
const _HP_FULL: Color = Color(0.82, 0.16, 0.12)
|
||||||
|
const _HP_EMPTY: Color = Color(0.35, 0.05, 0.05)
|
||||||
|
const _HP_SOCKET: Color = Color(0.05, 0.02, 0.02, 0.92)
|
||||||
|
|
||||||
|
const _HP_PIP_W: float = 22.0
|
||||||
|
const _HP_PIP_H: float = 14.0
|
||||||
|
|
||||||
|
|
||||||
|
func _build_health() -> void:
|
||||||
var panel := PanelContainer.new()
|
var panel := PanelContainer.new()
|
||||||
panel.add_theme_stylebox_override("panel", bg_style)
|
panel.add_theme_stylebox_override(
|
||||||
bar.add_child(panel)
|
"panel", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 12.0, 6.0))
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_bottom_col.add_child(panel)
|
||||||
|
|
||||||
var inner := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
inner.add_theme_constant_override("separation", 3)
|
row.add_theme_constant_override("separation", 8)
|
||||||
panel.add_child(inner)
|
panel.add_child(row)
|
||||||
|
|
||||||
for i: int in 10:
|
_hp_label = Label.new()
|
||||||
var seg := ColorRect.new()
|
_hp_label.text = "BULL"
|
||||||
seg.custom_minimum_size = Vector2(14.0, 14.0)
|
_hp_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
seg.color = Color(0.72, 0.08, 0.08)
|
_hp_label.add_theme_font_override("font", UiFonts.body())
|
||||||
inner.add_child(seg)
|
_hp_label.add_theme_color_override("font_color", _GOLD)
|
||||||
_health_segments.append(seg)
|
_hp_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
|
||||||
|
_hp_label.add_theme_constant_override("outline_size", 3)
|
||||||
|
_hp_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
row.add_child(_hp_label)
|
||||||
|
|
||||||
|
_hp_segments = HBoxContainer.new()
|
||||||
|
_hp_segments.add_theme_constant_override("separation", 4)
|
||||||
|
_hp_segments.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
_hp_segments.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
row.add_child(_hp_segments)
|
||||||
|
|
||||||
|
|
||||||
func _on_health_changed(new_health: int) -> void:
|
# The gold-underlined socket behind each pip — shared, non-mutating, so one instance
|
||||||
for i: int in _health_segments.size():
|
# is safe across every pip.
|
||||||
_health_segments[i].color = Color(0.72, 0.08, 0.08) if i < new_health \
|
func _hp_socket_style() -> StyleBoxFlat:
|
||||||
else Color(0.20, 0.10, 0.10)
|
var s := StyleBoxFlat.new()
|
||||||
|
s.bg_color = _HP_SOCKET
|
||||||
|
s.set_corner_radius_all(3)
|
||||||
|
s.corner_detail = 4
|
||||||
|
s.border_width_bottom = 2
|
||||||
|
s.border_color = Color(UiTheme.GOLD.r, UiTheme.GOLD.g, UiTheme.GOLD.b, 0.45)
|
||||||
|
s.content_margin_left = 2.0
|
||||||
|
s.content_margin_right = 2.0
|
||||||
|
s.content_margin_top = 2.0
|
||||||
|
s.content_margin_bottom = 2.0
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild_hp_pips(max_hp: int) -> void:
|
||||||
|
for child in _hp_segments.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
_hp_seg_fills.clear()
|
||||||
|
var socket_style := _hp_socket_style()
|
||||||
|
for _i: int in maxi(max_hp, 0):
|
||||||
|
var socket := PanelContainer.new()
|
||||||
|
socket.custom_minimum_size = Vector2(_HP_PIP_W, _HP_PIP_H)
|
||||||
|
socket.clip_contents = true
|
||||||
|
socket.add_theme_stylebox_override("panel", socket_style)
|
||||||
|
socket.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_hp_segments.add_child(socket)
|
||||||
|
|
||||||
|
var fill := ColorRect.new()
|
||||||
|
fill.color = _HP_FULL
|
||||||
|
fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
socket.add_child(fill)
|
||||||
|
_hp_seg_fills.append(fill)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_health_changed(current: int, max_hp: int) -> void:
|
||||||
|
if _hp_segments == null:
|
||||||
|
return
|
||||||
|
if _hp_seg_fills.size() != max_hp:
|
||||||
|
_rebuild_hp_pips(max_hp)
|
||||||
|
var cur := maxi(current, 0)
|
||||||
|
var frac := 0.0 if max_hp <= 0 else clampf(float(cur) / float(max_hp), 0.0, 1.0)
|
||||||
|
var lit := _HP_FULL.lerp(_HP_EMPTY, 1.0 - frac)
|
||||||
|
for i: int in _hp_seg_fills.size():
|
||||||
|
var fill := _hp_seg_fills[i]
|
||||||
|
fill.visible = i < cur
|
||||||
|
fill.color = lit
|
||||||
|
|
||||||
|
|
||||||
|
const _SLOT_SEPARATION: int = 10
|
||||||
|
|
||||||
|
|
||||||
func _build_ability_bar() -> void:
|
func _build_ability_bar() -> void:
|
||||||
var bar := HBoxContainer.new()
|
_ability_bar = HBoxContainer.new()
|
||||||
bar.add_theme_constant_override("separation", 10)
|
_ability_bar.add_theme_constant_override("separation", _SLOT_SEPARATION)
|
||||||
bar.anchor_left = 0.5
|
_ability_bar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
bar.anchor_right = 0.5
|
_ability_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
bar.anchor_top = 1.0
|
_bottom_col.add_child(_ability_bar)
|
||||||
bar.anchor_bottom = 1.0
|
for i: int in 4:
|
||||||
bar.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
_ability_bar.add_child(_build_ability_slot(i))
|
||||||
bar.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
_layout_ability_bar()
|
||||||
bar.offset_left = -120.0
|
get_viewport().size_changed.connect(_layout_ability_bar)
|
||||||
bar.offset_bottom = -20.0
|
|
||||||
add_child(bar)
|
|
||||||
for i: int in 3:
|
# Size the ability slots as a fraction of the viewport (clamped) so the bar scales
|
||||||
bar.add_child(_build_ability_slot(i))
|
# with the window; the bottom column keeps the bar horizontally centred.
|
||||||
|
func _layout_ability_bar() -> void:
|
||||||
|
var vp := get_viewport().get_visible_rect().size
|
||||||
|
var slot := clampf(minf(vp.x * 0.055, vp.y * 0.11), 56.0, 104.0)
|
||||||
|
for panel: PanelContainer in _ability_panels:
|
||||||
|
panel.custom_minimum_size = Vector2(slot, slot)
|
||||||
|
|
||||||
|
|
||||||
func _build_ability_slot(idx: int) -> Control:
|
func _build_ability_slot(idx: int) -> Control:
|
||||||
var vbox := VBoxContainer.new()
|
var vbox := VBoxContainer.new()
|
||||||
vbox.add_theme_constant_override("separation", 4)
|
vbox.add_theme_constant_override("separation", 3)
|
||||||
|
|
||||||
var bg_style := StyleBoxFlat.new()
|
|
||||||
bg_style.bg_color = _BG
|
|
||||||
bg_style.corner_radius_top_left = 6
|
|
||||||
bg_style.corner_radius_top_right = 6
|
|
||||||
bg_style.corner_radius_bottom_left = 6
|
|
||||||
bg_style.corner_radius_bottom_right = 6
|
|
||||||
bg_style.border_width_left = 1
|
|
||||||
bg_style.border_width_right = 1
|
|
||||||
bg_style.border_width_top = 1
|
|
||||||
bg_style.border_width_bottom = 1
|
|
||||||
bg_style.border_color = _BORDER
|
|
||||||
bg_style.content_margin_left = 4.0
|
|
||||||
bg_style.content_margin_right = 4.0
|
|
||||||
bg_style.content_margin_top = 4.0
|
|
||||||
bg_style.content_margin_bottom = 4.0
|
|
||||||
|
|
||||||
var panel := PanelContainer.new()
|
var panel := PanelContainer.new()
|
||||||
panel.custom_minimum_size = Vector2(72.0, 72.0)
|
panel.custom_minimum_size = Vector2(72.0, 72.0)
|
||||||
panel.add_theme_stylebox_override("panel", bg_style)
|
panel.add_theme_stylebox_override(
|
||||||
|
"panel", UiTheme.plaque(UiTheme.LEATHER, _ABILITY_ACCENT[idx], 4, 5.0, 5.0))
|
||||||
vbox.add_child(panel)
|
vbox.add_child(panel)
|
||||||
|
_ability_panels.append(panel)
|
||||||
|
|
||||||
var stack := Control.new()
|
var stack := Control.new()
|
||||||
stack.set_anchors_preset(Control.PRESET_FULL_RECT)
|
stack.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
@@ -174,9 +513,23 @@ func _build_ability_slot(idx: int) -> Control:
|
|||||||
if ResourceLoader.exists(_ABILITY_ICONS[idx]):
|
if ResourceLoader.exists(_ABILITY_ICONS[idx]):
|
||||||
var icon := TextureRect.new()
|
var icon := TextureRect.new()
|
||||||
icon.texture = load(_ABILITY_ICONS[idx])
|
icon.texture = load(_ABILITY_ICONS[idx])
|
||||||
|
# Ignore the texture's native size so a large source PNG doesn't blow the
|
||||||
|
# slot's minimum size out; scale-to-fit the box while keeping aspect.
|
||||||
|
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
icon.set_anchors_preset(Control.PRESET_FULL_RECT)
|
icon.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
stack.add_child(icon)
|
stack.add_child(icon)
|
||||||
|
else:
|
||||||
|
var name_lbl := Label.new()
|
||||||
|
name_lbl.text = _ABILITY_NAMES[idx]
|
||||||
|
name_lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
name_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
name_lbl.add_theme_color_override("font_color", _GOLD)
|
||||||
|
name_lbl.add_theme_font_size_override("font_size", 16)
|
||||||
|
stack.add_child(name_lbl)
|
||||||
|
|
||||||
var cd_overlay := ColorRect.new()
|
var cd_overlay := ColorRect.new()
|
||||||
cd_overlay.color = Color(0.0, 0.0, 0.0, 0.70)
|
cd_overlay.color = Color(0.0, 0.0, 0.0, 0.70)
|
||||||
@@ -188,35 +541,78 @@ func _build_ability_slot(idx: int) -> Control:
|
|||||||
stack.add_child(cd_overlay)
|
stack.add_child(cd_overlay)
|
||||||
_cd_overlays.append(cd_overlay)
|
_cd_overlays.append(cd_overlay)
|
||||||
|
|
||||||
|
var cd_lbl := Label.new()
|
||||||
|
cd_lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
cd_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
cd_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
cd_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
cd_lbl.visible = false
|
||||||
|
cd_lbl.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
cd_lbl.add_theme_font_size_override("font_size", 18)
|
||||||
|
stack.add_child(cd_lbl)
|
||||||
|
_cd_labels.append(cd_lbl)
|
||||||
|
|
||||||
|
# Keycap tucked directly beneath the tile — a solid dark plate wearing the
|
||||||
|
# ability's accent underline, so it reads as attached to its box without ever
|
||||||
|
# covering the artwork. Centred under the tile and tightly spaced.
|
||||||
|
var keycap := PanelContainer.new()
|
||||||
|
keycap.add_theme_stylebox_override("panel", _keycap_style(idx))
|
||||||
|
keycap.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
keycap.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
vbox.add_child(keycap)
|
||||||
|
|
||||||
var key_lbl := Label.new()
|
var key_lbl := Label.new()
|
||||||
key_lbl.text = _ABILITY_KEYS[idx]
|
key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
|
||||||
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
key_lbl.add_theme_color_override("font_color", _GOLD)
|
key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
key_lbl.add_theme_font_size_override("font_size", 12)
|
key_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
vbox.add_child(key_lbl)
|
key_lbl.add_theme_font_override("font", UiFonts.body_bold())
|
||||||
|
key_lbl.add_theme_color_override("font_color", _CREAM)
|
||||||
|
key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||||
|
key_lbl.add_theme_constant_override("outline_size", 3)
|
||||||
|
key_lbl.add_theme_font_size_override("font_size", 14)
|
||||||
|
keycap.add_child(key_lbl)
|
||||||
|
|
||||||
return vbox
|
return vbox
|
||||||
|
|
||||||
|
|
||||||
|
# Solid keycap plate for an ability bind: near-black fill with the ability's accent
|
||||||
|
# as a chunky top edge, so the accent bridges up to the tile it sits under (reads as
|
||||||
|
# attached) while the key stays on a high-contrast background against busy scenes.
|
||||||
|
func _keycap_style(idx: int) -> StyleBoxFlat:
|
||||||
|
var s := StyleBoxFlat.new()
|
||||||
|
s.bg_color = Color(0.04, 0.03, 0.02, 0.94)
|
||||||
|
s.set_corner_radius_all(4)
|
||||||
|
s.corner_detail = 4
|
||||||
|
s.set_border_width_all(1)
|
||||||
|
s.border_width_top = 3
|
||||||
|
s.border_color = _ABILITY_ACCENT[idx]
|
||||||
|
s.content_margin_left = 7.0
|
||||||
|
s.content_margin_right = 7.0
|
||||||
|
s.content_margin_top = 2.0
|
||||||
|
s.content_margin_bottom = 1.0
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
func _build_tab_hint() -> void:
|
||||||
|
_tab_hint = Label.new()
|
||||||
|
_tab_hint.text = "Press 'Tab' to see controls"
|
||||||
|
_tab_hint.anchor_left = 0.0
|
||||||
|
_tab_hint.anchor_top = 1.0
|
||||||
|
_tab_hint.anchor_right = 0.0
|
||||||
|
_tab_hint.anchor_bottom = 1.0
|
||||||
|
_tab_hint.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||||
|
_tab_hint.offset_left = 20.0
|
||||||
|
_tab_hint.offset_bottom = -20.0
|
||||||
|
_tab_hint.add_theme_color_override("font_color", _MUTED)
|
||||||
|
_tab_hint.add_theme_font_size_override("font_size", 12)
|
||||||
|
add_child(_tab_hint)
|
||||||
|
|
||||||
|
|
||||||
func _build_controls_panel() -> void:
|
func _build_controls_panel() -> void:
|
||||||
_controls_panel = PanelContainer.new()
|
_controls_panel = PanelContainer.new()
|
||||||
|
_controls_panel.add_theme_stylebox_override(
|
||||||
var bg := StyleBoxFlat.new()
|
"panel", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 16.0, 12.0))
|
||||||
bg.bg_color = _BG
|
|
||||||
bg.corner_radius_top_left = 7
|
|
||||||
bg.corner_radius_top_right = 7
|
|
||||||
bg.corner_radius_bottom_left = 7
|
|
||||||
bg.corner_radius_bottom_right = 7
|
|
||||||
bg.border_width_left = 1
|
|
||||||
bg.border_width_right = 1
|
|
||||||
bg.border_width_top = 1
|
|
||||||
bg.border_width_bottom = 1
|
|
||||||
bg.border_color = _BORDER
|
|
||||||
bg.content_margin_left = 14.0
|
|
||||||
bg.content_margin_right = 14.0
|
|
||||||
bg.content_margin_top = 10.0
|
|
||||||
bg.content_margin_bottom = 10.0
|
|
||||||
_controls_panel.add_theme_stylebox_override("panel", bg)
|
|
||||||
|
|
||||||
_controls_panel.anchor_left = 0.0
|
_controls_panel.anchor_left = 0.0
|
||||||
_controls_panel.anchor_top = 1.0
|
_controls_panel.anchor_top = 1.0
|
||||||
@@ -225,41 +621,35 @@ func _build_controls_panel() -> void:
|
|||||||
_controls_panel.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
_controls_panel.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||||
_controls_panel.offset_left = 20.0
|
_controls_panel.offset_left = 20.0
|
||||||
_controls_panel.offset_bottom = -20.0
|
_controls_panel.offset_bottom = -20.0
|
||||||
|
_controls_panel.visible = false
|
||||||
add_child(_controls_panel)
|
add_child(_controls_panel)
|
||||||
|
|
||||||
var vbox := VBoxContainer.new()
|
var vbox := VBoxContainer.new()
|
||||||
vbox.add_theme_constant_override("separation", 5)
|
vbox.add_theme_constant_override("separation", 5)
|
||||||
_controls_panel.add_child(vbox)
|
_controls_panel.add_child(vbox)
|
||||||
|
|
||||||
_row(vbox, "WASD / Arrows", "Move (not intended)", _MUTED)
|
var move_keys := "%s %s %s %s" % [
|
||||||
_row(vbox, "LMB (hold)", "Right horn", _CREAM)
|
Controls.get_key_label(&"move_forward"),
|
||||||
_row(vbox, "RMB (hold)", "Left horn", _CREAM)
|
Controls.get_key_label(&"move_left"),
|
||||||
_row(vbox, "F", "Kick", _CREAM)
|
Controls.get_key_label(&"move_back"),
|
||||||
_row(vbox, "G", "Slam", _CREAM)
|
Controls.get_key_label(&"move_right"),
|
||||||
_row(vbox, "H", "Dash", _CREAM)
|
]
|
||||||
|
_row(vbox, move_keys, "Move", _CREAM)
|
||||||
|
_row(vbox, Controls.get_key_label(&"ability_kick"), "Kick", _CREAM)
|
||||||
|
_row(vbox, Controls.get_key_label(&"ability_dash"), "Dash", _CREAM)
|
||||||
|
_row(vbox, Controls.get_key_label(&"ability_slam"), "Slam", _CREAM)
|
||||||
|
_row(vbox, Controls.get_key_label(&"ability_roll"), "Roll (Powerball: ram the matador!)", _CREAM)
|
||||||
_row(vbox, "Scroll", "Zoom", _CREAM)
|
_row(vbox, "Scroll", "Zoom", _CREAM)
|
||||||
_row(vbox, "R", "Respawn matadors (cooldown)", _MUTED)
|
|
||||||
_row(vbox, "Tab", "Toggle controls", _MUTED)
|
_row(vbox, "Tab", "Toggle controls", _MUTED)
|
||||||
|
_row(vbox, "Esc", "Back to menu", _MUTED)
|
||||||
|
|
||||||
var sep := HSeparator.new()
|
var sep := HSeparator.new()
|
||||||
var sep_style := StyleBoxFlat.new()
|
sep.add_theme_stylebox_override("separator", UiTheme.rule())
|
||||||
sep_style.bg_color = Color(0.65, 0.48, 0.15, 0.30)
|
|
||||||
sep_style.content_margin_top = 4.0
|
|
||||||
sep_style.content_margin_bottom = 4.0
|
|
||||||
sep.add_theme_stylebox_override("separator", sep_style)
|
|
||||||
vbox.add_child(sep)
|
vbox.add_child(sep)
|
||||||
|
|
||||||
var hbox := HBoxContainer.new()
|
_toggle_btn = _make_button("Show controls")
|
||||||
hbox.add_theme_constant_override("separation", 8)
|
|
||||||
vbox.add_child(hbox)
|
|
||||||
|
|
||||||
_toggle_btn = _make_button("Hide controls")
|
|
||||||
_toggle_btn.pressed.connect(_toggle_controls)
|
_toggle_btn.pressed.connect(_toggle_controls)
|
||||||
hbox.add_child(_toggle_btn)
|
vbox.add_child(_toggle_btn)
|
||||||
|
|
||||||
var reset_btn := _make_button("Reset")
|
|
||||||
reset_btn.pressed.connect(_on_reset_pressed)
|
|
||||||
hbox.add_child(reset_btn)
|
|
||||||
|
|
||||||
|
|
||||||
func _make_button(label_text: String) -> Button:
|
func _make_button(label_text: String) -> Button:
|
||||||
@@ -267,44 +657,17 @@ func _make_button(label_text: String) -> Button:
|
|||||||
btn.text = label_text
|
btn.text = label_text
|
||||||
btn.process_mode = Node.PROCESS_MODE_ALWAYS
|
btn.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
|
||||||
var normal := StyleBoxFlat.new()
|
var normal := UiTheme.plaque(Color(0.10, 0.07, 0.03, 0.90), _GOLD, 3, 18.0, 9.0)
|
||||||
normal.bg_color = Color(0.18, 0.12, 0.04, 0.90)
|
var hover := UiTheme.plaque(Color(0.16, 0.10, 0.04, 0.96), _CREAM, 3, 18.0, 9.0)
|
||||||
normal.corner_radius_top_left = 5
|
|
||||||
normal.corner_radius_top_right = 5
|
|
||||||
normal.corner_radius_bottom_left = 5
|
|
||||||
normal.corner_radius_bottom_right = 5
|
|
||||||
normal.border_width_left = 1
|
|
||||||
normal.border_width_right = 1
|
|
||||||
normal.border_width_top = 1
|
|
||||||
normal.border_width_bottom = 1
|
|
||||||
normal.border_color = _BORDER
|
|
||||||
normal.content_margin_left = 12.0
|
|
||||||
normal.content_margin_right = 12.0
|
|
||||||
normal.content_margin_top = 6.0
|
|
||||||
normal.content_margin_bottom = 6.0
|
|
||||||
|
|
||||||
var hover := StyleBoxFlat.new()
|
|
||||||
hover.bg_color = Color(0.30, 0.20, 0.06, 0.95)
|
|
||||||
hover.corner_radius_top_left = 5
|
|
||||||
hover.corner_radius_top_right = 5
|
|
||||||
hover.corner_radius_bottom_left = 5
|
|
||||||
hover.corner_radius_bottom_right = 5
|
|
||||||
hover.border_width_left = 1
|
|
||||||
hover.border_width_right = 1
|
|
||||||
hover.border_width_top = 1
|
|
||||||
hover.border_width_bottom = 1
|
|
||||||
hover.border_color = _GOLD
|
|
||||||
hover.content_margin_left = 12.0
|
|
||||||
hover.content_margin_right = 12.0
|
|
||||||
hover.content_margin_top = 6.0
|
|
||||||
hover.content_margin_bottom = 6.0
|
|
||||||
|
|
||||||
btn.add_theme_stylebox_override("normal", normal)
|
btn.add_theme_stylebox_override("normal", normal)
|
||||||
btn.add_theme_stylebox_override("hover", hover)
|
btn.add_theme_stylebox_override("hover", hover)
|
||||||
btn.add_theme_stylebox_override("pressed", hover)
|
btn.add_theme_stylebox_override("pressed", hover)
|
||||||
|
btn.add_theme_stylebox_override("focus", hover)
|
||||||
|
btn.add_theme_font_override("font", UiFonts.body())
|
||||||
btn.add_theme_color_override("font_color", _GOLD)
|
btn.add_theme_color_override("font_color", _GOLD)
|
||||||
btn.add_theme_color_override("font_hover_color", _GOLD)
|
btn.add_theme_color_override("font_hover_color", _CREAM)
|
||||||
btn.add_theme_font_size_override("font_size", 12)
|
btn.add_theme_font_size_override("font_size", 18)
|
||||||
|
|
||||||
return btn
|
return btn
|
||||||
|
|
||||||
@@ -315,17 +678,7 @@ func _row(parent: VBoxContainer, key: String, desc: String, desc_color: Color) -
|
|||||||
parent.add_child(row)
|
parent.add_child(row)
|
||||||
|
|
||||||
var badge := PanelContainer.new()
|
var badge := PanelContainer.new()
|
||||||
var bs := StyleBoxFlat.new()
|
badge.add_theme_stylebox_override("panel", UiTheme.badge())
|
||||||
bs.bg_color = _BADGE
|
|
||||||
bs.corner_radius_top_left = 3
|
|
||||||
bs.corner_radius_top_right = 3
|
|
||||||
bs.corner_radius_bottom_left = 3
|
|
||||||
bs.corner_radius_bottom_right = 3
|
|
||||||
bs.content_margin_left = 6.0
|
|
||||||
bs.content_margin_right = 6.0
|
|
||||||
bs.content_margin_top = 2.0
|
|
||||||
bs.content_margin_bottom = 2.0
|
|
||||||
badge.add_theme_stylebox_override("panel", bs)
|
|
||||||
badge.custom_minimum_size = Vector2(100.0, 0.0)
|
badge.custom_minimum_size = Vector2(100.0, 0.0)
|
||||||
row.add_child(badge)
|
row.add_child(badge)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
## Intro menu: Siim's arena animation as a looping video background.
|
||||||
|
## Play / Options / Credits / Exit UI are overlaid; Play follows the cape.
|
||||||
|
|
||||||
|
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
|
||||||
|
const CAPE_POS: Array = [
|
||||||
|
Vector2(0.4530, 0.6513),
|
||||||
|
Vector2(0.4521, 0.6516),
|
||||||
|
Vector2(0.4495, 0.6524),
|
||||||
|
Vector2(0.4456, 0.6537),
|
||||||
|
Vector2(0.4415, 0.6550),
|
||||||
|
Vector2(0.4373, 0.6578),
|
||||||
|
Vector2(0.4338, 0.6616),
|
||||||
|
Vector2(0.4306, 0.6669),
|
||||||
|
Vector2(0.4273, 0.6673),
|
||||||
|
Vector2(0.4256, 0.6586),
|
||||||
|
Vector2(0.4258, 0.6428),
|
||||||
|
Vector2(0.4281, 0.6243),
|
||||||
|
Vector2(0.4324, 0.6086),
|
||||||
|
Vector2(0.4382, 0.5997),
|
||||||
|
Vector2(0.4439, 0.5972),
|
||||||
|
Vector2(0.4483, 0.5970),
|
||||||
|
Vector2(0.4512, 0.5986),
|
||||||
|
Vector2(0.4529, 0.6025),
|
||||||
|
Vector2(0.4535, 0.6078),
|
||||||
|
Vector2(0.4533, 0.6135),
|
||||||
|
Vector2(0.4539, 0.6159),
|
||||||
|
Vector2(0.4545, 0.6120),
|
||||||
|
Vector2(0.4546, 0.6050),
|
||||||
|
Vector2(0.4541, 0.5988),
|
||||||
|
Vector2(0.4528, 0.5961),
|
||||||
|
Vector2(0.4508, 0.6012),
|
||||||
|
Vector2(0.4485, 0.6123),
|
||||||
|
Vector2(0.4466, 0.6256),
|
||||||
|
Vector2(0.4456, 0.6388),
|
||||||
|
Vector2(0.4457, 0.6491),
|
||||||
|
Vector2(0.4473, 0.6552),
|
||||||
|
Vector2(0.4492, 0.6567),
|
||||||
|
Vector2(0.4513, 0.6556),
|
||||||
|
Vector2(0.4524, 0.6526),
|
||||||
|
]
|
||||||
|
|
||||||
|
## Friendly labels for the rebindable actions (order defines list order).
|
||||||
|
const ACTION_LABELS: Dictionary = {
|
||||||
|
&"move_forward": "Move Forward",
|
||||||
|
&"move_back": "Move Back",
|
||||||
|
&"move_left": "Move Left",
|
||||||
|
&"move_right": "Move Right",
|
||||||
|
&"ability_kick": "Kick",
|
||||||
|
&"ability_dash": "Dash",
|
||||||
|
&"ability_slam": "Slam",
|
||||||
|
&"ability_roll": "Roll",
|
||||||
|
}
|
||||||
|
|
||||||
|
@onready var main_panel: Control = $MainPanel
|
||||||
|
@onready var options_panel: Control = $OptionsPanel
|
||||||
|
@onready var credits_panel: Control = $CreditsPanel
|
||||||
|
@onready var options_vbox: VBoxContainer = $OptionsPanel/VBox
|
||||||
|
@onready var options_scroll: ScrollContainer = $OptionsPanel/VBox/Scroll
|
||||||
|
@onready var rebind_list: VBoxContainer = $OptionsPanel/VBox/Scroll/RebindList
|
||||||
|
@onready var fade_rect: ColorRect = $FadeRect
|
||||||
|
@onready var play_button: Button = $VideoAspect/CapeLayer/PlayButton
|
||||||
|
@onready var options_button: Button = $MainPanel/OptionsButton
|
||||||
|
@onready var credits_button: Button = $MainPanel/CreditsButton
|
||||||
|
@onready var exit_button: Button = $MainPanel/ExitButton
|
||||||
|
@onready var options_back: Button = $OptionsPanel/VBox/BackButton
|
||||||
|
@onready var credits_back: Button = $CreditsPanel/VBox/BackButton
|
||||||
|
@onready var video: VideoStreamPlayer = $VideoAspect/VideoStreamPlayer
|
||||||
|
@onready var blur_video: VideoStreamPlayer = $VideoBlur/BlurVideo
|
||||||
|
|
||||||
|
var _listening_action: StringName = &""
|
||||||
|
var _listening_button: Button = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
options_panel.visible = false
|
||||||
|
credits_panel.visible = false
|
||||||
|
fade_rect.color = Color(0, 0, 0, 0)
|
||||||
|
if video.stream != null:
|
||||||
|
video.play()
|
||||||
|
video.finished.connect(video.play) # loop the background clip
|
||||||
|
if blur_video.stream != null:
|
||||||
|
blur_video.play()
|
||||||
|
blur_video.finished.connect(blur_video.play) # loop the blurred backdrop
|
||||||
|
|
||||||
|
play_button.pressed.connect(_on_play_pressed)
|
||||||
|
options_button.pressed.connect(_on_options_pressed)
|
||||||
|
credits_button.pressed.connect(_on_credits_pressed)
|
||||||
|
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_options_toggles()
|
||||||
|
_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)
|
||||||
|
var hint := get_node_or_null("OptionsPanel/VBox/Hint") as Label
|
||||||
|
if hint != null:
|
||||||
|
hint.add_theme_font_override("font", UiFonts.body())
|
||||||
|
hint.add_theme_color_override("font_color", MENU_CREAM)
|
||||||
|
_style_credits(get_node_or_null("CreditsPanel/VBox/Names") as RichTextLabel)
|
||||||
|
|
||||||
|
|
||||||
|
# The credits use a RichTextLabel so the Instagram handles are clickable links
|
||||||
|
# ([url] tags). meta_clicked opens the profile — in the web export OS.shell_open
|
||||||
|
# pops a new browser tab.
|
||||||
|
func _style_credits(names: RichTextLabel) -> void:
|
||||||
|
if names == null:
|
||||||
|
return
|
||||||
|
names.add_theme_font_override("normal_font", UiFonts.body())
|
||||||
|
names.add_theme_font_override("bold_font", UiFonts.title())
|
||||||
|
names.add_theme_color_override("default_color", MENU_CREAM)
|
||||||
|
names.meta_clicked.connect(func(meta: Variant) -> void:
|
||||||
|
OS.shell_open(str(meta)))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
var frame_count := CAPE_POS.size()
|
||||||
|
var t := video.stream_position
|
||||||
|
var frame_idx := int(t * VIDEO_FPS) % frame_count
|
||||||
|
var cape: Vector2 = CAPE_POS[frame_idx]
|
||||||
|
play_button.anchor_left = cape.x
|
||||||
|
play_button.anchor_right = cape.x
|
||||||
|
play_button.anchor_top = cape.y
|
||||||
|
play_button.anchor_bottom = cape.y
|
||||||
|
|
||||||
|
|
||||||
|
# Player-facing option toggles, seated above the rebind list in the Options panel:
|
||||||
|
# the master screen-shake switch and the gore toggle (both persisted via Settings).
|
||||||
|
func _build_options_toggles() -> void:
|
||||||
|
var scroll_idx := options_scroll.get_index()
|
||||||
|
# Insert in reverse so both land directly above the rebind scroll, in listed order.
|
||||||
|
_add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, scroll_idx)
|
||||||
|
_add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, scroll_idx)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_toggle_row(label_text: String, value: bool, setter: Callable, at_index: int) -> void:
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
var name_label := Label.new()
|
||||||
|
name_label.text = label_text
|
||||||
|
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
name_label.add_theme_font_override("font", UiFonts.body())
|
||||||
|
name_label.add_theme_font_size_override("font_size", 20)
|
||||||
|
name_label.add_theme_color_override("font_color", MENU_CREAM)
|
||||||
|
name_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
name_label.add_theme_constant_override("outline_size", 5)
|
||||||
|
row.add_child(name_label)
|
||||||
|
|
||||||
|
var toggle := CheckButton.new()
|
||||||
|
toggle.button_pressed = value
|
||||||
|
toggle.add_theme_color_override("font_color", MENU_GOLD)
|
||||||
|
toggle.toggled.connect(func(on: bool) -> void: setter.call(on))
|
||||||
|
row.add_child(toggle)
|
||||||
|
|
||||||
|
options_vbox.add_child(row)
|
||||||
|
options_vbox.move_child(row, at_index)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_rebind_rows() -> void:
|
||||||
|
for child in rebind_list.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
for action: StringName in ACTION_LABELS:
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
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_font_size_override("font_size", 20)
|
||||||
|
name_label.add_theme_color_override("font_color", MENU_CREAM)
|
||||||
|
name_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
name_label.add_theme_constant_override("outline_size", 5)
|
||||||
|
row.add_child(name_label)
|
||||||
|
|
||||||
|
var 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)
|
||||||
|
|
||||||
|
rebind_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_play_pressed() -> void:
|
||||||
|
_set_buttons_disabled(true)
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.tween_property(fade_rect, "color:a", 1.0, FADE_TIME)
|
||||||
|
tween.tween_callback(func() -> void:
|
||||||
|
get_tree().change_scene_to_file(GAME_SCENE))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_options_pressed() -> void:
|
||||||
|
main_panel.visible = false
|
||||||
|
play_button.visible = false
|
||||||
|
options_panel.visible = true
|
||||||
|
options_back.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_credits_pressed() -> void:
|
||||||
|
main_panel.visible = false
|
||||||
|
play_button.visible = false
|
||||||
|
credits_panel.visible = true
|
||||||
|
credits_back.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_exit_pressed() -> void:
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _close_subpanels() -> void:
|
||||||
|
_cancel_listening()
|
||||||
|
options_panel.visible = false
|
||||||
|
credits_panel.visible = false
|
||||||
|
main_panel.visible = true
|
||||||
|
play_button.visible = true
|
||||||
|
play_button.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_buttons_disabled(value: bool) -> void:
|
||||||
|
play_button.disabled = value
|
||||||
|
options_button.disabled = value
|
||||||
|
credits_button.disabled = value
|
||||||
|
exit_button.disabled = value
|
||||||
|
|
||||||
|
|
||||||
|
func _on_rebind_pressed(action: StringName, button: Button) -> void:
|
||||||
|
_cancel_listening()
|
||||||
|
_listening_action = action
|
||||||
|
_listening_button = button
|
||||||
|
button.text = "Press a key…"
|
||||||
|
|
||||||
|
|
||||||
|
func _cancel_listening() -> void:
|
||||||
|
if _listening_button != null:
|
||||||
|
_listening_button.text = Controls.get_key_label(_listening_action)
|
||||||
|
_listening_action = &""
|
||||||
|
_listening_button = null
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if _listening_action == &"":
|
||||||
|
return
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
|
if event.physical_keycode == KEY_ESCAPE:
|
||||||
|
_cancel_listening()
|
||||||
|
else:
|
||||||
|
var new_event := InputEventKey.new()
|
||||||
|
new_event.physical_keycode = event.physical_keycode
|
||||||
|
Controls.rebind(_listening_action, new_event)
|
||||||
|
_listening_button.text = Controls.get_key_label(_listening_action)
|
||||||
|
_listening_action = &""
|
||||||
|
_listening_button = null
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://46y10alai56q
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
class_name MatadorRagdoll
|
||||||
|
extends RefCounted
|
||||||
|
## Builds the matador's physical-bone ragdoll. Body joints are cone-twist with
|
||||||
|
## limited swing/twist (DP-tunable) so the corpse keeps its silhouette instead of
|
||||||
|
## folding into a flat "deflated" pile; the cape stays free (PIN) so it drapes.
|
||||||
|
|
||||||
|
# [bone, radius] → sphere collider; [bone, radius, height] → capsule.
|
||||||
|
const _BONES: Array = [
|
||||||
|
["matador", 0.18],
|
||||||
|
["COG", 0.12],
|
||||||
|
["chest", 0.13, 0.30],
|
||||||
|
["head", 0.14],
|
||||||
|
["collarbone_L", 0.06, 0.18],
|
||||||
|
["collarbone_R", 0.06, 0.18],
|
||||||
|
["arm_L", 0.07, 0.22],
|
||||||
|
["arm_R", 0.07, 0.22],
|
||||||
|
["forearm_L", 0.06, 0.22],
|
||||||
|
["forearm_R", 0.06, 0.22],
|
||||||
|
["hand_L", 0.05, 0.12],
|
||||||
|
["hand_R", 0.05, 0.12],
|
||||||
|
["leg_L", 0.10, 0.32],
|
||||||
|
["leg_R", 0.10, 0.32],
|
||||||
|
["shin_L", 0.08, 0.28],
|
||||||
|
["shin_R", 0.08, 0.28],
|
||||||
|
["foot_L", 0.07, 0.18],
|
||||||
|
["foot_R", 0.07, 0.18],
|
||||||
|
["Bone", 0.04, 0.30],
|
||||||
|
["Bone.001", 0.04, 0.34],
|
||||||
|
["Bone.002", 0.04, 0.30],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Reuse an existing simulator if the rig already has one, else build the bodies.
|
||||||
|
static func build(skeleton: Skeleton3D) -> PhysicalBoneSimulator3D:
|
||||||
|
for child: Node in skeleton.get_children():
|
||||||
|
if child is PhysicalBoneSimulator3D:
|
||||||
|
(child as PhysicalBoneSimulator3D).active = false
|
||||||
|
return child as PhysicalBoneSimulator3D
|
||||||
|
|
||||||
|
var sim := PhysicalBoneSimulator3D.new()
|
||||||
|
sim.active = false
|
||||||
|
skeleton.add_child(sim)
|
||||||
|
|
||||||
|
var swing := DP.f("mat_joint_swing")
|
||||||
|
var twist := DP.f("mat_joint_twist")
|
||||||
|
var damp := DP.f("mat_ragdoll_damp")
|
||||||
|
|
||||||
|
for entry: Array in _BONES:
|
||||||
|
var bname: String = entry[0]
|
||||||
|
if skeleton.find_bone(bname) == -1:
|
||||||
|
continue
|
||||||
|
var pb := PhysicalBone3D.new()
|
||||||
|
pb.bone_name = bname
|
||||||
|
pb.mass = 0.4
|
||||||
|
pb.linear_damp = 0.1
|
||||||
|
pb.collision_layer = 1
|
||||||
|
pb.collision_mask = 1
|
||||||
|
if bname.begins_with("Bone"):
|
||||||
|
# Cape: floppy cloth — free swing, light, draggy, one-way collision.
|
||||||
|
pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
|
||||||
|
pb.mass = 0.15
|
||||||
|
pb.linear_damp = 0.5
|
||||||
|
pb.angular_damp = 0.7
|
||||||
|
pb.collision_layer = 0
|
||||||
|
else:
|
||||||
|
# Body: cone-twist joints cap how far each bone bends from its rest
|
||||||
|
# pose, so the silhouette holds; extra angular damp settles the flail.
|
||||||
|
pb.joint_type = PhysicalBone3D.JOINT_TYPE_CONE
|
||||||
|
pb.angular_damp = damp
|
||||||
|
pb.set(&"joint_constraints/swing_span", swing)
|
||||||
|
pb.set(&"joint_constraints/twist_span", twist)
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
if entry.size() >= 3:
|
||||||
|
var cap := CapsuleShape3D.new()
|
||||||
|
cap.radius = entry[1]
|
||||||
|
cap.height = entry[2]
|
||||||
|
cs.shape = cap
|
||||||
|
else:
|
||||||
|
var sph := SphereShape3D.new()
|
||||||
|
sph.radius = entry[1]
|
||||||
|
cs.shape = sph
|
||||||
|
pb.add_child(cs)
|
||||||
|
sim.add_child(pb)
|
||||||
|
|
||||||
|
return sim
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dmf04b5exnejn
|
||||||
@@ -1,70 +1,142 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
signal matador_killed
|
signal matador_killed
|
||||||
|
signal all_defeated
|
||||||
|
|
||||||
const MATADOR := preload("res://Matador.tscn")
|
const MATADOR := preload("res://Matador.tscn")
|
||||||
const RESPAWN_COOLDOWN := 8.0
|
|
||||||
|
|
||||||
var _respawn_cooldown: float = 0.0
|
var _spawn_count: int = 1
|
||||||
var _wave_count: int = 1
|
|
||||||
var _alive: int = 0
|
var _alive: int = 0
|
||||||
|
var _kills: int = 0
|
||||||
|
var _resolved: bool = false
|
||||||
|
|
||||||
|
# Flat (x,z) positions of the most recent batch spawned. A fresh wave keeps at least
|
||||||
|
# _SPAWN_MIN_SEP away from these so it never lands on top of where the last one did.
|
||||||
|
const _SPAWN_MIN_SEP: float = 3.5
|
||||||
|
var _last_spawn_positions: Array[Vector3] = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group(&"matador_spawn")
|
add_to_group(&"matador_spawn")
|
||||||
_wave_count = maxi(1, int(DP.f("mat_spawn_count")))
|
_spawn_count = maxi(1, int(DP.f("mat_spawn_count")))
|
||||||
|
# Deferred so it's safe from _ready and matches the previous spawn timing.
|
||||||
_spawn.call_deferred()
|
_spawn.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
func _spawn() -> void:
|
func _spawn() -> void:
|
||||||
var positions := _spread_positions(_wave_count)
|
# Bull and matadors start at opposite ends of a random diameter, so they always
|
||||||
for pos: Vector3 in positions:
|
# begin as far apart as the arena allows (edge-to-edge across the centre).
|
||||||
|
var arena_r := DP.f("arena_spawn_radius")
|
||||||
|
var bull_angle := randf() * TAU
|
||||||
|
_place_bull(bull_angle, arena_r)
|
||||||
|
var placed: Array[Vector3] = _spread_positions(_spawn_count, bull_angle + PI, arena_r)
|
||||||
|
for pos: Vector3 in placed:
|
||||||
_spawn_at(pos)
|
_spawn_at(pos)
|
||||||
|
_last_spawn_positions = placed
|
||||||
|
|
||||||
|
|
||||||
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, spread across the far side (centred on `center_angle`)
|
||||||
|
# so every matador stays near the point diametrically opposite the bull. Each slot gets
|
||||||
|
# its own angular band and a staggered radius, then rejection-samples inside that band so
|
||||||
|
# no two matadors land closer than _SPAWN_MIN_SEP — otherwise they pile up on top of
|
||||||
|
# each other on spawn. The arc widens with the crowd so a big wave doesn't get cramped.
|
||||||
|
func _spread_positions(count: int, center_angle: float, arena_r: float) -> Array[Vector3]:
|
||||||
var positions: Array[Vector3] = []
|
var positions: Array[Vector3] = []
|
||||||
var arena_r := DP.f("mat_wander_radius") * 0.72
|
if count <= 1:
|
||||||
var min_r := 5.0
|
positions.append(Vector3(cos(center_angle) * arena_r, 1.0, sin(center_angle) * arena_r))
|
||||||
|
return positions
|
||||||
|
var arc := clampf(TAU * 0.33 + float(count) * 0.25, TAU * 0.33, TAU * 0.92)
|
||||||
|
var slot := arc / float(count)
|
||||||
|
var inner := maxf(2.5, arena_r * 0.5)
|
||||||
for i: int in count:
|
for i: int in count:
|
||||||
var sector := TAU / count
|
# Give each matador its own angular slot for an even fan, but sample the radius
|
||||||
var angle := sector * i + randf_range(-sector * 0.35, sector * 0.35)
|
# freely across the whole far-side band so the rejection test can push crowded
|
||||||
var radius := randf_range(min_r, arena_r)
|
# neighbours onto a different ring instead of piling them at one distance.
|
||||||
positions.append(Vector3(cos(angle) * radius, 1.0, sin(angle) * radius))
|
var base_angle := center_angle - arc * 0.5 + slot * (float(i) + 0.5)
|
||||||
|
var pos := Vector3(cos(base_angle) * arena_r, 1.0, sin(base_angle) * arena_r)
|
||||||
|
for attempt: int in 16:
|
||||||
|
var angle := base_angle + randf_range(-0.5, 0.5) * slot
|
||||||
|
var radius := randf_range(inner, arena_r)
|
||||||
|
pos = Vector3(cos(angle) * radius, 1.0, sin(angle) * radius)
|
||||||
|
if _clear_of(pos, positions, _SPAWN_MIN_SEP):
|
||||||
|
break
|
||||||
|
positions.append(pos)
|
||||||
return positions
|
return positions
|
||||||
|
|
||||||
|
|
||||||
func _spawn_at(pos: Vector3) -> void:
|
func _spawn_at(pos: Vector3, generation: int = 0) -> void:
|
||||||
var m: Node3D = MATADOR.instantiate()
|
var m: Node3D = MATADOR.instantiate()
|
||||||
m.position = pos
|
m.position = pos
|
||||||
|
m.set_meta(&"generation", generation)
|
||||||
get_parent().add_child(m)
|
get_parent().add_child(m)
|
||||||
m.killed.connect(_on_matador_killed)
|
m.killed.connect(_on_matador_killed.bind(m))
|
||||||
_alive += 1
|
_alive += 1
|
||||||
|
|
||||||
|
|
||||||
func _on_matador_killed() -> void:
|
# A slain matador is replaced by mat_split_factor fresh ones on the spot (a hydra),
|
||||||
|
# so the horde doubles each generation, up to mat_split_generations deep. The bull
|
||||||
|
# wins only when a whole generation dies out without splitting — i.e. once the spawns
|
||||||
|
# stop and the last body drops.
|
||||||
|
func _on_matador_killed(dead: Node3D) -> void:
|
||||||
matador_killed.emit()
|
matador_killed.emit()
|
||||||
_alive -= 1
|
_alive -= 1
|
||||||
if _alive <= 0:
|
_kills += 1
|
||||||
_wave_count *= 2
|
_split(dead)
|
||||||
_spawn.call_deferred()
|
var win_kills := int(DP.f("mat_win_kills"))
|
||||||
|
var reached_quota := win_kills > 0 and _kills >= win_kills
|
||||||
|
if (reached_quota or _alive <= 0) and not _resolved:
|
||||||
|
_resolved = true
|
||||||
|
all_defeated.emit()
|
||||||
|
|
||||||
|
|
||||||
func reset_spawn() -> void:
|
func _split(dead: Node3D) -> void:
|
||||||
for m: Node in get_tree().get_nodes_in_group(&"matador"):
|
var count := int(DP.f("mat_split_factor"))
|
||||||
m.queue_free()
|
var generation := int(dead.get_meta(&"generation", 0))
|
||||||
await get_tree().process_frame
|
if count < 2 or generation >= int(DP.f("mat_split_generations")):
|
||||||
_wave_count = maxi(1, int(DP.f("mat_spawn_count")))
|
return
|
||||||
_alive = 0
|
var arena_r := DP.f("arena_spawn_radius")
|
||||||
_spawn()
|
var origin := dead.global_position
|
||||||
|
origin.y = 0.0
|
||||||
|
var wave: Array[Vector3] = []
|
||||||
|
for i: int in count:
|
||||||
|
var pos := _pick_spawn(origin, arena_r, wave)
|
||||||
|
wave.append(pos)
|
||||||
|
_spawn_at(pos, generation + 1)
|
||||||
|
_last_spawn_positions = wave
|
||||||
|
|
||||||
|
|
||||||
|
# Rejection-sample a spawn point ringed around `origin`, pushing the ring outward on
|
||||||
|
# each failed attempt until it clears both the previous wave (_last_spawn_positions)
|
||||||
|
# and the spots already taken this wave — so a fresh wave never lands where the last
|
||||||
|
# one did. Falls back to the last candidate if the arena is too cramped to satisfy it.
|
||||||
|
func _pick_spawn(origin: Vector3, arena_r: float, taken: Array[Vector3]) -> Vector3:
|
||||||
|
var pos := origin
|
||||||
|
for attempt: int in 12:
|
||||||
|
var angle := randf() * TAU
|
||||||
|
var r := randf_range(2.0, 3.5) + float(attempt) * 0.4
|
||||||
|
pos = origin + Vector3(cos(angle) * r, 0.0, sin(angle) * r)
|
||||||
|
var flat := Vector2(pos.x, pos.z)
|
||||||
|
if flat.length() > arena_r:
|
||||||
|
flat = flat.normalized() * arena_r
|
||||||
|
pos = Vector3(flat.x, 0.0, flat.y)
|
||||||
|
if _clear_of(pos, _last_spawn_positions, _SPAWN_MIN_SEP) \
|
||||||
|
and _clear_of(pos, taken, _SPAWN_MIN_SEP):
|
||||||
|
break
|
||||||
|
pos.y = 1.0
|
||||||
|
return pos
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_of(pos: Vector3, others: Array[Vector3], min_sep: float) -> bool:
|
||||||
|
for o: Vector3 in others:
|
||||||
|
if Vector2(pos.x - o.x, pos.z - o.z).length() < min_sep:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
shader_type canvas_item;
|
||||||
|
|
||||||
|
// Blurred, darkened backdrop for the intro video. A COVER-scaled copy of the
|
||||||
|
// clip sits behind the sharp fitted clip so portrait/ultrawide screens fill
|
||||||
|
// edge-to-edge instead of showing dead bars.
|
||||||
|
|
||||||
|
uniform float blur_radius : hint_range(0.0, 8.0) = 3.0;
|
||||||
|
uniform float darken : hint_range(0.0, 1.0) = 0.35;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec2 px = TEXTURE_PIXEL_SIZE * blur_radius;
|
||||||
|
vec4 sum = vec4(0.0);
|
||||||
|
// 3x3 gaussian-ish tap, weights sum to 16.
|
||||||
|
sum += texture(TEXTURE, UV + vec2(-px.x, -px.y)) * 1.0;
|
||||||
|
sum += texture(TEXTURE, UV + vec2(0.0, -px.y)) * 2.0;
|
||||||
|
sum += texture(TEXTURE, UV + vec2(px.x, -px.y)) * 1.0;
|
||||||
|
sum += texture(TEXTURE, UV + vec2(-px.x, 0.0)) * 2.0;
|
||||||
|
sum += texture(TEXTURE, UV) * 4.0;
|
||||||
|
sum += texture(TEXTURE, UV + vec2(px.x, 0.0)) * 2.0;
|
||||||
|
sum += texture(TEXTURE, UV + vec2(-px.x, px.y)) * 1.0;
|
||||||
|
sum += texture(TEXTURE, UV + vec2(0.0, px.y)) * 2.0;
|
||||||
|
sum += texture(TEXTURE, UV + vec2(px.x, px.y)) * 1.0;
|
||||||
|
sum /= 16.0;
|
||||||
|
COLOR = vec4(sum.rgb * (1.0 - darken), sum.a);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bmdtg6qlh7wri
|
||||||
@@ -11,15 +11,23 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Bullosseum"
|
config/name="Bullosseum"
|
||||||
run/main_scene="res://scene.tscn"
|
config/version="0.2.0"
|
||||||
|
run/main_scene="res://MainMenu.tscn"
|
||||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
config/run/main_scene="res://scene.tscn"
|
config/run/main_scene="res://MainMenu.tscn"
|
||||||
|
boot_splash/bg_color=Color(0, 0, 0, 1)
|
||||||
|
boot_splash/image="res://Assets/loading_screen.png"
|
||||||
|
boot_splash/fullsize=true
|
||||||
|
boot_splash/use_filter=true
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
DP="*res://debug_params.gd"
|
DP="*res://debug_params.gd"
|
||||||
DebugMenu="*res://debug_menu.gd"
|
Console="*res://console.gd"
|
||||||
|
Controls="*res://controls_manager.gd"
|
||||||
|
DebugDraw="*res://debug_draw.gd"
|
||||||
|
Settings="*res://settings.gd"
|
||||||
|
|
||||||
[editor_plugins]
|
[editor_plugins]
|
||||||
|
|
||||||
@@ -59,12 +67,6 @@ move_right={
|
|||||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
jump={
|
|
||||||
"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":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
|
||||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
zoom_in={
|
zoom_in={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(726, 8),"global_position":Vector2(735, 56),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(726, 8),"global_position":Vector2(735, 56),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
||||||
@@ -80,24 +82,6 @@ toggle_mouse_capture={
|
|||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":4,"position":Vector2(461, 22),"global_position":Vector2(470, 70),"factor":1.0,"button_index":3,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":4,"position":Vector2(461, 22),"global_position":Vector2(470, 70),"factor":1.0,"button_index":3,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
charge={
|
|
||||||
"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":4194325,"key_label":0,"unicode":0,"location":1,"echo":false,"script":null)
|
|
||||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":false,"script":null)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
thrust_left={
|
|
||||||
"deadzone": 0.2,
|
|
||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
|
||||||
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":4,"axis_value":1.0,"script":null)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
thrust_right={
|
|
||||||
"deadzone": 0.2,
|
|
||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
|
||||||
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
ability_kick={
|
ability_kick={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
|
||||||
@@ -116,6 +100,16 @@ ability_dash={
|
|||||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null)
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
ability_roll={
|
||||||
|
"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":74,"key_label":0,"unicode":106,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":true,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
[input_devices]
|
||||||
|
|
||||||
|
pointing/emulate_touch_from_mouse=true
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
extends ColorRect
|
||||||
|
## Drives the fullscreen PS1/CRT post-process (ps1_filter.gdshader) from the DP
|
||||||
|
## registry so every uniform is live-tunable in the console. `show_ps1` toggles the
|
||||||
|
## whole overlay; the ps1_* floats feed the matching shader uniforms 1:1.
|
||||||
|
|
||||||
|
# DP param key → shader uniform name. Keys are registered in debug_params.gd with
|
||||||
|
# ranges that mirror the shader's hint_range, so nothing here can go out of bounds.
|
||||||
|
const _UNIFORMS := {
|
||||||
|
"ps1_pixel_scale": "pixel_scale",
|
||||||
|
"ps1_color_depth": "color_depth",
|
||||||
|
"ps1_dither_strength": "dither_strength",
|
||||||
|
"ps1_scanline_strength": "scanline_strength",
|
||||||
|
"ps1_roll_speed": "roll_speed",
|
||||||
|
"ps1_roll_strength": "roll_strength",
|
||||||
|
}
|
||||||
|
|
||||||
|
@onready var _mat: ShaderMaterial = material as ShaderMaterial
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
DP.any_changed.connect(_on_param_changed)
|
||||||
|
_apply_all()
|
||||||
|
|
||||||
|
|
||||||
|
# reset_all() fires with key "__all__"; a single edit fires with its own key. Either
|
||||||
|
# way just re-push everything — it's a handful of cheap uniform writes.
|
||||||
|
func _on_param_changed(_key: String, _value: Variant) -> void:
|
||||||
|
_apply_all()
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_all() -> void:
|
||||||
|
visible = DP.b("show_ps1")
|
||||||
|
if _mat == null:
|
||||||
|
return
|
||||||
|
for key: String in _UNIFORMS:
|
||||||
|
_mat.set_shader_parameter(_UNIFORMS[key], DP.f(key))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ce5n4qu88mhyx
|
||||||
@@ -2,6 +2,9 @@ extends Node3D
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
# Mark the crowd so debug overlays (show_animation_name / show_bones) skip it —
|
||||||
|
# 72 ambient clapping spectators would otherwise flood the view with tags.
|
||||||
|
add_to_group(&"public")
|
||||||
for child in get_children():
|
for child in get_children():
|
||||||
var anim := child.get_node_or_null(^"AnimationPlayer") as AnimationPlayer
|
var anim := child.get_node_or_null(^"AnimationPlayer") as AnimationPlayer
|
||||||
if anim:
|
if anim:
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ echo ""
|
|||||||
echo "=== Logic tests ==="
|
echo "=== Logic tests ==="
|
||||||
"$GODOT" --headless --script tests/logic_test.gd
|
"$GODOT" --headless --script tests/logic_test.gd
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Console focus tests ==="
|
||||||
|
"$GODOT" --headless --script tests/console_focus_test.gd
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Performance tests (frame-budget regression guard) ==="
|
||||||
|
"$GODOT" --headless --script tests/performance_test.gd
|
||||||
|
|
||||||
if [[ "$SKIP_GAMEPLAY" -eq 0 ]]; then
|
if [[ "$SKIP_GAMEPLAY" -eq 0 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Gameplay tests (headless — assertions only, screenshots may be blank) ==="
|
echo "=== Gameplay tests (headless — assertions only, screenshots may be blank) ==="
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
extends Node
|
||||||
|
## Player-facing game options (autoload `Settings`), persisted to user://settings.cfg.
|
||||||
|
## Distinct from DP (debug tunables) and Controls (keybinds): this is the small set of
|
||||||
|
## comfort/accessibility toggles the options menu exposes to the player.
|
||||||
|
|
||||||
|
signal changed(key: String, value: Variant)
|
||||||
|
|
||||||
|
const SAVE_PATH := "user://settings.cfg"
|
||||||
|
const SECTION := "options"
|
||||||
|
|
||||||
|
## Master screen-shake toggle — gates every camera_iso.trigger_hit (hits and ability FX).
|
||||||
|
var screen_shake: bool = true
|
||||||
|
## Gore toggle — when off, no blood spawns (impact spurt + ongoing spear-wound bleed).
|
||||||
|
var gore: bool = true
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
load_saved()
|
||||||
|
|
||||||
|
|
||||||
|
func set_screen_shake(value: bool) -> void:
|
||||||
|
if screen_shake == value:
|
||||||
|
return
|
||||||
|
screen_shake = value
|
||||||
|
changed.emit("screen_shake", value)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func set_gore(value: bool) -> void:
|
||||||
|
if gore == value:
|
||||||
|
return
|
||||||
|
gore = value
|
||||||
|
changed.emit("gore", value)
|
||||||
|
save()
|
||||||
|
|
||||||
|
|
||||||
|
func save() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
cfg.set_value(SECTION, "screen_shake", screen_shake)
|
||||||
|
cfg.set_value(SECTION, "gore", gore)
|
||||||
|
cfg.save(SAVE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
func load_saved() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(SAVE_PATH) != OK:
|
||||||
|
return
|
||||||
|
screen_shake = cfg.get_value(SECTION, "screen_shake", true) as bool
|
||||||
|
gore = cfg.get_value(SECTION, "gore", true) as bool
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dyspgr7m238la
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
class_name SpearProjectile
|
||||||
|
extends RigidBody3D
|
||||||
|
|
||||||
|
# A thrown spear. It flies a ballistic arc (gravity does the work — the launch
|
||||||
|
# velocity is solved by the matador to land on the bull) and keeps its shaft aimed
|
||||||
|
# along its own velocity, so it noses over at the apex and drives in point-first.
|
||||||
|
# On impact it embeds where it struck: the tip buried just past the contact point
|
||||||
|
# with the shaft trailing back along the flight line, so it reads as stuck-in rather
|
||||||
|
# than floating alongside. A hit on the bull leaves the spear riding it like a spine
|
||||||
|
# (see shed_all, which the bull's slam calls to fling them all off); other hits pin
|
||||||
|
# it in place until the despawn timer clears it.
|
||||||
|
|
||||||
|
const _DESPAWN_SEC: float = 8.0
|
||||||
|
const _STUCK_GROUP: StringName = &"stuck_spear"
|
||||||
|
# Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin.
|
||||||
|
const _TIP_Z: float = 1.915
|
||||||
|
const _EMBED_DEPTH: float = 0.45 # how far past the surface the tip buries
|
||||||
|
|
||||||
|
var _mesh: Node3D = null
|
||||||
|
var _stopped: bool = false
|
||||||
|
var _flight_dir: Vector3 = Vector3.ZERO # last pre-impact travel direction
|
||||||
|
|
||||||
|
|
||||||
|
# Build, seat and launch a spear from an already-instantiated mesh (the one that was
|
||||||
|
# in the matador's hand). `origin` is the release point; `velocity` is the full
|
||||||
|
# ballistic launch vector; `gravity` is the RigidBody gravity_scale for the arc.
|
||||||
|
static func launch(parent: Node, mesh: Node3D, origin: Vector3, velocity: Vector3,
|
||||||
|
gravity: float) -> SpearProjectile:
|
||||||
|
var s := SpearProjectile.new()
|
||||||
|
s.gravity_scale = gravity
|
||||||
|
s.collision_layer = 0
|
||||||
|
s.collision_mask = 1
|
||||||
|
s.contact_monitor = true
|
||||||
|
s.max_contacts_reported = 6
|
||||||
|
parent.add_child(s)
|
||||||
|
# Seat the body so its local +Z (the shaft, see spear.glb) points along the throw.
|
||||||
|
s.global_transform = Transform3D(_aim_basis(velocity), origin)
|
||||||
|
mesh.reparent(s, false)
|
||||||
|
mesh.transform = Transform3D.IDENTITY
|
||||||
|
s._mesh = mesh
|
||||||
|
s.add_child(_shaft_collider())
|
||||||
|
s.linear_velocity = velocity
|
||||||
|
_despawn(s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# Shed every spear stuck in `bull` — pop each off as a tumbling body that flies clear
|
||||||
|
# and clears itself on the despawn timer. Called by the bull's slam.
|
||||||
|
static func shed_all(bull: Node) -> void:
|
||||||
|
if not (bull is Node3D):
|
||||||
|
return
|
||||||
|
var scene: Node = bull.get_tree().current_scene
|
||||||
|
if scene == null:
|
||||||
|
scene = bull
|
||||||
|
var center: Vector3 = (bull as Node3D).global_position
|
||||||
|
for child in bull.get_children():
|
||||||
|
if not child.is_in_group(_STUCK_GROUP):
|
||||||
|
continue
|
||||||
|
var mesh := child as Node3D
|
||||||
|
var gx := mesh.global_transform
|
||||||
|
var rb := RigidBody3D.new()
|
||||||
|
rb.collision_layer = 0
|
||||||
|
rb.collision_mask = 1
|
||||||
|
scene.add_child(rb)
|
||||||
|
rb.global_transform = gx
|
||||||
|
mesh.reparent(rb, false)
|
||||||
|
mesh.transform = Transform3D.IDENTITY
|
||||||
|
mesh.remove_from_group(_STUCK_GROUP)
|
||||||
|
rb.add_child(_shaft_collider())
|
||||||
|
# Fling outward from the bull with a bit of lift and spin.
|
||||||
|
var out := gx.origin - center
|
||||||
|
out.y = 0.0
|
||||||
|
out = out.normalized() if out.length() > 0.01 else Vector3(randf() - 0.5, 0.0, randf() - 0.5)
|
||||||
|
rb.linear_velocity = out * randf_range(3.0, 6.0) + Vector3.UP * randf_range(3.0, 5.0)
|
||||||
|
rb.angular_velocity = Vector3(randf_range(-10.0, 10.0),
|
||||||
|
randf_range(-10.0, 10.0), randf_range(-10.0, 10.0))
|
||||||
|
_despawn(rb)
|
||||||
|
|
||||||
|
|
||||||
|
static func _shaft_collider() -> CollisionShape3D:
|
||||||
|
# Collider along local +Z (mesh runs z≈-0.34 … 1.92), fattened so a fast throw
|
||||||
|
# can't tunnel past the bull's collision spheres.
|
||||||
|
var cap := CapsuleShape3D.new()
|
||||||
|
cap.radius = 0.13
|
||||||
|
cap.height = 2.2
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
cs.shape = cap
|
||||||
|
cs.position = Vector3(0.0, 0.0, 0.79)
|
||||||
|
cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
|
||||||
|
return cs
|
||||||
|
|
||||||
|
|
||||||
|
# Continuous bleed from a spear lodged in the bull: a steady spurt of dark-red gobs out
|
||||||
|
# of the entry wound. Parented to the stuck mesh so it tracks the wound as the bull
|
||||||
|
# moves; world-space particles (local_coords off) so they arc off and fall behind as a
|
||||||
|
# trail. Dies with the mesh when the spear is shed or despawned.
|
||||||
|
static func _wound_emitter() -> CPUParticles3D:
|
||||||
|
var ramp := Gradient.new()
|
||||||
|
ramp.set_color(0, Color(0.5, 0.02, 0.02, 1.0))
|
||||||
|
ramp.set_color(1, Color(0.22, 0.0, 0.0, 0.0))
|
||||||
|
|
||||||
|
var sc := Curve.new()
|
||||||
|
sc.add_point(Vector2(0.0, 1.0))
|
||||||
|
sc.add_point(Vector2(0.6, 0.7))
|
||||||
|
sc.add_point(Vector2(1.0, 0.0))
|
||||||
|
|
||||||
|
var p := CPUParticles3D.new()
|
||||||
|
# Seat at the wound (where the shaft meets the hide) and spray back out along the
|
||||||
|
# shaft: mesh local -Z is the exit direction, rotated into world by the node basis.
|
||||||
|
p.position = Vector3(0.0, 0.0, _TIP_Z - _EMBED_DEPTH)
|
||||||
|
p.emitting = true
|
||||||
|
p.amount = 22
|
||||||
|
p.lifetime = 0.6
|
||||||
|
p.explosiveness = 0.0
|
||||||
|
p.randomness = 0.6
|
||||||
|
p.local_coords = false
|
||||||
|
p.direction = Vector3(0.0, 0.55, -1.0).normalized()
|
||||||
|
p.spread = 32.0
|
||||||
|
p.gravity = Vector3(0.0, -12.0, 0.0)
|
||||||
|
p.initial_velocity_min = 1.6
|
||||||
|
p.initial_velocity_max = 4.0
|
||||||
|
p.scale_amount_min = 0.5
|
||||||
|
p.scale_amount_max = 1.4
|
||||||
|
p.mesh = _blood_mesh()
|
||||||
|
p.color_ramp = ramp
|
||||||
|
p.scale_amount_curve = sc
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
# Small unshaded, vertex-coloured sphere for blood gobs.
|
||||||
|
static func _blood_mesh() -> SphereMesh:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
mat.vertex_color_use_as_albedo = true
|
||||||
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
|
var s := SphereMesh.new()
|
||||||
|
s.radius = 0.045
|
||||||
|
s.height = 0.09
|
||||||
|
s.radial_segments = 4
|
||||||
|
s.rings = 2
|
||||||
|
s.material = mat
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
static func _despawn(node: Node) -> void:
|
||||||
|
node.get_tree().create_timer(_DESPAWN_SEC).timeout.connect(func() -> void:
|
||||||
|
if is_instance_valid(node):
|
||||||
|
node.queue_free()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Basis whose local +Z points along `v` (looking_at points -Z at its target).
|
||||||
|
static func _aim_basis(v: Vector3) -> Basis:
|
||||||
|
var dir := v.normalized()
|
||||||
|
if dir.length_squared() < 0.0001:
|
||||||
|
return Basis.IDENTITY
|
||||||
|
var up := Vector3.UP if absf(dir.dot(Vector3.UP)) < 0.99 else Vector3.FORWARD
|
||||||
|
return Basis.looking_at(-dir, up)
|
||||||
|
|
||||||
|
|
||||||
|
# While flying, keep the shaft aligned with the velocity; on the first contact freeze
|
||||||
|
# and hand off to _embed so the spear sticks where it struck instead of bouncing off.
|
||||||
|
func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
|
||||||
|
if _stopped:
|
||||||
|
return
|
||||||
|
if state.get_contact_count() > 0:
|
||||||
|
_stopped = true
|
||||||
|
# Use the remembered pre-impact heading — state.linear_velocity is already the
|
||||||
|
# bounced (post-collision) velocity by now, which would aim the shaft wrong.
|
||||||
|
var contact := state.get_contact_collider_position(0)
|
||||||
|
var other := state.get_contact_collider_object(0)
|
||||||
|
state.linear_velocity = Vector3.ZERO
|
||||||
|
state.angular_velocity = Vector3.ZERO
|
||||||
|
_embed.call_deferred(other, contact, _flight_dir)
|
||||||
|
return
|
||||||
|
var v := state.linear_velocity
|
||||||
|
if v.length_squared() > 0.04:
|
||||||
|
_flight_dir = v.normalized()
|
||||||
|
var t := state.transform
|
||||||
|
t.basis = _aim_basis(v)
|
||||||
|
state.transform = t
|
||||||
|
state.angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
# Compose the stuck pose (tip buried _EMBED_DEPTH past the contact, shaft along the
|
||||||
|
# flight line) and plant the spear. A bull hit hands the mesh to the bull so it rides
|
||||||
|
# along as a spine; anything else pins the body in place for the despawn timer.
|
||||||
|
func _embed(other: Object, contact: Vector3, flight: Vector3) -> void:
|
||||||
|
var f := flight.normalized()
|
||||||
|
if f.length_squared() < 0.0001:
|
||||||
|
f = global_transform.basis.z.normalized()
|
||||||
|
var pose := Transform3D(_aim_basis(f), contact + f * (_EMBED_DEPTH - _TIP_Z))
|
||||||
|
var bull := other as Node
|
||||||
|
if bull != null and bull.is_in_group(&"player"):
|
||||||
|
bull.call(&"take_sword_hit", "thrown", contact)
|
||||||
|
if is_instance_valid(_mesh) and is_instance_valid(bull):
|
||||||
|
_mesh.reparent(bull, false)
|
||||||
|
_mesh.global_transform = pose
|
||||||
|
_mesh.add_to_group(_STUCK_GROUP)
|
||||||
|
if Settings.gore:
|
||||||
|
_mesh.add_child(_wound_emitter()) # keep bleeding from the wound while lodged
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
freeze = true
|
||||||
|
global_transform = pose
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dwjakh1is1jtj
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Headless focus tests for the dev console — verifies the command line keeps (or
|
||||||
|
## reclaims) keyboard focus after the actions that were observed to drop it.
|
||||||
|
## Run: godot --headless --script tests/console_focus_test.gd
|
||||||
|
##
|
||||||
|
## Exit code 0 = all passed, 1 = any failure.
|
||||||
|
|
||||||
|
var _passed: int = 0
|
||||||
|
var _failed: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
print("=".repeat(60))
|
||||||
|
print("CONSOLE FOCUS TESTS")
|
||||||
|
print("=".repeat(60))
|
||||||
|
|
||||||
|
await test_focus_after_submit()
|
||||||
|
await test_focus_after_external_steal()
|
||||||
|
|
||||||
|
print("=".repeat(60))
|
||||||
|
print("Results: %d passed, %d failed" % [_passed, _failed])
|
||||||
|
print("=".repeat(60))
|
||||||
|
|
||||||
|
quit(1 if _failed > 0 else 0)
|
||||||
|
|
||||||
|
|
||||||
|
func _assert_true(condition: bool, desc: String) -> void:
|
||||||
|
if condition:
|
||||||
|
_passed += 1
|
||||||
|
print(" PASS: %s" % desc)
|
||||||
|
else:
|
||||||
|
_failed += 1
|
||||||
|
print(" FAIL: %s" % desc)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_console() -> Node:
|
||||||
|
var c: Node = root.get_node_or_null("/root/Console")
|
||||||
|
if c == null:
|
||||||
|
_assert_true(false, "Console autoload should exist")
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
# Drive the real path: open via _toggle (pauses the tree, slide tween grabs focus),
|
||||||
|
# type a command as real key events, submit with a real Enter, and confirm the guard
|
||||||
|
# keeps the command line focused across the paused submit.
|
||||||
|
func test_focus_after_submit() -> void:
|
||||||
|
print("\n-- test_focus_after_submit --")
|
||||||
|
var menu := _get_console()
|
||||||
|
if menu == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
menu._toggle()
|
||||||
|
# Let the open slide tween finish and its grab_focus callback fire.
|
||||||
|
for _i in 20:
|
||||||
|
await process_frame
|
||||||
|
_assert_true(paused, "tree paused while console open")
|
||||||
|
_assert_true(menu._input_field.has_focus(), "input has focus after open")
|
||||||
|
|
||||||
|
menu._input_field.text = "clear"
|
||||||
|
menu._input_field.caret_column = 5
|
||||||
|
_press(KEY_ENTER)
|
||||||
|
# The submit and any focus theft settle over the next few idle frames.
|
||||||
|
for _i in 5:
|
||||||
|
await process_frame
|
||||||
|
_assert_true(menu._input_field.has_focus(), "input still has focus after submit")
|
||||||
|
|
||||||
|
menu._toggle()
|
||||||
|
for _i in 20:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
# Push a key down+up through the viewport so LineEdit / _input handle it for real.
|
||||||
|
func _press(keycode: Key) -> void:
|
||||||
|
var down := InputEventKey.new()
|
||||||
|
down.keycode = keycode
|
||||||
|
down.physical_keycode = keycode
|
||||||
|
down.pressed = true
|
||||||
|
root.push_input(down)
|
||||||
|
var up := InputEventKey.new()
|
||||||
|
up.keycode = keycode
|
||||||
|
up.physical_keycode = keycode
|
||||||
|
up.pressed = false
|
||||||
|
root.push_input(up)
|
||||||
|
|
||||||
|
|
||||||
|
# Simulate anything yanking focus away mid-session (a stray click, a UI button); the
|
||||||
|
# guard in _process must pull it straight back while the console is open.
|
||||||
|
func test_focus_after_external_steal() -> void:
|
||||||
|
print("\n-- test_focus_after_external_steal --")
|
||||||
|
var menu := _get_console()
|
||||||
|
if menu == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
menu._is_open = true
|
||||||
|
menu._panel.visible = true
|
||||||
|
menu._input_field.grab_focus()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
menu._input_field.release_focus()
|
||||||
|
_assert_true(not menu._input_field.has_focus(), "focus released (precondition)")
|
||||||
|
|
||||||
|
await process_frame
|
||||||
|
_assert_true(menu._input_field.has_focus(), "guard reclaims focus next frame")
|
||||||
|
|
||||||
|
menu._is_open = false
|
||||||
|
menu._panel.visible = false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://n6ymmhlqu6wp
|
||||||
@@ -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"]])
|
||||||