Compare commits
72 Commits
80a35a4553
...
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 | |||
| 708a35d51f | |||
| 115872b98f | |||
| 64124990a6 | |||
| 037bfad700 | |||
| 661b4e470b | |||
| b564cd5a71 | |||
| 7b5fcb1d1e | |||
| 45e69101b6 | |||
| a9810c8c9f | |||
| ba20cd8580 | |||
| aac0099faa | |||
| 4fc64dedae | |||
| 7423665c06 | |||
| 09c39e068b | |||
| c1852e8736 | |||
| dda9862589 | |||
| d538679697 | |||
| e668895c12 | |||
| 064f07ace7 | |||
| 9d0799083c | |||
| 2bea615307 | |||
| a61f4c4689 | |||
| f6355605cd | |||
| 033d26ee18 | |||
| d3315836c3 | |||
| 3279645ac4 | |||
| 371a2fded0 | |||
| 49a9bced4b | |||
| f0d932eac3 | |||
| ca1a0ce581 | |||
| 5ad4f64474 | |||
| 48cac53fc6 | |||
| 5c489318a9 | |||
| 1f24a750fb | |||
| 33ec51625c | |||
| 2e2b70e030 | |||
| 04597cdc01 | |||
| c407513cbd | |||
| ea065e4d81 | |||
| 0e654ec0b6 | |||
| df27e9d807 | |||
| 866cc12c2c | |||
| d98a5ba0ac | |||
| 6abf5355c2 | |||
| bfb026c7ae | |||
| 245ce31eab | |||
| b30fb6e0a6 | |||
| 950d93ed0e | |||
| 85b9c86164 | |||
| 174b14fa61 | |||
| 30136a9b90 |
@@ -0,0 +1,5 @@
|
|||||||
|
disable:
|
||||||
|
- class-definitions-order
|
||||||
|
- no-else-return
|
||||||
|
max-line-length: 120
|
||||||
|
max-file-lines: 1200
|
||||||
@@ -4,3 +4,18 @@
|
|||||||
|
|
||||||
# JetBrains IDE — contains absolute paths (Godot binary, SDK dirs), varies per user
|
# JetBrains IDE — contains absolute paths (Godot binary, SDK dirs), varies per user
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Blender render previews (generated by render_anim_preview.py)
|
||||||
|
Blender/preview/
|
||||||
|
|
||||||
|
Blender/*.blend1
|
||||||
|
|
||||||
|
# Test output (generated screenshots, frame strips)
|
||||||
|
tests/output/
|
||||||
|
|
||||||
|
# Web export output
|
||||||
|
build/
|
||||||
|
|
||||||
|
#PRISM
|
||||||
|
BullPrism/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group(&"public") # excluded from debug overlays — see publikum_controller.gd
|
||||||
|
var anim := get_node_or_null(^"AnimationPlayer") as AnimationPlayer
|
||||||
|
if anim:
|
||||||
|
anim.play(&"Juubeldab")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://wj6tgh8nb0ka
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
$AnimationPlayer.play("ArmatureAction")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://gt3kfchac3m3
|
||||||
@@ -3,19 +3,21 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://qy6qrxdfgx2g"
|
uid="uid://cl54dt38yj2x"
|
||||||
path="res://.godot/imported/bull_v10.fbx-0bb89c99686d56acf88cf41a200f2277.scn"
|
path="res://.godot/imported/Arena.glb-bf6b057edaab2fa44513d7f705517361.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Assets/bull_v10.fbx"
|
source_file="res://Assets/Arena.glb"
|
||||||
dest_files=["res://.godot/imported/bull_v10.fbx-0bb89c99686d56acf88cf41a200f2277.scn"]
|
dest_files=["res://.godot/imported/Arena.glb-bf6b057edaab2fa44513d7f705517361.scn"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -30,7 +32,7 @@ meshes/force_disable_compression=false
|
|||||||
skins/use_named_skins=true
|
skins/use_named_skins=true
|
||||||
animation/import=true
|
animation/import=true
|
||||||
animation/fps=30
|
animation/fps=30
|
||||||
animation/trimming=true
|
animation/trimming=false
|
||||||
animation/remove_immutable_tracks=true
|
animation/remove_immutable_tracks=true
|
||||||
animation/import_rest_as_RESET=false
|
animation/import_rest_as_RESET=false
|
||||||
import_script/path=""
|
import_script/path=""
|
||||||
@@ -38,7 +40,6 @@ materials/extract=0
|
|||||||
materials/extract_format=0
|
materials/extract_format=0
|
||||||
materials/extract_path=""
|
materials/extract_path=""
|
||||||
_subresources={}
|
_subresources={}
|
||||||
fbx/importer=0
|
gltf/naming_version=2
|
||||||
fbx/allow_geometry_helper_nodes=false
|
gltf/embedded_image_handling=1
|
||||||
fbx/embedded_image_handling=1
|
gltf/texture_map_mode=1
|
||||||
fbx/naming_version=2
|
|
||||||
|
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://eu2lqjhowj4t"
|
||||||
|
path="res://.godot/imported/Matador.glb-84387af784b13addc4467fc8edace9ff.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/Matador.glb"
|
||||||
|
dest_files=["res://.godot/imported/Matador.glb-84387af784b13addc4467fc8edace9ff.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,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
|
||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/arena_placeholder_v01.glb-fc11365d8ec7ae6778e
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -38,5 +40,6 @@ materials/extract=0
|
|||||||
materials/extract_format=0
|
materials/extract_format=0
|
||||||
materials/extract_path=""
|
materials/extract_path=""
|
||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=0
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
|
|||||||
@@ -3,19 +3,21 @@
|
|||||||
importer="scene"
|
importer="scene"
|
||||||
importer_version=1
|
importer_version=1
|
||||||
type="PackedScene"
|
type="PackedScene"
|
||||||
uid="uid://qix7axtc0lf3"
|
uid="uid://bun14xmk20pcj"
|
||||||
path="res://.godot/imported/matador_v02.fbx-220033ca52e262349a727eddc79dcf22.scn"
|
path="res://.godot/imported/bull.fbx-50f6d03a92c862695bbeaeba563be0db.scn"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://Blender/matador_v02.fbx"
|
source_file="res://Assets/bull.fbx"
|
||||||
dest_files=["res://.godot/imported/matador_v02.fbx-220033ca52e262349a727eddc79dcf22.scn"]
|
dest_files=["res://.godot/imported/bull.fbx-50f6d03a92c862695bbeaeba563be0db.scn"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ml0ntjv5injp"
|
||||||
|
path="res://.godot/imported/bull_hud_bg.png-4645abe7114935a3d4a8e0f88ccce15c.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/bull_hud_bg.png"
|
||||||
|
dest_files=["res://.godot/imported/bull_hud_bg.png-4645abe7114935a3d4a8e0f88ccce15c.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
After Width: | Height: | Size: 115 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d166u18p3wqiy"
|
||||||
|
path="res://.godot/imported/crowd_clap_sheet.png-a110e7d663af8fcbe9ebf5de44188c85.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/crowd_clap_sheet.png"
|
||||||
|
dest_files=["res://.godot/imported/crowd_clap_sheet.png-a110e7d663af8fcbe9ebf5de44188c85.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 @@
|
|||||||
|
uid://csxxlnq74fej4
|
||||||
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 12 MiB |
@@ -0,0 +1,5 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
$AnimationPlayer.play("Taunt")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bn3xwnlktwdlf
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bl6jlbq1dpkc"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="1_m7x6l"]
|
||||||
|
[ext_resource type="Script" uid="uid://dpoq87w5pvqa" path="res://matador_2.gd" id="2_mftd2"]
|
||||||
|
|
||||||
|
[node name="matador2" unique_id=1179710042 instance=ExtResource("1_m7x6l")]
|
||||||
|
script = ExtResource("2_mftd2")
|
||||||
|
|
||||||
|
[node name="Skeleton3D" parent="Armature" parent_id_path=PackedInt32Array(1269629432) index="0" unique_id=978366810]
|
||||||
|
bones/0/position = Vector3(0.056660548, 0.6424767, -0.0167365)
|
||||||
|
bones/0/rotation = Quaternion(0.080643006, 0, 0, 0.996743)
|
||||||
|
bones/1/rotation = Quaternion(0.083861314, 0, 0, 0.9964774)
|
||||||
|
bones/3/rotation = Quaternion(0.04833339, 0.5302496, -0.71466804, 0.4535955)
|
||||||
|
bones/4/rotation = Quaternion(0.031473875, -0.17018215, -0.15981929, 0.97185665)
|
||||||
|
bones/5/rotation = Quaternion(0.6747468, -0.021427644, -0.06245957, 0.7350895)
|
||||||
|
bones/6/rotation = Quaternion(3.1828083e-08, -9.5969504e-08, -0.0587008, 0.99827564)
|
||||||
|
bones/7/rotation = Quaternion(0.1907551, 0.20328633, 0.7258648, 0.6288143)
|
||||||
|
bones/8/rotation = Quaternion(0.16639966, -0.05474137, 0.010937729, 0.984477)
|
||||||
|
bones/9/rotation = Quaternion(0.5914298, 0.005839911, -0.041949864, 0.8052435)
|
||||||
|
bones/10/rotation = Quaternion(2.3105285e-08, 1.0152064e-07, 0.05870079, 0.99827564)
|
||||||
|
bones/11/rotation = Quaternion(0.39202613, 0.47060952, 0.5799399, 0.53713316)
|
||||||
|
bones/12/rotation = Quaternion(0.62021786, 0.40894383, 0.44191495, 0.5027981)
|
||||||
|
bones/13/rotation = Quaternion(-0.11584477, 0.0021730124, 0.6343679, 0.7642989)
|
||||||
|
bones/14/rotation = Quaternion(0.1454883, 0.14013468, -0.7254941, 0.65791625)
|
||||||
|
bones/15/rotation = Quaternion(0.71668345, 0.065085255, -0.6923973, -0.05210323)
|
||||||
|
bones/16/rotation = Quaternion(-0.10161893, -0.0019664483, -0.65947086, 0.7448275)
|
||||||
|
bones/17/rotation = Quaternion(0.06964938, -0.042099692, 0.74388325, 0.6633358)
|
||||||
|
bones/18/rotation = Quaternion(0.5305965, -0.28927496, 0.76720744, -0.21489602)
|
||||||
|
bones/19/position = Vector3(0.16341288, 0.31487355, 0.3966543)
|
||||||
|
bones/20/position = Vector3(0.22012731, 0.38421977, 0.95168185)
|
||||||
|
bones/21/position = Vector3(-0.09492579, 0.40446192, -0.46390092)
|
||||||
|
bones/21/rotation = Quaternion(0.7216726, 0.064735115, -0.6891729, 0.0062273443)
|
||||||
|
bones/22/position = Vector3(-0.22012731, 0.38421977, 0.95168185)
|
||||||
|
bones/23/position = Vector3(-0.0382514, 1.7423625, 0.0026792884)
|
||||||
|
bones/23/rotation = Quaternion(0.8626007, 0.0068245474, -0.099159576, -0.49602506)
|
||||||
|
bones/23/scale = Vector3(0.6647317, 1.001677, 1.0066445)
|
||||||
|
bones/24/rotation = Quaternion(-0.007878712, 8.3020435e-09, -0.0023950166, 0.9999661)
|
||||||
|
bones/25/rotation = Quaternion(5.5870206e-09, 5.5879354e-09, -2.0518176e-09, 1)
|
||||||
@@ -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 |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bxv1uuymonrp6"
|
||||||
|
path="res://.godot/imported/menu_bg.png-3906063022fa3c81090e3c663f8866b0.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/menu_bg.png"
|
||||||
|
dest_files=["res://.godot/imported/menu_bg.png-3906063022fa3c81090e3c663f8866b0.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,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://jepc74w8opbu"
|
||||||
|
path="res://.godot/imported/spear.glb-0139879d219c93f9dd35db2304db7d5d.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/spear.glb"
|
||||||
|
dest_files=["res://.godot/imported/spear.glb-0139879d219c93f9dd35db2304db7d5d.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://xyvank512l5n"
|
||||||
|
path="res://.godot/imported/sword.glb-a20d99f4c597d7fdae7f36c74685e1ff.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/sword.glb"
|
||||||
|
dest_files=["res://.godot/imported/sword.glb-a20d99f4c597d7fdae7f36c74685e1ff.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
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="scene"
|
|
||||||
importer_version=1
|
|
||||||
type="PackedScene"
|
|
||||||
uid="uid://b203q5pgrk5s3"
|
|
||||||
path="res://.godot/imported/bull_v11.blend-0dd387f5e964c51fff069fa50a99986e.scn"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://Blender/bull_v11.blend"
|
|
||||||
dest_files=["res://.godot/imported/bull_v11.blend-0dd387f5e964c51fff069fa50a99986e.scn"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
nodes/root_type=""
|
|
||||||
nodes/root_name=""
|
|
||||||
nodes/root_script=null
|
|
||||||
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={}
|
|
||||||
blender/nodes/visible=0
|
|
||||||
blender/nodes/active_collection_only=false
|
|
||||||
blender/nodes/punctual_lights=true
|
|
||||||
blender/nodes/cameras=true
|
|
||||||
blender/nodes/custom_properties=true
|
|
||||||
blender/nodes/modifiers=1
|
|
||||||
blender/meshes/colors=false
|
|
||||||
blender/meshes/uvs=true
|
|
||||||
blender/meshes/normals=true
|
|
||||||
blender/meshes/export_geometry_nodes_instances=false
|
|
||||||
blender/meshes/gpu_instances=false
|
|
||||||
blender/meshes/tangents=true
|
|
||||||
blender/meshes/skins=2
|
|
||||||
blender/meshes/export_bones_deforming_mesh_only=false
|
|
||||||
blender/materials/unpack_enabled=true
|
|
||||||
blender/materials/export_materials=1
|
|
||||||
blender/animation/limit_playback=true
|
|
||||||
blender/animation/always_sample=true
|
|
||||||
blender/animation/group_tracks=true
|
|
||||||
gltf/naming_version=2
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
Blender 5.1 script: create walk + idle animations for matador_v02.
|
||||||
|
Run: blender --background matador_v02.blend --python create_matador_anims.py
|
||||||
|
"""
|
||||||
|
import bpy, math, os
|
||||||
|
|
||||||
|
|
||||||
|
# ── setup ──────────────────────────────────────────────────────────────────────
|
||||||
|
arm_obj = next(o for o in bpy.data.objects if o.type == 'ARMATURE')
|
||||||
|
bpy.context.view_layer.objects.active = arm_obj
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
|
if arm_obj.animation_data is None:
|
||||||
|
arm_obj.animation_data_create()
|
||||||
|
|
||||||
|
pb = arm_obj.pose.bones
|
||||||
|
|
||||||
|
# Ensure all animated bones use XYZ Euler
|
||||||
|
for bname in ['leg_L', 'leg_R', 'shin_L', 'shin_R',
|
||||||
|
'arm_L', 'arm_R', 'forearm_L', 'forearm_R',
|
||||||
|
'collarbone_L', 'collarbone_R']:
|
||||||
|
pb[bname].rotation_mode = 'XYZ'
|
||||||
|
|
||||||
|
CYCLE = 30 # frames (1 s at 30 fps)
|
||||||
|
LEG_SWING = 25 # degrees
|
||||||
|
KNEE_BEND = 30
|
||||||
|
ARM_SWING = 15
|
||||||
|
ARM_DOWN = 55 # lower arms from T-pose around bone-local Y
|
||||||
|
ELBOW_BEND = 25
|
||||||
|
|
||||||
|
|
||||||
|
# ── WALK action ────────────────────────────────────────────────────────────────
|
||||||
|
# Remove old actions if they exist
|
||||||
|
for old_name in ['walk', 'idle']:
|
||||||
|
if old_name in bpy.data.actions:
|
||||||
|
bpy.data.actions.remove(bpy.data.actions[old_name])
|
||||||
|
|
||||||
|
walk = bpy.data.actions.new(name='walk')
|
||||||
|
walk.use_fake_user = True
|
||||||
|
walk.use_cyclic = True
|
||||||
|
arm_obj.animation_data.action = walk
|
||||||
|
|
||||||
|
# Clear all pose bone transforms first
|
||||||
|
bpy.ops.pose.select_all(action='SELECT')
|
||||||
|
bpy.ops.pose.rot_clear()
|
||||||
|
|
||||||
|
arm_down_l = math.radians(ARM_DOWN)
|
||||||
|
arm_down_r = math.radians(-ARM_DOWN)
|
||||||
|
|
||||||
|
for f in range(0, CYCLE + 1, 2):
|
||||||
|
t = (f / CYCLE) * math.tau
|
||||||
|
|
||||||
|
# Legs – swing forward/back on bone-local X
|
||||||
|
pb['leg_L'].rotation_euler = (math.sin(t) * math.radians(LEG_SWING), 0, 0)
|
||||||
|
pb['leg_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
pb['leg_R'].rotation_euler = (math.sin(t + math.pi) * math.radians(LEG_SWING), 0, 0)
|
||||||
|
pb['leg_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
# Shins – only bend backward when leg is back
|
||||||
|
val_l = max(0.0, -math.sin(t)) * math.radians(KNEE_BEND)
|
||||||
|
val_r = max(0.0, -math.sin(t + math.pi)) * math.radians(KNEE_BEND)
|
||||||
|
pb['shin_L'].rotation_euler = (val_l, 0, 0)
|
||||||
|
pb['shin_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['shin_R'].rotation_euler = (val_r, 0, 0)
|
||||||
|
pb['shin_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
# Arms – lower from T-pose (Y) + swing (X, opposite phase to legs)
|
||||||
|
swing_l = math.sin(t + math.pi) * math.radians(ARM_SWING)
|
||||||
|
swing_r = math.sin(t) * math.radians(ARM_SWING)
|
||||||
|
pb['arm_L'].rotation_euler = (swing_l, arm_down_l, 0)
|
||||||
|
pb['arm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['arm_R'].rotation_euler = (swing_r, arm_down_r, 0)
|
||||||
|
pb['arm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
# Forearms – constant slight bend
|
||||||
|
pb['forearm_L'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
|
||||||
|
pb['forearm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['forearm_R'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
|
||||||
|
pb['forearm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
# Collarbones – identity
|
||||||
|
pb['collarbone_L'].rotation_euler = (0, 0, 0)
|
||||||
|
pb['collarbone_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['collarbone_R'].rotation_euler = (0, 0, 0)
|
||||||
|
pb['collarbone_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
print("Created 'walk' action")
|
||||||
|
|
||||||
|
|
||||||
|
# ── IDLE action ────────────────────────────────────────────────────────────────
|
||||||
|
idle = bpy.data.actions.new(name='idle')
|
||||||
|
idle.use_fake_user = True
|
||||||
|
arm_obj.animation_data.action = idle
|
||||||
|
|
||||||
|
bpy.ops.pose.select_all(action='SELECT')
|
||||||
|
bpy.ops.pose.rot_clear()
|
||||||
|
|
||||||
|
# Arms lowered, everything else at rest – 2 frames for a valid action
|
||||||
|
for f in [0, 1]:
|
||||||
|
pb['arm_L'].rotation_euler = (0, arm_down_l, 0)
|
||||||
|
pb['arm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['arm_R'].rotation_euler = (0, arm_down_r, 0)
|
||||||
|
pb['arm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['forearm_L'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
|
||||||
|
pb['forearm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['forearm_R'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
|
||||||
|
pb['forearm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
for bname in ['collarbone_L', 'collarbone_R', 'leg_L', 'leg_R', 'shin_L', 'shin_R']:
|
||||||
|
pb[bname].rotation_euler = (0, 0, 0)
|
||||||
|
pb[bname].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
print("Created 'idle' action")
|
||||||
|
|
||||||
|
|
||||||
|
# ── NLA tracks for export ─────────────────────────────────────────────────────
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
# Clear existing NLA tracks
|
||||||
|
if arm_obj.animation_data.nla_tracks:
|
||||||
|
for t in list(arm_obj.animation_data.nla_tracks):
|
||||||
|
arm_obj.animation_data.nla_tracks.remove(t)
|
||||||
|
|
||||||
|
tw = arm_obj.animation_data.nla_tracks.new()
|
||||||
|
tw.name = 'walk'
|
||||||
|
sw = tw.strips.new('walk', 0, walk)
|
||||||
|
|
||||||
|
ti = arm_obj.animation_data.nla_tracks.new()
|
||||||
|
ti.name = 'idle'
|
||||||
|
si = ti.strips.new('idle', 0, idle)
|
||||||
|
|
||||||
|
# ── export FBX ─────────────────────────────────────────────────────────────────
|
||||||
|
fbx_path = os.path.join(os.path.dirname(bpy.data.filepath), 'matador_v02.fbx')
|
||||||
|
bpy.ops.export_scene.fbx(
|
||||||
|
filepath=fbx_path,
|
||||||
|
use_selection=False,
|
||||||
|
bake_anim=True,
|
||||||
|
bake_anim_use_all_actions=True,
|
||||||
|
bake_anim_use_nla_strips=False,
|
||||||
|
bake_anim_force_startend_keying=True,
|
||||||
|
bake_anim_step=1.0,
|
||||||
|
bake_anim_simplify_factor=0.0,
|
||||||
|
add_leaf_bones=False,
|
||||||
|
primary_bone_axis='Y',
|
||||||
|
secondary_bone_axis='X',
|
||||||
|
apply_scale_options='FBX_SCALE_ALL',
|
||||||
|
)
|
||||||
|
print('Exported FBX to:', fbx_path)
|
||||||
|
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
|
||||||
|
print('Saved .blend')
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ymaeifakqovf
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="scene"
|
|
||||||
importer_version=1
|
|
||||||
type="PackedScene"
|
|
||||||
uid="uid://c6yhxdaba1nho"
|
|
||||||
path="res://.godot/imported/matador_v01.blend-ca770df72393bcfc88aeadbf3f984fc9.scn"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://Blender/matador_v01.blend"
|
|
||||||
dest_files=["res://.godot/imported/matador_v01.blend-ca770df72393bcfc88aeadbf3f984fc9.scn"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
nodes/root_type=""
|
|
||||||
nodes/root_name=""
|
|
||||||
nodes/root_script=null
|
|
||||||
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={}
|
|
||||||
blender/nodes/visible=0
|
|
||||||
blender/nodes/active_collection_only=false
|
|
||||||
blender/nodes/punctual_lights=true
|
|
||||||
blender/nodes/cameras=true
|
|
||||||
blender/nodes/custom_properties=true
|
|
||||||
blender/nodes/modifiers=1
|
|
||||||
blender/meshes/colors=false
|
|
||||||
blender/meshes/uvs=true
|
|
||||||
blender/meshes/normals=true
|
|
||||||
blender/meshes/export_geometry_nodes_instances=false
|
|
||||||
blender/meshes/gpu_instances=false
|
|
||||||
blender/meshes/tangents=true
|
|
||||||
blender/meshes/skins=2
|
|
||||||
blender/meshes/export_bones_deforming_mesh_only=false
|
|
||||||
blender/materials/unpack_enabled=true
|
|
||||||
blender/materials/export_materials=1
|
|
||||||
blender/animation/limit_playback=true
|
|
||||||
blender/animation/always_sample=true
|
|
||||||
blender/animation/group_tracks=true
|
|
||||||
gltf/naming_version=2
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="scene"
|
|
||||||
importer_version=1
|
|
||||||
type="PackedScene"
|
|
||||||
uid="uid://cbkylf6ba2qmw"
|
|
||||||
path="res://.godot/imported/matador_v02.blend-78c935c19487a89cf2e8e43e696513b9.scn"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://Blender/matador_v02.blend"
|
|
||||||
dest_files=["res://.godot/imported/matador_v02.blend-78c935c19487a89cf2e8e43e696513b9.scn"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
nodes/root_type=""
|
|
||||||
nodes/root_name=""
|
|
||||||
nodes/root_script=null
|
|
||||||
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={}
|
|
||||||
blender/nodes/visible=0
|
|
||||||
blender/nodes/active_collection_only=false
|
|
||||||
blender/nodes/punctual_lights=true
|
|
||||||
blender/nodes/cameras=true
|
|
||||||
blender/nodes/custom_properties=true
|
|
||||||
blender/nodes/modifiers=1
|
|
||||||
blender/meshes/colors=false
|
|
||||||
blender/meshes/uvs=true
|
|
||||||
blender/meshes/normals=true
|
|
||||||
blender/meshes/export_geometry_nodes_instances=false
|
|
||||||
blender/meshes/gpu_instances=false
|
|
||||||
blender/meshes/tangents=true
|
|
||||||
blender/meshes/skins=2
|
|
||||||
blender/meshes/export_bones_deforming_mesh_only=false
|
|
||||||
blender/materials/unpack_enabled=true
|
|
||||||
blender/materials/export_materials=1
|
|
||||||
blender/animation/limit_playback=true
|
|
||||||
blender/animation/always_sample=true
|
|
||||||
blender/animation/group_tracks=true
|
|
||||||
gltf/naming_version=2
|
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
Blender 5.1 script: render preview frames of matador animations to PNG.
|
||||||
|
Run: blender --background matador_v02.blend --python render_anim_preview.py
|
||||||
|
|
||||||
|
Outputs to Blender/preview/ directory:
|
||||||
|
walk_frame_00.png .. walk_frame_09.png
|
||||||
|
idle_frame_00.png
|
||||||
|
|
||||||
|
Use this to visually verify bone orientations BEFORE exporting to Godot.
|
||||||
|
"""
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
|
||||||
|
# ── Configuration ─────────────────────────────────────────────────────────────
|
||||||
|
PREVIEW_DIR = os.path.join(os.path.dirname(bpy.data.filepath), "preview")
|
||||||
|
RENDER_RES_X = 512
|
||||||
|
RENDER_RES_Y = 512
|
||||||
|
WALK_FRAMES = 10 # number of evenly-spaced frames to render from walk cycle
|
||||||
|
CAMERA_DISTANCE = 5.0
|
||||||
|
CAMERA_HEIGHT = 1.0
|
||||||
|
CAMERA_ANGLES = [0, 90] # degrees — front and side views
|
||||||
|
|
||||||
|
os.makedirs(PREVIEW_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# ── Find armature ─────────────────────────────────────────────────────────────
|
||||||
|
arm_obj = next((o for o in bpy.data.objects if o.type == 'ARMATURE'), None)
|
||||||
|
if arm_obj is None:
|
||||||
|
raise RuntimeError("No armature found in scene")
|
||||||
|
|
||||||
|
# ── Setup render settings ─────────────────────────────────────────────────────
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.render.resolution_x = RENDER_RES_X
|
||||||
|
scene.render.resolution_y = RENDER_RES_Y
|
||||||
|
scene.render.resolution_percentage = 100
|
||||||
|
scene.render.film_transparent = True
|
||||||
|
scene.render.image_settings.file_format = 'PNG'
|
||||||
|
scene.render.image_settings.color_mode = 'RGBA'
|
||||||
|
scene.render.engine = 'BLENDER_EEVEE'
|
||||||
|
|
||||||
|
# ── Create or reuse camera ────────────────────────────────────────────────────
|
||||||
|
cam_data = bpy.data.cameras.get("PreviewCam") or bpy.data.cameras.new("PreviewCam")
|
||||||
|
cam_data.lens = 50
|
||||||
|
cam_obj = bpy.data.objects.get("PreviewCamObj")
|
||||||
|
if cam_obj is None:
|
||||||
|
cam_obj = bpy.data.objects.new("PreviewCamObj", cam_data)
|
||||||
|
scene.collection.objects.link(cam_obj)
|
||||||
|
scene.camera = cam_obj
|
||||||
|
|
||||||
|
# ── Create or reuse key light ─────────────────────────────────────────────────
|
||||||
|
light_data = bpy.data.lights.get("PreviewLight") or bpy.data.lights.new("PreviewLight", type='SUN')
|
||||||
|
light_data.energy = 3.0
|
||||||
|
light_obj = bpy.data.objects.get("PreviewLightObj")
|
||||||
|
if light_obj is None:
|
||||||
|
light_obj = bpy.data.objects.new("PreviewLightObj", light_data)
|
||||||
|
scene.collection.objects.link(light_obj)
|
||||||
|
light_obj.rotation_euler = (math.radians(45), math.radians(30), 0)
|
||||||
|
|
||||||
|
# ── Create or reuse fill light ────────────────────────────────────────────────
|
||||||
|
fill_data = bpy.data.lights.get("PreviewFill") or bpy.data.lights.new("PreviewFill", type='SUN')
|
||||||
|
fill_data.energy = 1.0
|
||||||
|
fill_obj = bpy.data.objects.get("PreviewFillObj")
|
||||||
|
if fill_obj is None:
|
||||||
|
fill_obj = bpy.data.objects.new("PreviewFillObj", fill_data)
|
||||||
|
scene.collection.objects.link(fill_obj)
|
||||||
|
fill_obj.rotation_euler = (math.radians(60), math.radians(-45), 0)
|
||||||
|
|
||||||
|
# ── Ensure armature is visible ────────────────────────────────────────────────
|
||||||
|
arm_obj.hide_render = False
|
||||||
|
arm_obj.hide_viewport = False
|
||||||
|
for child in arm_obj.children:
|
||||||
|
child.hide_render = False
|
||||||
|
child.hide_viewport = False
|
||||||
|
|
||||||
|
|
||||||
|
def position_camera(angle_deg: float) -> None:
|
||||||
|
"""Place camera at given horizontal angle around the armature center."""
|
||||||
|
rad = math.radians(angle_deg)
|
||||||
|
cam_obj.location = (
|
||||||
|
math.sin(rad) * CAMERA_DISTANCE,
|
||||||
|
CAMERA_HEIGHT,
|
||||||
|
math.cos(rad) * CAMERA_DISTANCE,
|
||||||
|
)
|
||||||
|
# Point camera at armature center (roughly hip height)
|
||||||
|
target = arm_obj.location.copy()
|
||||||
|
target.y = 0.9
|
||||||
|
direction = target - cam_obj.location
|
||||||
|
rot_quat = direction.to_track_quat('-Z', 'Y')
|
||||||
|
cam_obj.rotation_euler = rot_quat.to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def render_action(action_name: str, frame_count: int) -> None:
|
||||||
|
"""Render frame_count evenly-spaced frames of the named action."""
|
||||||
|
action = bpy.data.actions.get(action_name)
|
||||||
|
if action is None:
|
||||||
|
print(f"WARNING: Action '{action_name}' not found, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Assign action
|
||||||
|
if arm_obj.animation_data is None:
|
||||||
|
arm_obj.animation_data_create()
|
||||||
|
arm_obj.animation_data.action = action
|
||||||
|
|
||||||
|
frame_start = int(action.frame_range[0])
|
||||||
|
frame_end = int(action.frame_range[1])
|
||||||
|
total_frames = max(frame_end - frame_start, 1)
|
||||||
|
|
||||||
|
for angle_deg in CAMERA_ANGLES:
|
||||||
|
position_camera(angle_deg)
|
||||||
|
angle_label = f"{angle_deg:03d}deg"
|
||||||
|
|
||||||
|
for i in range(frame_count):
|
||||||
|
t = i / max(frame_count - 1, 1) if frame_count > 1 else 0
|
||||||
|
frame = frame_start + int(t * total_frames)
|
||||||
|
scene.frame_set(frame)
|
||||||
|
|
||||||
|
filename = f"{action_name}_{angle_label}_frame_{i:02d}.png"
|
||||||
|
filepath = os.path.join(PREVIEW_DIR, filename)
|
||||||
|
scene.render.filepath = filepath
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
print(f" Rendered: {filename}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Render all animations ─────────────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("RENDERING ANIMATION PREVIEWS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
render_action("walk", WALK_FRAMES)
|
||||||
|
render_action("idle", 1)
|
||||||
|
|
||||||
|
# ── Cleanup preview objects (optional — keep them for re-runs) ────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Preview images saved to: {PREVIEW_DIR}")
|
||||||
|
print(f"Total actions rendered: {len([a for a in ['walk', 'idle'] if bpy.data.actions.get(a)])}")
|
||||||
|
print("=" * 60)
|
||||||
@@ -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
|
||||||
|
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)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[gd_scene format=3 uid="uid://matador_scene_v1"]
|
[gd_scene format=3 uid="uid://matador_scene_v1"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://matador.gd" id="1_matgd"]
|
[ext_resource type="Script" path="res://matador.gd" id="1_matgd"]
|
||||||
[ext_resource type="PackedScene" uid="uid://qix7axtc0lf3" path="res://Blender/matador_v02.fbx" id="2_matfbx"]
|
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="2_matfbx"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
||||||
radius = 0.25
|
radius = 0.25
|
||||||
@@ -26,4 +26,4 @@ monitorable = false
|
|||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||||
shape = SubResource("CapsuleShape3D_hit")
|
shape = SubResource("CapsuleShape3D_hit")
|
||||||
|
|
||||||
[node name="matador_v02" parent="." instance=ExtResource("2_matfbx")]
|
[node name="matador2" parent="." instance=ExtResource("2_matfbx")]
|
||||||
|
|||||||
@@ -1,57 +1,38 @@
|
|||||||
[gd_scene format=3 uid="uid://crtm0xtiql3r8"]
|
[gd_scene format=3 uid="uid://crtm0xtiql3r8"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bn777gguclud5" path="res://player.gd" id="1_xhfnw"]
|
[ext_resource type="Script" uid="uid://bn777gguclud5" path="res://player.gd" id="1_xhfnw"]
|
||||||
[ext_resource type="Script" uid="uid://b4td4k8yo5y78" path="res://camera_spring_arm.gd" id="2_kpjcp"]
|
[ext_resource type="Script" uid="uid://cpyq2r5cq653g" path="res://camera_iso.gd" id="2_camera_iso"]
|
||||||
[ext_resource type="Script" uid="uid://b720l6m0ehrj3" path="res://camera_follow.gd" id="3_kne1u"]
|
[ext_resource type="PackedScene" uid="uid://bun14xmk20pcj" path="res://Assets/bull.fbx" id="4_e80uo"]
|
||||||
[ext_resource type="PackedScene" uid="uid://qy6qrxdfgx2g" path="res://Assets/bull_v10.fbx" id="4_e80uo"]
|
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_kne1u"]
|
|
||||||
radius = 0.5
|
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_chest"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_chest"]
|
||||||
radius = 0.35
|
radius = 0.35
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_belly"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_belly"]
|
||||||
radius = 0.40
|
radius = 0.4
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_hips"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_hips"]
|
||||||
radius = 0.30
|
radius = 0.3
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody3D" unique_id=165510044]
|
[node name="Player" type="CharacterBody3D" unique_id=165510044]
|
||||||
collision_layer = 3
|
collision_layer = 3
|
||||||
script = ExtResource("1_xhfnw")
|
script = ExtResource("1_xhfnw")
|
||||||
|
|
||||||
[node name="CollisionChest" type="CollisionShape3D" parent="."]
|
[node name="CollisionChest" type="CollisionShape3D" parent="." unique_id=447032296]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.2)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, -0.2)
|
||||||
shape = SubResource("SphereShape3D_chest")
|
shape = SubResource("SphereShape3D_chest")
|
||||||
|
|
||||||
[node name="CollisionBelly" type="CollisionShape3D" parent="."]
|
[node name="CollisionBelly" type="CollisionShape3D" parent="." unique_id=571780801]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.1)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.1)
|
||||||
shape = SubResource("SphereShape3D_belly")
|
shape = SubResource("SphereShape3D_belly")
|
||||||
|
|
||||||
[node name="CollisionHips" type="CollisionShape3D" parent="."]
|
[node name="CollisionHips" type="CollisionShape3D" parent="." unique_id=717874116]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.4)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.4)
|
||||||
shape = SubResource("SphereShape3D_hips")
|
shape = SubResource("SphereShape3D_hips")
|
||||||
|
|
||||||
[node name="SpringArmPivot" type="Node3D" parent="." unique_id=1671817680]
|
[node name="Camera3D" type="Camera3D" parent="." unique_id=1120161087]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.22, 0)
|
script = ExtResource("2_camera_iso")
|
||||||
script = ExtResource("2_kpjcp")
|
|
||||||
|
|
||||||
[node name="SpringArm3D" type="SpringArm3D" parent="SpringArmPivot" unique_id=141439848]
|
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
|
||||||
collision_mask = 3
|
|
||||||
shape = SubResource("SphereShape3D_kne1u")
|
|
||||||
spring_length = 7.0
|
|
||||||
margin = 0.5
|
|
||||||
|
|
||||||
[node name="SpringPosition" type="Node3D" parent="SpringArmPivot/SpringArm3D" unique_id=1307131677]
|
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="SpringArmPivot" unique_id=1120161087 node_paths=PackedStringArray("spring_arm")]
|
|
||||||
keep_aspect = 0
|
|
||||||
script = ExtResource("3_kne1u")
|
|
||||||
spring_arm = NodePath("../SpringArm3D/SpringPosition")
|
|
||||||
lerp_power = 6.0
|
|
||||||
|
|
||||||
[node name="bull_v10" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
|
||||||
|
|
||||||
[editable path="bull_v10"]
|
[editable path="bull"]
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
extends AnimationPlayer
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
speed_scale = randf_range(0.5, 1.5)
|
||||||
|
play("ArmatureAction")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b6gdjsr6fcwc2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://sbf260u32upu
|
||||||
@@ -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
|
||||||
@@ -1,53 +1,23 @@
|
|||||||
extends Node
|
extends Node
|
||||||
## Procedural leg stepping, body wobble, and tail simulation.
|
## Body wobble and tail simulation. Legs stay in rest pose (no IK, no ragdoll).
|
||||||
## Created at runtime by player.gd; call setup() before the first _process tick.
|
## Created at runtime by player.gd; call setup() before the first _process tick.
|
||||||
|
|
||||||
# ── Leg State Machine ─────────────────────────────────────────────────────────
|
|
||||||
enum LegState { GALLOP, LAUNCH, RAGDOLL, TUCK, LAND }
|
|
||||||
|
|
||||||
# Leg IK config — rest offsets are in bull_v10 local space
|
|
||||||
const LEG_CFG: Dictionary = {
|
|
||||||
"FL": { "root": &"front_leg_1_L", "tip": &"front_leg_2_L_end",
|
|
||||||
"rest": Vector3(-0.30, -0.25, -0.50) },
|
|
||||||
"FR": { "root": &"front_leg_1_R", "tip": &"front_leg_2_R_end",
|
|
||||||
"rest": Vector3( 0.30, -0.25, -0.50) },
|
|
||||||
"BL": { "root": &"rear_leg_1_L", "tip": &"rear_leg_3_L_end",
|
|
||||||
"rest": Vector3(-0.30, -0.25, 0.60) },
|
|
||||||
"BR": { "root": &"rear_leg_1_R", "tip": &"rear_leg_3_R_end",
|
|
||||||
"rest": Vector3( 0.30, -0.25, 0.60) },
|
|
||||||
}
|
|
||||||
|
|
||||||
# Gallop gait phase offset per leg (0.0–1.0 cycle).
|
|
||||||
const LEG_PHASES: Dictionary = { "BL": 0.0, "FL": 0.2, "BR": 0.5, "FR": 0.7 }
|
|
||||||
|
|
||||||
# Per-leg ragdoll phase offsets for variety
|
|
||||||
const RAGDOLL_PHASE: Dictionary = { "FL": 0.0, "FR": 1.3, "BL": 2.6, "BR": 3.9 }
|
|
||||||
|
|
||||||
var _player: CharacterBody3D
|
var _player: CharacterBody3D
|
||||||
var _bull: Node3D
|
var _bull: Node3D
|
||||||
var _skeleton: Skeleton3D
|
var _skeleton: Skeleton3D
|
||||||
|
|
||||||
# Per-leg state
|
|
||||||
var _ik: Dictionary = {} # id -> SkeletonIK3D
|
|
||||||
var _foot: Dictionary = {} # id -> current foot (world)
|
|
||||||
var _step_from: Dictionary = {} # id -> Vector3
|
|
||||||
var _step_to: Dictionary = {} # id -> Vector3
|
|
||||||
var _step_t: Dictionary = {} # id -> float (1.0 = planted)
|
|
||||||
|
|
||||||
# Body wobble
|
# Body wobble
|
||||||
var _prev_vel: Vector3 = Vector3.ZERO
|
var _prev_vel: Vector3 = Vector3.ZERO
|
||||||
var _tilt_x: float = 0.0
|
var _tilt_x: float = 0.0
|
||||||
var _tilt_z: float = 0.0
|
var _tilt_z: float = 0.0
|
||||||
var _aerial_pitch: float = 0.0
|
var _aerial_pitch: float = 0.0
|
||||||
|
|
||||||
# Leg state machine
|
var charge_pitch_target: float = 0.0
|
||||||
var _leg_state: LegState = LegState.GALLOP
|
var _charge_pitch_curr: float = 0.0
|
||||||
var _state_time: float = 0.0 # time spent in current state
|
|
||||||
var _was_on_floor: bool = true
|
|
||||||
var _launch_feet: Dictionary = {} # frozen foot positions at launch
|
|
||||||
|
|
||||||
# Gait
|
# When true, another system (the roll's ball-tumble) owns the bull's full
|
||||||
var _gait_phase: float = 0.0
|
# 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 = [
|
||||||
@@ -55,11 +25,18 @@ const TAIL_BONES: Array = [
|
|||||||
&"tail_5", &"tail_6",
|
&"tail_5", &"tail_6",
|
||||||
&"tail_tip_1", &"tail_tip_2", &"tail_tip_2_end",
|
&"tail_tip_1", &"tail_tip_2", &"tail_tip_2_end",
|
||||||
]
|
]
|
||||||
var _tail_idx: Array[int] = []
|
var _tail_idx: Array[int] = []
|
||||||
var _tail_world: Array[Vector3] = []
|
var _tail_world: Array[Vector3] = []
|
||||||
var _tail_prev: Array[Vector3] = []
|
var _tail_prev: Array[Vector3] = []
|
||||||
var _tail_lengths: Array[float] = []
|
var _tail_lengths: Array[float] = []
|
||||||
var _tail_rest_dir: Array[Vector3] = []
|
var _tail_rest_dir: Array[Vector3] = []
|
||||||
|
var _tail_parent_idx: int = -1
|
||||||
|
|
||||||
|
var tail_ragdoll: bool = false
|
||||||
|
var _prev_on_floor: bool = true
|
||||||
|
|
||||||
|
var _dbg_on: bool = false
|
||||||
|
var _dbg_nodes: Array[MeshInstance3D] = []
|
||||||
|
|
||||||
|
|
||||||
func setup(player: CharacterBody3D, bull: Node3D) -> void:
|
func setup(player: CharacterBody3D, bull: Node3D) -> void:
|
||||||
@@ -67,34 +44,16 @@ func setup(player: CharacterBody3D, bull: Node3D) -> void:
|
|||||||
_bull = bull
|
_bull = bull
|
||||||
_skeleton = _find_skeleton(bull)
|
_skeleton = _find_skeleton(bull)
|
||||||
if not _skeleton:
|
if not _skeleton:
|
||||||
push_error("BullLegs: Skeleton3D not found under bull_v10")
|
push_error("BullLegs: Skeleton3D not found under bull")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Disable any physical bone simulator — we don't need it
|
# Disable any physical bone simulator
|
||||||
var phys_sim := _find_physical_bones(_skeleton)
|
var phys_sim := _find_physical_bones(_skeleton)
|
||||||
if phys_sim:
|
if phys_sim:
|
||||||
phys_sim.active = false
|
phys_sim.active = false
|
||||||
|
|
||||||
_setup_tail()
|
_setup_tail()
|
||||||
|
|
||||||
for id: String in LEG_CFG:
|
|
||||||
var cfg: Dictionary = LEG_CFG[id]
|
|
||||||
|
|
||||||
var ik := SkeletonIK3D.new()
|
|
||||||
ik.root_bone = cfg["root"]
|
|
||||||
ik.tip_bone = cfg["tip"]
|
|
||||||
ik.interpolation = 1.0
|
|
||||||
ik.min_distance = 0.01
|
|
||||||
_skeleton.add_child(ik)
|
|
||||||
ik.start()
|
|
||||||
_ik[id] = ik
|
|
||||||
|
|
||||||
var world_rest: Vector3 = _bull.to_global(cfg["rest"])
|
|
||||||
_foot[id] = world_rest
|
|
||||||
_step_from[id] = world_rest
|
|
||||||
_step_to[id] = world_rest
|
|
||||||
_step_t[id] = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
func _setup_tail() -> void:
|
func _setup_tail() -> void:
|
||||||
for bone_name: StringName in TAIL_BONES:
|
for bone_name: StringName in TAIL_BONES:
|
||||||
@@ -108,34 +67,27 @@ func _setup_tail() -> void:
|
|||||||
push_error("BullLegs: not enough tail bones found")
|
push_error("BullLegs: not enough tail bones found")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Initialise world positions first
|
|
||||||
for idx: int in _tail_idx:
|
for idx: int in _tail_idx:
|
||||||
var world: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(idx).origin)
|
var world: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(idx).origin)
|
||||||
_tail_world.append(world)
|
_tail_world.append(world)
|
||||||
_tail_prev.append(world)
|
_tail_prev.append(world)
|
||||||
|
|
||||||
# Detect degenerate rest pose (FBX bones all at same position)
|
|
||||||
var max_sep: float = 0.0
|
var max_sep: float = 0.0
|
||||||
for i in range(_tail_world.size() - 1):
|
for i in range(_tail_world.size() - 1):
|
||||||
max_sep = maxf(max_sep, _tail_world[i].distance_to(_tail_world[i + 1]))
|
max_sep = maxf(max_sep, _tail_world[i].distance_to(_tail_world[i + 1]))
|
||||||
if max_sep < 0.01:
|
if max_sep < 0.01:
|
||||||
push_warning("BullLegs: tail bones have zero separation in rest pose, distributing along spine")
|
push_warning("BullLegs: tail bones have zero separation in rest pose, distributing along spine")
|
||||||
var root: Vector3 = _tail_world[0]
|
var root: Vector3 = _tail_world[0]
|
||||||
# Backward-downward direction in world space from the skeleton
|
|
||||||
var back: Vector3 = (_skeleton.global_transform.basis.z + Vector3.DOWN * 0.3).normalized()
|
var back: Vector3 = (_skeleton.global_transform.basis.z + Vector3.DOWN * 0.3).normalized()
|
||||||
for i in range(_tail_world.size()):
|
for i in range(_tail_world.size()):
|
||||||
_tail_world[i] = root + back * (i * 0.09)
|
_tail_world[i] = root + back * (i * 0.09)
|
||||||
_tail_prev[i] = _tail_world[i]
|
_tail_prev[i] = _tail_world[i]
|
||||||
|
|
||||||
# Segment lengths in world space — used by the Verlet length constraint
|
|
||||||
const MIN_SEG: float = 0.06
|
const MIN_SEG: float = 0.06
|
||||||
for i in range(_tail_world.size() - 1):
|
for i in range(_tail_world.size() - 1):
|
||||||
var seg_len: float = _tail_world[i].distance_to(_tail_world[i + 1])
|
var seg_len: float = _tail_world[i].distance_to(_tail_world[i + 1])
|
||||||
_tail_lengths.append(maxf(seg_len, MIN_SEG))
|
_tail_lengths.append(maxf(seg_len, MIN_SEG))
|
||||||
|
|
||||||
# Rest directions in SKELETON-LOCAL space — must match point_dir_skel in _apply_tail
|
|
||||||
# so that Quaternion(rest_dir, point_dir_skel) operates in a consistent frame.
|
|
||||||
# _tail_world holds world positions; to_local converts them to skeleton space.
|
|
||||||
for i in range(_tail_world.size() - 1):
|
for i in range(_tail_world.size() - 1):
|
||||||
var p1_skel: Vector3 = _skeleton.to_local(_tail_world[i])
|
var p1_skel: Vector3 = _skeleton.to_local(_tail_world[i])
|
||||||
var p2_skel: Vector3 = _skeleton.to_local(_tail_world[i + 1])
|
var p2_skel: Vector3 = _skeleton.to_local(_tail_world[i + 1])
|
||||||
@@ -143,16 +95,19 @@ func _setup_tail() -> void:
|
|||||||
_tail_rest_dir.append(seg_skel.normalized() if seg_skel.length() > 0.001 else Vector3(0, 0, 1))
|
_tail_rest_dir.append(seg_skel.normalized() if seg_skel.length() > 0.001 else Vector3(0, 0, 1))
|
||||||
_tail_rest_dir.append(_tail_rest_dir[-1])
|
_tail_rest_dir.append(_tail_rest_dir[-1])
|
||||||
|
|
||||||
|
_tail_parent_idx = _skeleton.get_bone_parent(_tail_idx[0])
|
||||||
|
|
||||||
|
|
||||||
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_leg_state(delta)
|
|
||||||
_step_feet(delta)
|
|
||||||
_apply_ik()
|
|
||||||
_update_tail(delta)
|
_update_tail(delta)
|
||||||
_apply_tail()
|
_apply_tail()
|
||||||
|
if DP.b("show_tail") != _dbg_on:
|
||||||
|
_dbg_toggle()
|
||||||
|
if _dbg_on:
|
||||||
|
_dbg_update()
|
||||||
|
|
||||||
|
|
||||||
# ── Body Wobble ───────────────────────────────────────────────────────────────
|
# ── Body Wobble ───────────────────────────────────────────────────────────────
|
||||||
@@ -176,194 +131,13 @@ func _wobble_body(delta: float) -> void:
|
|||||||
target_pitch = clampf(-atan2(vel.y, flat_spd) * DP.f("aerial_pitch_scale"), -1.4, 1.4)
|
target_pitch = clampf(-atan2(vel.y, flat_spd) * DP.f("aerial_pitch_scale"), -1.4, 1.4)
|
||||||
_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"))
|
||||||
|
|
||||||
_bull.rotation.x = _tilt_x + _aerial_pitch
|
_charge_pitch_curr = lerpf(_charge_pitch_curr, charge_pitch_target, delta * DP.f("charge_head_speed"))
|
||||||
|
if external_rotation:
|
||||||
|
return
|
||||||
|
_bull.rotation.x = _tilt_x + _aerial_pitch + _charge_pitch_curr
|
||||||
_bull.rotation.z = _tilt_z
|
_bull.rotation.z = _tilt_z
|
||||||
|
|
||||||
|
|
||||||
# ── Leg State Machine ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func _update_leg_state(delta: float) -> void:
|
|
||||||
var on_floor: bool = _player.is_on_floor()
|
|
||||||
_state_time += delta
|
|
||||||
|
|
||||||
match _leg_state:
|
|
||||||
LegState.GALLOP:
|
|
||||||
if not on_floor and _was_on_floor:
|
|
||||||
# Just left the ground — enter LAUNCH
|
|
||||||
_leg_state = LegState.LAUNCH
|
|
||||||
_state_time = 0.0
|
|
||||||
# Freeze current foot positions
|
|
||||||
for id: String in _foot:
|
|
||||||
_launch_feet[id] = _foot[id]
|
|
||||||
|
|
||||||
LegState.LAUNCH:
|
|
||||||
if on_floor:
|
|
||||||
_leg_state = LegState.GALLOP
|
|
||||||
_state_time = 0.0
|
|
||||||
elif _state_time > DP.f("launch_hold_time"):
|
|
||||||
_leg_state = LegState.RAGDOLL
|
|
||||||
_state_time = 0.0
|
|
||||||
|
|
||||||
LegState.RAGDOLL:
|
|
||||||
if on_floor:
|
|
||||||
_leg_state = LegState.GALLOP
|
|
||||||
_state_time = 0.0
|
|
||||||
elif _player.velocity.y < DP.f("tuck_apex_threshold"):
|
|
||||||
# Approaching or past apex — start tucking
|
|
||||||
_leg_state = LegState.TUCK
|
|
||||||
_state_time = 0.0
|
|
||||||
|
|
||||||
LegState.TUCK:
|
|
||||||
if on_floor:
|
|
||||||
_leg_state = LegState.GALLOP
|
|
||||||
_state_time = 0.0
|
|
||||||
else:
|
|
||||||
# Check for ground proximity — transition to LAND
|
|
||||||
var space := _player.get_world_3d().direct_space_state
|
|
||||||
var start := _player.global_position
|
|
||||||
var end_pt := start + Vector3.DOWN * DP.f("jump_land_threshold")
|
|
||||||
var params := PhysicsRayQueryParameters3D.create(start, end_pt)
|
|
||||||
params.exclude = [_player.get_rid()]
|
|
||||||
var hit := space.intersect_ray(params)
|
|
||||||
if hit:
|
|
||||||
_leg_state = LegState.LAND
|
|
||||||
_state_time = 0.0
|
|
||||||
|
|
||||||
LegState.LAND:
|
|
||||||
if on_floor:
|
|
||||||
_leg_state = LegState.GALLOP
|
|
||||||
_state_time = 0.0
|
|
||||||
|
|
||||||
_was_on_floor = on_floor
|
|
||||||
|
|
||||||
|
|
||||||
# ── Feet Stepping ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func _step_feet(delta: float) -> void:
|
|
||||||
match _leg_state:
|
|
||||||
LegState.GALLOP:
|
|
||||||
_step_gallop(delta)
|
|
||||||
LegState.LAUNCH:
|
|
||||||
_step_launch(delta)
|
|
||||||
LegState.RAGDOLL:
|
|
||||||
_step_ragdoll(delta)
|
|
||||||
LegState.TUCK:
|
|
||||||
_step_tuck(delta)
|
|
||||||
LegState.LAND:
|
|
||||||
_step_land(delta)
|
|
||||||
|
|
||||||
|
|
||||||
func _step_gallop(delta: float) -> void:
|
|
||||||
var threshold: float = DP.f("leg_step_threshold")
|
|
||||||
var duration: float = DP.f("leg_step_duration")
|
|
||||||
var height: float = DP.f("leg_step_height")
|
|
||||||
var overshoot: float = DP.f("leg_step_overshoot")
|
|
||||||
var flat_spd: float = Vector2(_player.velocity.x, _player.velocity.z).length()
|
|
||||||
|
|
||||||
var prev_phase: float = _gait_phase
|
|
||||||
_gait_phase = fmod(_gait_phase + flat_spd * DP.f("gait_freq") * delta, 1.0)
|
|
||||||
|
|
||||||
# Advance in-progress steps
|
|
||||||
for id: String in _step_t:
|
|
||||||
if _step_t[id] >= 1.0:
|
|
||||||
continue
|
|
||||||
_step_t[id] = minf(_step_t[id] + delta / duration, 1.0)
|
|
||||||
var t: float = _step_t[id]
|
|
||||||
var e: float = t * t * (3.0 - 2.0 * t)
|
|
||||||
var arc: float = height * sin(t * PI)
|
|
||||||
_foot[id] = _step_from[id].lerp(_step_to[id], e) + Vector3(0.0, arc, 0.0)
|
|
||||||
|
|
||||||
# Trigger steps when gait phase crosses each leg's offset
|
|
||||||
for id: String in LEG_PHASES:
|
|
||||||
if _step_t[id] < 1.0:
|
|
||||||
continue
|
|
||||||
if not _phase_crossed(prev_phase, _gait_phase, LEG_PHASES[id]):
|
|
||||||
continue
|
|
||||||
var ideal: Vector3 = _ideal_foot(id)
|
|
||||||
if _foot[id].distance_to(ideal) < threshold:
|
|
||||||
continue
|
|
||||||
_step_from[id] = _foot[id]
|
|
||||||
_step_to[id] = ideal + _player.velocity * duration * overshoot
|
|
||||||
_step_t[id] = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func _step_launch(_delta: float) -> void:
|
|
||||||
# Hold feet at their frozen launch positions
|
|
||||||
for id: String in _foot:
|
|
||||||
if _launch_feet.has(id):
|
|
||||||
_foot[id] = _launch_feet[id]
|
|
||||||
|
|
||||||
|
|
||||||
func _step_ragdoll(_delta: float) -> void:
|
|
||||||
# Procedural floppy dangle — NOT real physics ragdoll
|
|
||||||
var time: float = _state_time
|
|
||||||
var amp: float = DP.f("ragdoll_wobble_amp")
|
|
||||||
# Amplitude grows over time (legs spread more the longer airborne)
|
|
||||||
var grow: float = clampf(time * 0.8, 0.0, 1.5)
|
|
||||||
|
|
||||||
for id: String in _foot:
|
|
||||||
var rest: Vector3 = LEG_CFG[id]["rest"]
|
|
||||||
var phase: float = RAGDOLL_PHASE[id]
|
|
||||||
var dangle_offset := Vector3(
|
|
||||||
sin(time * 3.0 + phase) * amp * grow, # side sway
|
|
||||||
-0.4 - sin(time * 2.0 + phase) * 0.1 * grow, # gravity pull down
|
|
||||||
cos(time * 2.5 + phase) * amp * grow # forward/back wobble
|
|
||||||
)
|
|
||||||
_foot[id] = _bull.to_global(rest + dangle_offset)
|
|
||||||
|
|
||||||
|
|
||||||
func _step_tuck(delta: float) -> void:
|
|
||||||
# Smoothly pull legs into tucked position under the body
|
|
||||||
var speed: float = DP.f("jump_tuck_speed")
|
|
||||||
for id: String in _foot:
|
|
||||||
var tucked: Vector3 = _get_tucked_foot(id)
|
|
||||||
_foot[id] = _foot[id].lerp(tucked, clampf(delta * speed, 0.0, 1.0))
|
|
||||||
|
|
||||||
|
|
||||||
func _step_land(delta: float) -> void:
|
|
||||||
# Extend legs from tucked pose toward ideal ground positions, resume gallop cycle
|
|
||||||
var speed: float = DP.f("jump_tuck_speed") * 1.5 # slightly faster extend
|
|
||||||
for id: String in _foot:
|
|
||||||
var ideal: Vector3 = _ideal_foot(id)
|
|
||||||
_foot[id] = _foot[id].lerp(ideal, clampf(delta * speed, 0.0, 1.0))
|
|
||||||
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func _phase_crossed(prev: float, curr: float, target: float) -> bool:
|
|
||||||
if curr >= prev:
|
|
||||||
return target >= prev and target < curr
|
|
||||||
else:
|
|
||||||
return target >= prev or target < curr
|
|
||||||
|
|
||||||
|
|
||||||
func _ideal_foot(id: String) -> Vector3:
|
|
||||||
var world_rest: Vector3 = _bull.to_global(LEG_CFG[id]["rest"])
|
|
||||||
|
|
||||||
# Raycast for ground
|
|
||||||
var space := _player.get_world_3d().direct_space_state
|
|
||||||
var params := PhysicsRayQueryParameters3D.create(
|
|
||||||
world_rest + Vector3(0.0, 0.6, 0.0),
|
|
||||||
world_rest + Vector3(0.0, -1.4, 0.0),
|
|
||||||
)
|
|
||||||
params.exclude = [_player.get_rid()]
|
|
||||||
var hit := space.intersect_ray(params)
|
|
||||||
return hit["position"] if hit else world_rest
|
|
||||||
|
|
||||||
|
|
||||||
func _get_tucked_foot(id: String) -> Vector3:
|
|
||||||
var rest: Vector3 = LEG_CFG[id]["rest"]
|
|
||||||
var tucked_local := rest + Vector3(0.0, DP.f("jump_tuck_up"), DP.f("jump_tuck_fwd"))
|
|
||||||
return _bull.to_global(tucked_local)
|
|
||||||
|
|
||||||
|
|
||||||
func _apply_ik() -> void:
|
|
||||||
var skel_inv: Transform3D = _skeleton.global_transform.affine_inverse()
|
|
||||||
for id: String in _ik:
|
|
||||||
var ik: SkeletonIK3D = _ik[id]
|
|
||||||
ik.target = Transform3D(Basis.IDENTITY, skel_inv * _foot[id])
|
|
||||||
|
|
||||||
|
|
||||||
# ── Tail ──────────────────────────────────────────────────────────────────────
|
# ── Tail ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _update_tail(delta: float) -> void:
|
func _update_tail(delta: float) -> void:
|
||||||
@@ -373,26 +147,44 @@ func _update_tail(delta: float) -> void:
|
|||||||
var gravity := Vector3(0.0, -DP.f("tail_gravity"), 0.0)
|
var gravity := Vector3(0.0, -DP.f("tail_gravity"), 0.0)
|
||||||
var damping := 1.0 - clampf(DP.f("tail_damping"), 0.0, 0.99)
|
var damping := 1.0 - clampf(DP.f("tail_damping"), 0.0, 0.99)
|
||||||
|
|
||||||
# Re-anchor root joint
|
var anchor: Vector3
|
||||||
var anchor: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(_tail_idx[0]).origin)
|
if _tail_parent_idx >= 0:
|
||||||
|
var parent_t := _skeleton.get_bone_global_pose(_tail_parent_idx)
|
||||||
|
anchor = _skeleton.to_global((parent_t * _skeleton.get_bone_rest(_tail_idx[0])).origin)
|
||||||
|
else:
|
||||||
|
anchor = _skeleton.to_global(_skeleton.get_bone_global_rest(_tail_idx[0]).origin)
|
||||||
_tail_world[0] = anchor
|
_tail_world[0] = anchor
|
||||||
_tail_prev[0] = anchor
|
_tail_prev[0] = anchor
|
||||||
|
|
||||||
# Idle wag force
|
var on_floor := _player.is_on_floor()
|
||||||
|
if not on_floor and _prev_on_floor:
|
||||||
|
# Bull just left the ground — push all tail particles upward with it
|
||||||
|
for i: int in range(1, _tail_world.size()):
|
||||||
|
_tail_prev[i] = _tail_world[i] - _player.velocity * delta
|
||||||
|
_prev_on_floor = on_floor
|
||||||
|
|
||||||
|
var gravity_mult: float
|
||||||
|
if tail_ragdoll:
|
||||||
|
gravity_mult = 2.5
|
||||||
|
elif not on_floor:
|
||||||
|
gravity_mult = 0.15
|
||||||
|
else:
|
||||||
|
gravity_mult = 1.0
|
||||||
|
gravity *= gravity_mult
|
||||||
|
|
||||||
var flat_speed: float = Vector2(_player.velocity.x, _player.velocity.z).length()
|
var flat_speed: float = Vector2(_player.velocity.x, _player.velocity.z).length()
|
||||||
var wag_strength: float = lerpf(DP.f("tail_wag_idle"), 0.0, clampf(flat_speed / 3.0, 0.0, 1.0))
|
var wag_strength: float = lerpf(DP.f("tail_wag_idle"), 0.0, clampf(flat_speed / 3.0, 0.0, 1.0))
|
||||||
|
var wag_freq: float = DP.f("tail_wag_freq")
|
||||||
var time: float = Time.get_ticks_msec() / 1000.0
|
var time: float = Time.get_ticks_msec() / 1000.0
|
||||||
|
var basis_x: Vector3 = _bull.global_transform.basis.x
|
||||||
|
|
||||||
# Verlet integrate
|
|
||||||
for i in range(1, _tail_world.size()):
|
for i in range(1, _tail_world.size()):
|
||||||
var vel: Vector3 = (_tail_world[i] - _tail_prev[i]) * damping
|
var vel: Vector3 = (_tail_world[i] - _tail_prev[i]) * damping
|
||||||
_tail_prev[i] = _tail_world[i]
|
_tail_prev[i] = _tail_world[i]
|
||||||
# Add wag force (sinusoidal side-to-side)
|
var wag := Vector3.ZERO if tail_ragdoll \
|
||||||
var wag := _bull.global_transform.basis.x \
|
else basis_x * sin(time * wag_freq + i * 0.5) * wag_strength
|
||||||
* sin(time * DP.f("tail_wag_freq") + i * 0.5) * wag_strength
|
|
||||||
_tail_world[i] = _tail_world[i] + vel + gravity * (delta * delta) + wag * delta
|
_tail_world[i] = _tail_world[i] + vel + gravity * (delta * delta) + wag * delta
|
||||||
|
|
||||||
# Length constraint
|
|
||||||
for i in range(1, _tail_world.size()):
|
for i in range(1, _tail_world.size()):
|
||||||
var dir: Vector3 = _tail_world[i] - _tail_world[i - 1]
|
var dir: Vector3 = _tail_world[i] - _tail_world[i - 1]
|
||||||
var len: float = dir.length()
|
var len: float = dir.length()
|
||||||
@@ -402,12 +194,9 @@ func _update_tail(delta: float) -> void:
|
|||||||
else:
|
else:
|
||||||
_tail_world[i] = _tail_world[i - 1] + Vector3(0.0, -target_len, 0.0)
|
_tail_world[i] = _tail_world[i - 1] + Vector3(0.0, -target_len, 0.0)
|
||||||
|
|
||||||
# Body collision pass — prevent tail from clipping through torso
|
|
||||||
# Use bull's local space; body is approximated as a capsule along Z axis
|
|
||||||
var body_radius: float = DP.f("tail_body_radius")
|
var body_radius: float = DP.f("tail_body_radius")
|
||||||
for i in range(1, _tail_world.size()):
|
for i in range(1, _tail_world.size()):
|
||||||
var p := _bull.to_local(_tail_world[i])
|
var p := _bull.to_local(_tail_world[i])
|
||||||
# Capsule centered at roughly (0, 0, 0.1) in bull local space, half-length 0.4 along Z
|
|
||||||
var closest := Vector3(0.0, 0.0, clampf(p.z, -0.3, 0.5))
|
var closest := Vector3(0.0, 0.0, clampf(p.z, -0.3, 0.5))
|
||||||
var diff := p - closest
|
var diff := p - closest
|
||||||
var dist := diff.length()
|
var dist := diff.length()
|
||||||
@@ -420,16 +209,21 @@ func _apply_tail() -> void:
|
|||||||
if _tail_world.is_empty():
|
if _tail_world.is_empty():
|
||||||
return
|
return
|
||||||
|
|
||||||
for i in range(_tail_world.size()):
|
var n := _tail_world.size()
|
||||||
|
var local_pos: Array[Vector3] = []
|
||||||
|
local_pos.resize(n)
|
||||||
|
for i in n:
|
||||||
|
local_pos[i] = _skeleton.to_local(_tail_world[i])
|
||||||
|
|
||||||
|
for i in n:
|
||||||
var idx: int = _tail_idx[i]
|
var idx: int = _tail_idx[i]
|
||||||
var pos_skel: Vector3 = _skeleton.to_local(_tail_world[i])
|
var pos_skel: Vector3 = local_pos[i]
|
||||||
|
|
||||||
var point_dir_skel: Vector3
|
var point_dir_skel: Vector3
|
||||||
if i < _tail_world.size() - 1:
|
if i < n - 1:
|
||||||
point_dir_skel = (_skeleton.to_local(_tail_world[i + 1]) - pos_skel).normalized()
|
point_dir_skel = (local_pos[i + 1] - pos_skel).normalized()
|
||||||
else:
|
else:
|
||||||
var prev_skel: Vector3 = _skeleton.to_local(_tail_world[i - 1])
|
point_dir_skel = (pos_skel - local_pos[i - 1]).normalized()
|
||||||
point_dir_skel = (pos_skel - prev_skel).normalized()
|
|
||||||
|
|
||||||
var rest_dir: Vector3 = _tail_rest_dir[i]
|
var rest_dir: Vector3 = _tail_rest_dir[i]
|
||||||
var rest_t: Transform3D = _skeleton.get_bone_global_rest(idx)
|
var rest_t: Transform3D = _skeleton.get_bone_global_rest(idx)
|
||||||
@@ -442,6 +236,46 @@ func _apply_tail() -> void:
|
|||||||
_skeleton.set_bone_global_pose_override(idx, Transform3D(new_basis, pos_skel), 1.0, false)
|
_skeleton.set_bone_global_pose_override(idx, Transform3D(new_basis, pos_skel), 1.0, false)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Debug Visualization ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _dbg_toggle() -> void:
|
||||||
|
if _dbg_on:
|
||||||
|
for n: MeshInstance3D in _dbg_nodes:
|
||||||
|
if is_instance_valid(n):
|
||||||
|
n.queue_free()
|
||||||
|
_dbg_nodes.clear()
|
||||||
|
_dbg_on = false
|
||||||
|
else:
|
||||||
|
_dbg_build()
|
||||||
|
_dbg_on = true
|
||||||
|
|
||||||
|
|
||||||
|
func _dbg_build() -> void:
|
||||||
|
var root: Node = _player.get_parent()
|
||||||
|
var n: int = _tail_world.size()
|
||||||
|
for i: int in n:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.render_priority = 1
|
||||||
|
mat.albedo_color = Color.RED if i == 0 \
|
||||||
|
else Color(1.0 - float(i) / n, 0.4 + 0.6 * float(i) / n, 0.0)
|
||||||
|
var sm := SphereMesh.new()
|
||||||
|
sm.radius = 0.10 if i == 0 else 0.06
|
||||||
|
sm.height = sm.radius * 2.0
|
||||||
|
sm.radial_segments = 6
|
||||||
|
sm.rings = 3
|
||||||
|
sm.material = mat
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.mesh = sm
|
||||||
|
root.add_child(mi)
|
||||||
|
_dbg_nodes.append(mi)
|
||||||
|
|
||||||
|
|
||||||
|
func _dbg_update() -> void:
|
||||||
|
for i: int in _dbg_nodes.size():
|
||||||
|
_dbg_nodes[i].global_position = _tail_world[i]
|
||||||
|
|
||||||
|
|
||||||
# ── Utility ───────────────────────────────────────────────────────────────────
|
# ── Utility ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _find_skeleton(node: Node) -> Skeleton3D:
|
func _find_skeleton(node: Node) -> Skeleton3D:
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
extends Camera3D
|
||||||
|
|
||||||
|
@export var height: float = 25.0
|
||||||
|
@export var horizontal_distance: float = 25.0
|
||||||
|
@export var horizontal_angle_deg: float = 45.0
|
||||||
|
|
||||||
|
# Perspective zoom (FOV)
|
||||||
|
@export var fov_min: float = 20.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 blur_transition: float = 15.0
|
||||||
|
|
||||||
|
var _shake_amount: float = 0.0
|
||||||
|
var _shake_decay: float = 0.0
|
||||||
|
var _user_fov: float = 0.0
|
||||||
|
|
||||||
|
# Trailing-camera state: _focus is the smoothed point the camera frames (it lags
|
||||||
|
# the bull), _drag_offset is the velocity-driven trail that slides it behind.
|
||||||
|
var _focus: Vector3 = Vector3.ZERO
|
||||||
|
var _drag_offset: Vector3 = Vector3.ZERO
|
||||||
|
var _focus_init: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_as_top_level(true)
|
||||||
|
|
||||||
|
projection = Camera3D.PROJECTION_PERSPECTIVE
|
||||||
|
fov = default_fov
|
||||||
|
_user_fov = default_fov
|
||||||
|
|
||||||
|
near = 0.1
|
||||||
|
far = 150.0
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("zoom_in"):
|
||||||
|
_user_fov = clamp(_user_fov / 1.1, fov_min, fov_max)
|
||||||
|
|
||||||
|
if event.is_action_pressed("zoom_out"):
|
||||||
|
_user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
fov = lerpf(fov, _user_fov, delta * 8.0)
|
||||||
|
|
||||||
|
var angle_rad := deg_to_rad(horizontal_angle_deg)
|
||||||
|
|
||||||
|
var offset := Vector3(
|
||||||
|
sin(angle_rad) * horizontal_distance,
|
||||||
|
height,
|
||||||
|
cos(angle_rad) * horizontal_distance
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
global_position += Vector3(
|
||||||
|
randf_range(-_shake_amount, _shake_amount),
|
||||||
|
randf_range(-_shake_amount * 0.25, _shake_amount * 0.25),
|
||||||
|
randf_range(-_shake_amount, _shake_amount)
|
||||||
|
)
|
||||||
|
|
||||||
|
_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:
|
||||||
|
if not Settings.screen_shake:
|
||||||
|
return
|
||||||
|
_shake_amount = strength * 0.5
|
||||||
|
_shake_decay = _shake_amount / 0.4
|
||||||
|
|
||||||
|
|
||||||
|
func _update_dof(target_pos: Vector3) -> void:
|
||||||
|
var cam_attr: CameraAttributesPractical = null
|
||||||
|
|
||||||
|
if attributes is CameraAttributesPractical:
|
||||||
|
cam_attr = attributes
|
||||||
|
|
||||||
|
if cam_attr == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var distance := global_position.distance_to(target_pos)
|
||||||
|
|
||||||
|
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_near_enabled = false
|
||||||
|
cam_attr.dof_blur_far_enabled = false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cpyq2r5cq653g
|
||||||
@@ -24,9 +24,9 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
rotation.x = clamp(rotation.x, min_vert, max_vert)
|
rotation.x = clamp(rotation.x, min_vert, max_vert)
|
||||||
|
|
||||||
if event.is_action_pressed("zoom_in"):
|
if event.is_action_pressed("zoom_in"):
|
||||||
_desired_length = clamp(_desired_length - 1.0, min_zoom, max_zoom)
|
_desired_length = clamp(_desired_length / 1.2, min_zoom, max_zoom)
|
||||||
if event.is_action_pressed("zoom_out"):
|
if event.is_action_pressed("zoom_out"):
|
||||||
_desired_length = clamp(_desired_length + 1.0, min_zoom, max_zoom)
|
_desired_length = clamp(_desired_length * 1.2, min_zoom, max_zoom)
|
||||||
|
|
||||||
if event.is_action_pressed("toggle_mouse_capture"):
|
if event.is_action_pressed("toggle_mouse_capture"):
|
||||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
|
|||||||
@@ -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
|
||||||