52 Commits

Author SHA1 Message Date
richard 56593c58cf Release 0.2.0: device-aware controls, spear, settings, shaders
Show touch controls only on touch platforms and keyboard hints only on
desktop, via a shared Controls.use_touch_ui() gate (is_touchscreen_available
is unreliable with emulate_touch_from_mouse). Bumps version to 0.2.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-09 19:39:37 +03:00
richard ce6f3d7075 Deployment script update 2026-08-09 15:26:01 +03:00
siim a74182255b light tim 2026-08-08 11:47:36 +03:00
siim ebf5046461 credits 2026-07-30 20:21:10 +03:00
richard 271de1b045 ps1 filter variables, crowds 2026-07-30 15:12:22 +03:00
richard 3defc9da95 UI theme 2026-07-30 13:52:50 +03:00
richard ff75dacfb6 New screens, new hud, new intro menu, some UI element changes 2026-07-30 13:52:39 +03:00
richard bf7a454ad0 Win, lose screen. Game version info. Matador a lot more aggressive/wins easier (tweaks needed). No more score/combo/waves 2026-07-30 12:39:51 +03:00
richard ebb6ec6335 Bull roll ability, console screen, tests 2026-07-28 23:10:38 +03:00
siim a0b248748c hud ja camera change 2026-07-24 15:16:21 +03:00
siim 5ef5889979 siim push 2026-07-24 14:51:57 +03:00
siim 2e9d62b964 bull uptade 2026-07-24 10:02:56 +03:00
richard 622830a6ee Hide controls by default 2026-07-22 23:00:45 +03:00
richard 603a674e08 menu interactive, refactor, perf tests, charge bar, debug menu stuff 2026-07-22 22:57:02 +03:00
siim 631df017d1 seq uptate 2026-07-22 22:25:14 +03:00
siim 2c0142d208 + intro sequence 2026-07-22 22:00:22 +03:00
richard c283eb0927 Menu, options with changeable controls, fade transition, placeholder background 2026-07-22 19:45:49 +03:00
siim d379b215e2 intro test 2026-07-21 23:36:41 +03:00
siim cd73a21ab7 intro menu alustasin 2026-07-20 22:40:50 +03:00
richard 6a67bfea36 better ragdoll, launch ragdolls on slam weee 2026-06-25 00:17:12 +03:00
richard b929ed2a7a cooldowns, sword holstering, ability colors 2026-06-25 00:10:40 +03:00
richard 708a35d51f return of the bull 2026-06-24 23:13:14 +03:00
richard 115872b98f sword holster, health 2026-06-24 23:11:19 +03:00
siim 64124990a6 +Sword 2026-06-20 10:58:35 +03:00
siim 037bfad700 matador 2026-06-20 10:53:21 +03:00
siim 661b4e470b gdignore 2026-06-16 18:30:05 +03:00
siim b564cd5a71 sword keset platsi 2026-06-14 23:13:52 +03:00
siim 7b5fcb1d1e +sword 2026-06-14 23:09:18 +03:00
siim 45e69101b6 gamepad buttons 2026-06-11 20:51:59 +03:00
siim a9810c8c9f dash 2026-06-10 22:50:07 +03:00
richard ba20cd8580 remove bounce after slam 2026-06-10 22:42:12 +03:00
richard aac0099faa Merge remote-tracking branch 'origin/main' 2026-06-10 22:36:53 +03:00
richard 4fc64dedae dash speed 3x, slam visual/damage fix 2026-06-10 22:36:47 +03:00
siim 7423665c06 dash*3 2026-06-10 22:07:11 +03:00
richard 09c39e068b Ability refinement, more and more matadors spawn after clears 2026-06-10 21:53:57 +03:00
richard c1852e8736 abilities first draft 2026-06-10 19:39:35 +03:00
richard dda9862589 fix bad references 2026-06-10 19:23:34 +03:00
richard d538679697 Merge remote-tracking branch 'origin/main' 2026-06-10 19:07:35 +03:00
siim e668895c12 bull anim 2026-06-10 18:56:17 +03:00
siim 064f07ace7 hud_pics 2026-06-10 01:23:20 +03:00
siim 9d0799083c dof update 2026-06-08 03:05:11 +03:00
siim 2bea615307 DOF test 2026-06-08 02:50:04 +03:00
siim a61f4c4689 koristasin selle lehvitaja ära 2026-06-07 19:48:33 +03:00
richard f6355605cd smarter AI 2026-06-07 17:30:58 +03:00
siim 033d26ee18 blend1 test 2026-06-07 16:58:52 +03:00
richard d3315836c3 delete autosaves 2026-06-07 16:56:20 +03:00
richard 3279645ac4 gitignore remove blender autosave 2026-06-07 16:55:50 +03:00
richard 371a2fded0 remove blender autosaves from git history 2026-06-07 16:54:15 +03:00
richard 49a9bced4b matador animationingng 2026-06-07 16:47:18 +03:00
siim f0d932eac3 testing 2026-06-06 12:08:56 +03:00
siim ca1a0ce581 - 2026-06-04 01:12:09 +03:00
siim 5ad4f64474 matador 2026-06-04 00:10:13 +03:00
184 changed files with 9268 additions and 1570 deletions
+1
View File
@@ -2,3 +2,4 @@ disable:
- class-definitions-order - class-definitions-order
- no-else-return - no-else-return
max-line-length: 120 max-line-length: 120
max-file-lines: 1200
+6 -3
View File
@@ -2,17 +2,20 @@
.godot/ .godot/
/android/ /android/
# Godot UID sidecar files (auto-generated, machine-local)
*.uid
# 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 render previews (generated by render_anim_preview.py)
Blender/preview/ Blender/preview/
Blender/*.blend1
# Test output (generated screenshots, frame strips) # Test output (generated screenshots, frame strips)
tests/output/ tests/output/
# Web export output # Web export output
build/ build/
#PRISM
BullPrism/
+4 -1
View File
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/ANIM_publik_mees.glb-77ff7b15eaa09f6b7edf4365
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
@@ -4153,5 +4155,6 @@ _subresources={
} }
} }
} }
gltf/naming_version=2 gltf/naming_version=0
gltf/embedded_image_handling=1 gltf/embedded_image_handling=1
gltf/texture_map_mode=1
+1
View File
@@ -2,6 +2,7 @@ extends Node3D
func _ready() -> void: func _ready() -> void:
add_to_group(&"public") # excluded from debug overlays — see publikum_controller.gd
var anim := get_node_or_null(^"AnimationPlayer") as AnimationPlayer var anim := get_node_or_null(^"AnimationPlayer") as AnimationPlayer
if anim: if anim:
anim.play(&"Juubeldab") anim.play(&"Juubeldab")
+1
View File
@@ -0,0 +1 @@
uid://wj6tgh8nb0ka
+1
View File
@@ -0,0 +1 @@
uid://gt3kfchac3m3
+4 -1
View File
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/mees_plaksutab.glb-0ce4e4ee8c8f92069abfac9352
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
@@ -2097,5 +2099,6 @@ _subresources={
} }
} }
} }
gltf/naming_version=2 gltf/naming_version=0
gltf/embedded_image_handling=1 gltf/embedded_image_handling=1
gltf/texture_map_mode=1
BIN
View File
Binary file not shown.
@@ -3,19 +3,21 @@
importer="scene" importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://qix7axtc0lf3" uid="uid://cl54dt38yj2x"
path="res://.godot/imported/matador_v02.fbx-220033ca52e262349a727eddc79dcf22.scn" path="res://.godot/imported/Arena.glb-bf6b057edaab2fa44513d7f705517361.scn"
[deps] [deps]
source_file="res://Blender/matador_v02.fbx" source_file="res://Assets/Arena.glb"
dest_files=["res://.godot/imported/matador_v02.fbx-220033ca52e262349a727eddc79dcf22.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

+44
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

+44
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

+44
View File
@@ -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
Binary file not shown.
+45
View File
@@ -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
Binary file not shown.
+45
View File
@@ -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
+3
View File
@@ -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
@@ -40,3 +42,4 @@ materials/extract_path=""
_subresources={} _subresources={}
gltf/naming_version=0 gltf/naming_version=0
gltf/embedded_image_handling=1 gltf/embedded_image_handling=1
gltf/texture_map_mode=1
BIN
View File
Binary file not shown.
@@ -3,19 +3,21 @@
importer="scene" importer="scene"
importer_version=1 importer_version=1
type="PackedScene" type="PackedScene"
uid="uid://qy6qrxdfgx2g" uid="uid://bun14xmk20pcj"
path="res://.godot/imported/bull_v10.fbx-0bb89c99686d56acf88cf41a200f2277.scn" path="res://.godot/imported/bull.fbx-50f6d03a92c862695bbeaeba563be0db.scn"
[deps] [deps]
source_file="res://Assets/bull_v10.fbx" source_file="res://Assets/bull.fbx"
dest_files=["res://.godot/imported/bull_v10.fbx-0bb89c99686d56acf88cf41a200f2277.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

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

After

Width:  |  Height:  |  Size: 115 KiB

+40
View File
@@ -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
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
uid://csxxlnq74fej4
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

+5
View File
@@ -0,0 +1,5 @@
extends Node3D
func _ready() -> void:
$AnimationPlayer.play("Taunt")
+1
View File
@@ -0,0 +1 @@
uid://bn3xwnlktwdlf
+38
View File
@@ -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)
+12
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
uid://dycnoiadljbb1
Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

+40
View File
@@ -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
BIN
View File
Binary file not shown.
+45
View File
@@ -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
BIN
View File
Binary file not shown.
+45
View File
@@ -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
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-60
View File
@@ -1,60 +0,0 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://fv7q2kiamni5"
path="res://.godot/imported/ANIM_publik_mees.blend-d47f9b6736ce083865e1f16c01bc46f4.scn"
[deps]
source_file="res://Blender/ANIM_publik_mees.blend"
dest_files=["res://.godot/imported/ANIM_publik_mees.blend-d47f9b6736ce083865e1f16c01bc46f4.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
Binary file not shown.
-60
View File
@@ -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=0
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
uid://ymaeifakqovf
-60
View File
@@ -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=0
Binary file not shown.
-60
View File
@@ -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=0
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-60
View File
@@ -1,60 +0,0 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://ce8p5ywum87oj"
path="res://.godot/imported/publik_mees_v01.blend-bd82e3b38f4d34ceacdfaddc95016c7b.scn"
[deps]
source_file="res://Blender/publik_mees_v01.blend"
dest_files=["res://.godot/imported/publik_mees_v01.blend-bd82e3b38f4d34ceacdfaddc95016c7b.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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22
View File
@@ -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
Binary file not shown.
@@ -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
Binary file not shown.
+41
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

+40
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+40
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

+40
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+40
View File
@@ -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
+137
View File
@@ -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"]
+308
View File
@@ -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)
+2 -2
View File
@@ -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")]
+9 -10
View File
@@ -1,39 +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" path="res://camera_iso.gd" id="2_camera_iso"] [ext_resource type="Script" uid="uid://cpyq2r5cq653g" path="res://camera_iso.gd" id="2_camera_iso"]
[ext_resource type="PackedScene" uid="uid://qy6qrxdfgx2g" path="res://Assets/bull_v10.fbx" id="4_e80uo"] [ext_resource type="PackedScene" uid="uid://bun14xmk20pcj" path="res://Assets/bull.fbx" id="4_e80uo"]
[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.505, -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.505, 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.505, 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="Camera3D" type="Camera3D" parent="." unique_id=1120161087] [node name="Camera3D" type="Camera3D" parent="." unique_id=1120161087]
script = ExtResource("2_camera_iso") script = ExtResource("2_camera_iso")
[node name="bull_v10" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")] [node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
[editable path="bull_v10"] [editable path="bull"]
+1
View File
@@ -0,0 +1 @@
uid://b6gdjsr6fcwc2
+1
View File
@@ -0,0 +1 @@
uid://sbf260u32upu
+49
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://cnhmhom4w5o3i
+89 -8
View File
@@ -15,17 +15,28 @@ var _aerial_pitch: float = 0.0
var charge_pitch_target: float = 0.0 var charge_pitch_target: float = 0.0
var _charge_pitch_curr: float = 0.0 var _charge_pitch_curr: float = 0.0
# When true, another system (the roll's ball-tumble) owns the bull's full
# orientation — skip writing body wobble so we don't fight it.
var external_rotation: bool = false
# Tail — Verlet chain simulation # Tail — Verlet chain simulation
const TAIL_BONES: Array = [ const TAIL_BONES: Array = [
&"tail_1", &"tail_2", &"tail_3", &"tail_4", &"tail_1", &"tail_2", &"tail_3", &"tail_4",
&"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:
@@ -33,7 +44,7 @@ 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 # Disable any physical bone simulator
@@ -84,6 +95,8 @@ 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:
@@ -91,6 +104,10 @@ func _process(delta: float) -> void:
_wobble_body(delta) _wobble_body(delta)
_update_tail(delta) _update_tail(delta)
_apply_tail() _apply_tail()
if DP.b("show_tail") != _dbg_on:
_dbg_toggle()
if _dbg_on:
_dbg_update()
# ── Body Wobble ─────────────────────────────────────────────────────────────── # ── Body Wobble ───────────────────────────────────────────────────────────────
@@ -115,6 +132,8 @@ func _wobble_body(delta: float) -> void:
_aerial_pitch = lerpf(_aerial_pitch, target_pitch, delta * DP.f("aerial_pitch_speed")) _aerial_pitch = lerpf(_aerial_pitch, target_pitch, delta * DP.f("aerial_pitch_speed"))
_charge_pitch_curr = lerpf(_charge_pitch_curr, charge_pitch_target, delta * DP.f("charge_head_speed")) _charge_pitch_curr = lerpf(_charge_pitch_curr, charge_pitch_target, delta * DP.f("charge_head_speed"))
if external_rotation:
return
_bull.rotation.x = _tilt_x + _aerial_pitch + _charge_pitch_curr _bull.rotation.x = _tilt_x + _aerial_pitch + _charge_pitch_curr
_bull.rotation.z = _tilt_z _bull.rotation.z = _tilt_z
@@ -128,10 +147,31 @@ 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)
var anchor: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(_tail_idx[0]).origin) var anchor: Vector3
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
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 wag_freq: float = DP.f("tail_wag_freq")
@@ -141,7 +181,8 @@ func _update_tail(delta: float) -> void:
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]
var wag := basis_x * sin(time * wag_freq + i * 0.5) * wag_strength var wag := Vector3.ZERO if tail_ragdoll \
else basis_x * sin(time * 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
for i in range(1, _tail_world.size()): for i in range(1, _tail_world.size()):
@@ -195,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:
+79 -34
View File
@@ -1,74 +1,119 @@
extends Camera3D extends Camera3D
@export var height: float = 18.0 @export var height: float = 25.0
@export var horizontal_distance: float = 18.0 @export var horizontal_distance: float = 25.0
@export var horizontal_angle_deg: float = 45.0 @export var horizontal_angle_deg: float = 45.0
@export var ortho_size: float = 15.0
@export var ortho_size_max: float = 40.0
const _FLASH_DURATION: float = 0.30 # 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_amount: float = 0.0
var _shake_decay: float = 0.0 var _shake_decay: float = 0.0
var _flash_timer: float = 0.0 var _user_fov: float = 0.0
var _flash_rect: ColorRect = null
var _target_size: float = 0.0 # Trailing-camera state: _focus is the smoothed point the camera frames (it lags
# the bull), _drag_offset is the velocity-driven trail that slides it behind.
var _focus: Vector3 = Vector3.ZERO
var _drag_offset: Vector3 = Vector3.ZERO
var _focus_init: bool = false
func _ready() -> void: func _ready() -> void:
set_as_top_level(true) set_as_top_level(true)
projection = PROJECTION_ORTHOGONAL
_target_size = ortho_size
size = ortho_size
_setup_flash()
projection = Camera3D.PROJECTION_PERSPECTIVE
fov = default_fov
_user_fov = default_fov
func _setup_flash() -> void: near = 0.1
var canvas := CanvasLayer.new() far = 150.0
canvas.layer = 20
add_child(canvas)
_flash_rect = ColorRect.new()
_flash_rect.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_flash_rect.color = Color(1.0, 0.25, 0.2, 0.0)
_flash_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
canvas.add_child(_flash_rect)
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("zoom_in"): if event.is_action_pressed("zoom_in"):
_target_size = clamp(_target_size / 1.2, ortho_size, ortho_size_max) _user_fov = clamp(_user_fov / 1.1, fov_min, fov_max)
if event.is_action_pressed("zoom_out"): if event.is_action_pressed("zoom_out"):
_target_size = clamp(_target_size * 1.2, ortho_size, ortho_size_max) _user_fov = clamp(_user_fov * 1.1, fov_min, fov_max)
func _process(delta: float) -> void: func _process(delta: float) -> void:
size = lerpf(size, _target_size, delta * 10.0) fov = lerpf(fov, _user_fov, delta * 8.0)
var angle_rad := deg_to_rad(horizontal_angle_deg) var angle_rad := deg_to_rad(horizontal_angle_deg)
var offset := Vector3( var offset := Vector3(
sin(angle_rad) * horizontal_distance, sin(angle_rad) * horizontal_distance,
height, height,
cos(angle_rad) * horizontal_distance, cos(angle_rad) * horizontal_distance
) )
var target := get_parent() as Node3D var target := get_parent() as Node3D
global_position = target.global_position + offset var focus := _drag_focus(target, delta)
look_at(target.global_position, Vector3.UP)
global_position = focus + offset
look_at(focus, Vector3.UP)
_update_dof(focus)
if _shake_amount > 0.005: if _shake_amount > 0.005:
global_position += Vector3( global_position += Vector3(
randf_range(-_shake_amount, _shake_amount), randf_range(-_shake_amount, _shake_amount),
randf_range(-_shake_amount * 0.25, _shake_amount * 0.25), randf_range(-_shake_amount * 0.25, _shake_amount * 0.25),
randf_range(-_shake_amount, _shake_amount), randf_range(-_shake_amount, _shake_amount)
) )
_shake_amount = move_toward(_shake_amount, 0.0, _shake_decay * delta) _shake_amount = move_toward(_shake_amount, 0.0, _shake_decay * delta)
if _flash_timer > 0.0:
_flash_timer = maxf(0.0, _flash_timer - delta) # Smoothed follow point that drags behind the bull. The target trail sits opposite
_flash_rect.color.a = (_flash_timer / _FLASH_DURATION) * 0.6 # the bull's flat velocity (cam_drag metres per m/s); the offset and the focus both
else: # ease toward their targets so the camera lags and settles rather than snapping.
_flash_rect.color.a = 0.0 func _drag_focus(target: Node3D, delta: float) -> Vector3:
var pos := target.global_position
if not _focus_init:
_focus = pos
_focus_init = true
var vel := Vector3.ZERO
if target is CharacterBody3D:
vel = (target as CharacterBody3D).velocity
vel.y = 0.0
var drag_target := -vel * DP.f("cam_drag")
_drag_offset = _drag_offset.lerp(drag_target, clampf(DP.f("cam_drag_speed") * delta, 0.0, 1.0))
_focus = _focus.lerp(pos + _drag_offset, clampf(DP.f("cam_follow_lerp") * delta, 0.0, 1.0))
return _focus
func trigger_hit(strength: float) -> void: func trigger_hit(strength: float) -> void:
if not Settings.screen_shake:
return
_shake_amount = strength * 0.5 _shake_amount = strength * 0.5
_shake_decay = _shake_amount / 0.4 _shake_decay = _shake_amount / 0.4
_flash_timer = _FLASH_DURATION
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
+1
View File
@@ -0,0 +1 @@
uid://cpyq2r5cq653g
+651
View File
@@ -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)
+108
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://dvxx75l5re2mf
+35
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
uid://hkdivctavovb
+137
View File
@@ -0,0 +1,137 @@
extends MultiMeshInstance3D
## A whole stand of cheering spectators rendered as camera-facing billboards in a
## single draw call — the performant replacement for dozens of skinned crowd skeletons.
## The sprite sheet is baked from the existing crowd figure by tools/bake_crowd_sheet.gd.
##
## Placement mirrors an existing crowd node (default: the sibling PublikNode) so the
## billboards land exactly where the crowd was authored; if that node is missing it
## falls back to a generated tribune so the scene still populates.
const SHEET_PATH := "res://Assets/crowd_clap_sheet.png"
const SHADER_PATH := "res://crowd_billboard.gdshader"
## Node whose descendant figures (names starting with `figure_prefix`) donate their
## world positions. Empty = use the generated fallback stand.
@export var source_path: NodePath = ^"../PublikNode"
@export var figure_prefix: String = "mees"
## World height of a spectator billboard, in metres. Sheet aspect sets the width.
@export var figure_height: float = 2.0
@export var frames: int = 8
@export var clap_fps: float = 10.0
## Subtle per-spectator brightness spread so the crowd doesn't read as clones.
@export_range(0.0, 0.5) var tint_variation: float = 0.18
# Fallback tribune (used only when source_path resolves to nothing) — concentric arcs
# of seats ringing the arena.
@export_group("Fallback stand")
@export var fallback_count: int = 120
@export var fallback_inner_radius: float = 26.0
@export var fallback_rows: int = 6
@export var fallback_row_rise: float = 1.6
@export var fallback_row_step: float = 2.2
@export var fallback_seat_step: float = 2.4
var _rng := RandomNumberGenerator.new()
func _ready() -> void:
_rng.seed = hash("bullosseum-crowd") # deterministic layout/phases across runs
_build()
func _build() -> void:
var sheet := load(SHEET_PATH) as Texture2D
if sheet == null:
push_warning("crowd_billboards: missing sheet %s — run tools/bake_crowd_sheet.gd" % SHEET_PATH)
return
# Sheet holds `frames` columns and 2 rows (front / back), so a cell is half-height.
var aspect := (float(sheet.get_width()) / float(frames)) / (float(sheet.get_height()) / 2.0)
var quad := QuadMesh.new()
quad.size = Vector2(figure_height * aspect, figure_height)
quad.center_offset = Vector3(0.0, figure_height * 0.5, 0.0) # pivot at the feet
var mat := ShaderMaterial.new()
mat.shader = load(SHADER_PATH) as Shader
mat.set_shader_parameter("sheet", sheet)
mat.set_shader_parameter("hframes", frames)
mat.set_shader_parameter("fps", clap_fps)
material_override = mat
var seats := _seat_positions()
var center := _arena_center(seats)
var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.use_custom_data = true
mm.mesh = quad
mm.instance_count = seats.size()
for i in seats.size():
var s := 1.0 + _rng.randf_range(-0.06, 0.06) # slight height variety
mm.set_instance_transform(i, Transform3D(_facing_basis(seats[i], center, s), seats[i]))
var b := 1.0 + _rng.randf_range(-tint_variation, tint_variation)
mm.set_instance_custom_data(i, Color(_rng.randf(), b, b * 0.99, b * 0.98))
multimesh = mm
# Orientation for a seat: the sprite's forward (+Z, the quad's face) points horizontally
# inward at the arena centre, so each ring segment faces the action instead of the
# camera. Upright (world +Y), uniformly scaled by `s`.
func _facing_basis(seat: Vector3, center: Vector3, s: float) -> Basis:
var look := center - seat
look.y = 0.0
look = look.normalized() if look.length() > 0.001 else Vector3(0.0, 0.0, -1.0)
var up := Vector3.UP
var right := up.cross(look).normalized()
var basis := Basis()
basis.x = right
basis.y = up
basis.z = look
return basis.scaled(Vector3.ONE * s)
# Horizontal centre the crowd faces — the mean of the seat positions (the ring/stand
# centroid), which for an arena crowd is the middle of the pitch.
func _arena_center(seats: PackedVector3Array) -> Vector3:
if seats.is_empty():
return global_position
var sum := Vector3.ZERO
for p in seats:
sum += p
return sum / float(seats.size())
# Global feet-positions for every spectator: harvested from the source crowd if present,
# otherwise the generated fallback tribune.
func _seat_positions() -> PackedVector3Array:
var out := PackedVector3Array()
var src := get_node_or_null(source_path)
if src != null:
_collect_figures(src, out)
if out.is_empty():
_fallback_stand(out)
return out
func _collect_figures(node: Node, out: PackedVector3Array) -> void:
if node is Node3D and node != self and (node as Node3D).name.begins_with(figure_prefix):
out.append((node as Node3D).global_position)
return # don't descend into a figure's own rig
for c in node.get_children():
_collect_figures(c, out)
func _fallback_stand(out: PackedVector3Array) -> void:
var placed := 0
for row in fallback_rows:
var radius := fallback_inner_radius + float(row) * fallback_row_step
var y := float(row) * fallback_row_rise
var circumference := TAU * radius
var seats_in_row := int(circumference / fallback_seat_step)
for s in seats_in_row:
if placed >= fallback_count:
return
var ang := TAU * float(s) / float(seats_in_row)
out.append(Vector3(cos(ang) * radius, y, sin(ang) * radius))
placed += 1
+1
View File
@@ -0,0 +1 @@
uid://cj0ytlkuavmf4
+16
View File
@@ -0,0 +1,16 @@
shader_type canvas_item;
// Faint red gradient hugging the screen edges, flashed on a hit. `flash` is tweened
// 0.9 → 0 by the HUD; the vignette is transparent in the centre and reddens toward
// the corners so the middle of the screen stays clear.
uniform float flash : hint_range(0.0, 1.0) = 0.0;
uniform vec3 tint : source_color = vec3(0.75, 0.0, 0.0);
uniform float inner : hint_range(0.0, 1.0) = 0.45;
uniform float outer : hint_range(0.0, 1.0) = 1.0;
void fragment() {
// Distance from centre normalised so the corners land at ~1.0.
float dist = length(UV - vec2(0.5)) / 0.70710678;
float v = smoothstep(inner, outer, dist);
COLOR = vec4(tint, v * flash);
}

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