Compare commits
46 Commits
uurin
..
1f24a750fb
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f24a750fb | |||
| 33ec51625c | |||
| 2e2b70e030 | |||
| 04597cdc01 | |||
| c407513cbd | |||
| ea065e4d81 | |||
| 0e654ec0b6 | |||
| df27e9d807 | |||
| 866cc12c2c | |||
| d98a5ba0ac | |||
| 6abf5355c2 | |||
| bfb026c7ae | |||
| 245ce31eab | |||
| b30fb6e0a6 | |||
| 950d93ed0e | |||
| 85b9c86164 | |||
| 174b14fa61 | |||
| 30136a9b90 | |||
| 80a35a4553 | |||
| 98d579f733 | |||
| f19e6980cd | |||
| ab270ac04f | |||
| 22ed4f7722 | |||
| af0790a1ce | |||
| a8b9f166a6 | |||
| de5b5f3b7c | |||
| 13b027d91c | |||
| d6fe527906 | |||
| 6549240337 | |||
| fe5d143047 | |||
| dc40057fe1 | |||
| cb3a54c1cc | |||
| 34b3866a5e | |||
| 4f1126563c | |||
| c587b9877f | |||
| a809ed6cc4 | |||
| 9675ae884d | |||
| a318e65e70 | |||
| 8b3a816203 | |||
| 4e7bc29507 | |||
| b7c50eaae2 | |||
| 4fa965725d | |||
| f4d61b7f0a | |||
| adbd3859de | |||
| fb6ac05890 | |||
| 22d8e5df8c |
@@ -0,0 +1,4 @@
|
|||||||
|
disable:
|
||||||
|
- class-definitions-order
|
||||||
|
- no-else-return
|
||||||
|
max-line-length: 120
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Godot 4+ specific ignores
|
||||||
|
.godot/
|
||||||
|
/android/
|
||||||
|
|
||||||
|
# Godot UID sidecar files (auto-generated, machine-local)
|
||||||
|
*.uid
|
||||||
|
|
||||||
|
# JetBrains IDE — contains absolute paths (Godot binary, SDK dirs), varies per user
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Blender render previews (generated by render_anim_preview.py)
|
||||||
|
Blender/preview/
|
||||||
|
|
||||||
|
# Test output (generated screenshots, frame strips)
|
||||||
|
tests/output/
|
||||||
|
|
||||||
|
# Web export output
|
||||||
|
build/
|
||||||
Generated
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Rider ignored files
|
||||||
|
/.idea.Bullosseum.iml
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
/contentModel.xml
|
||||||
|
/modules.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
Generated
+4
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders />
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Godot 4+ specific ignores
|
|
||||||
.godot/
|
|
||||||
/android/
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
[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://b4td4k8yo5y78" path="res://camera_spring_arm.gd" id="2_kpjcp"]
|
|
||||||
[ext_resource type="Script" uid="uid://b720l6m0ehrj3" path="res://camera_follow.gd" id="3_kne1u"]
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_xhfnw"]
|
|
||||||
size = Vector3(1, 1.4072266, 1)
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_0rwas"]
|
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_kne1u"]
|
|
||||||
radius = 0.3
|
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody3D" unique_id=165510044]
|
|
||||||
script = ExtResource("1_xhfnw")
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1104396769]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.20361328, 0)
|
|
||||||
shape = SubResource("BoxShape3D_xhfnw")
|
|
||||||
|
|
||||||
[node name="CubeGuy" type="Node3D" parent="." unique_id=1651396216]
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="CubeGuy" unique_id=367855086]
|
|
||||||
mesh = SubResource("BoxMesh_0rwas")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D2" type="MeshInstance3D" parent="CubeGuy" unique_id=1565560527]
|
|
||||||
transform = Transform3D(0.22778136, 0, 0, 0, 0.4572928, 0, 0, 0, 0.2800231, 0.36544353, -0.65451574, 0)
|
|
||||||
mesh = SubResource("BoxMesh_0rwas")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D3" type="MeshInstance3D" parent="CubeGuy" unique_id=2097296241]
|
|
||||||
transform = Transform3D(0.22778136, 0, 0, 0, 0.4572928, 0, 0, 0, 0.2800231, -0.38543648, -0.65451574, 0)
|
|
||||||
mesh = SubResource("BoxMesh_0rwas")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D4" type="MeshInstance3D" parent="CubeGuy" unique_id=383816037]
|
|
||||||
transform = Transform3D(0.14033328, 0.3601992, 0, -0.17941824, 0.28173247, 0, 0, 0, 0.28002307, -0.60614854, -0.07838535, 0)
|
|
||||||
mesh = SubResource("BoxMesh_0rwas")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D5" type="MeshInstance3D" parent="CubeGuy" unique_id=326472138]
|
|
||||||
transform = Transform3D(-0.14033328, -0.3601992, 2.4480395e-08, -0.17941824, 0.28173247, 0, -1.2268325e-08, -3.1489616e-08, -0.28002307, 0.6122027, -0.07838535, 0)
|
|
||||||
mesh = SubResource("BoxMesh_0rwas")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D6" type="MeshInstance3D" parent="CubeGuy" unique_id=1642768377]
|
|
||||||
transform = Transform3D(-0.113692805, 0, -2.4480393e-08, 0, 0.11736439, 0, 9.939342e-09, 0, -0.28002304, -0.26236355, 0.3103568, 0.42819703)
|
|
||||||
mesh = SubResource("BoxMesh_0rwas")
|
|
||||||
|
|
||||||
[node name="MeshInstance3D7" type="MeshInstance3D" parent="CubeGuy" unique_id=695468518]
|
|
||||||
transform = Transform3D(-0.113692805, 0, -2.4480393e-08, 0, 0.11736439, 0, 9.939342e-09, 0, -0.28002304, 0.2908606, 0.28320956, 0.42819703)
|
|
||||||
mesh = SubResource("BoxMesh_0rwas")
|
|
||||||
|
|
||||||
[node name="SpringArmPivot" type="Node3D" parent="." unique_id=1671817680]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.22, 0)
|
|
||||||
script = ExtResource("2_kpjcp")
|
|
||||||
|
|
||||||
[node name="SpringArm3D" type="SpringArm3D" parent="SpringArmPivot" unique_id=141439848]
|
|
||||||
collision_mask = 2
|
|
||||||
shape = SubResource("SphereShape3D_kne1u")
|
|
||||||
spring_length = 7.0
|
|
||||||
|
|
||||||
[node name="SpringPosition" type="Node3D" parent="SpringArmPivot/SpringArm3D" unique_id=1307131677]
|
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="SpringArmPivot" unique_id=1120161087 node_paths=PackedStringArray("spring_arm")]
|
|
||||||
keep_aspect = 0
|
|
||||||
script = ExtResource("3_kne1u")
|
|
||||||
spring_arm = NodePath("../SpringArm3D/SpringPosition")
|
|
||||||
lerp_power = 6.0
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
extends Camera3D
|
|
||||||
|
|
||||||
|
|
||||||
@export var spring_arm: Node3D
|
|
||||||
@export var lerp_power: float = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
|
||||||
position = lerp(position, spring_arm.position, delta * lerp_power)
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
extends Node3D
|
|
||||||
|
|
||||||
|
|
||||||
@export var mouse_sensitivity: float = 0.005
|
|
||||||
@export_range(-90.0, 0.0, 0.1, "radians_as_degrees") var min_vertical_angle: float = -PI/2
|
|
||||||
@export_range(0.0, 90.0, 0.1, "radians_as_degrees") var max_vertical_angle: float = PI/4
|
|
||||||
|
|
||||||
@onready var spring_arm := $SpringArm3D
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event : InputEvent) -> void:
|
|
||||||
if event is InputEventMouseMotion:
|
|
||||||
rotation.y -= event.relative.x * mouse_sensitivity
|
|
||||||
rotation.y = wrapf(rotation.y, 0.0, TAU)
|
|
||||||
|
|
||||||
rotation.x -= event.relative.y * mouse_sensitivity
|
|
||||||
rotation.x = clamp(rotation.x, min_vertical_angle, max_vertical_angle)
|
|
||||||
|
|
||||||
if event.is_action_pressed("wheel_up"):
|
|
||||||
spring_arm.spring_length -=1
|
|
||||||
if event.is_action_pressed("wheel_down"):
|
|
||||||
spring_arm.spring_length +=1
|
|
||||||
|
|
||||||
if event.is_action_pressed("toggle_mouse_capture"):
|
|
||||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
||||||
else:
|
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
extends CharacterBody3D
|
|
||||||
|
|
||||||
|
|
||||||
const SPEED = 5.0
|
|
||||||
const JUMP_VELOCITY = 4.5
|
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
|
||||||
# Add the gravity.
|
|
||||||
if not is_on_floor():
|
|
||||||
velocity += get_gravity() * delta
|
|
||||||
|
|
||||||
# Handle jump.
|
|
||||||
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
|
|
||||||
velocity.y = JUMP_VELOCITY
|
|
||||||
|
|
||||||
# Get the input direction and handle the movement/deceleration.
|
|
||||||
# As good practice, you should replace UI actions with custom gameplay actions.
|
|
||||||
var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
|
|
||||||
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
|
|
||||||
if direction:
|
|
||||||
velocity.x = direction.x * SPEED
|
|
||||||
velocity.z = direction.z * SPEED
|
|
||||||
else:
|
|
||||||
velocity.x = move_toward(velocity.x, 0, SPEED)
|
|
||||||
velocity.z = move_toward(velocity.z, 0, SPEED)
|
|
||||||
|
|
||||||
move_and_slide()
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
; Engine configuration file.
|
|
||||||
; It's best edited using the editor UI and not directly,
|
|
||||||
; since the parameters that go here are not all obvious.
|
|
||||||
;
|
|
||||||
; Format:
|
|
||||||
; [section] ; section goes between []
|
|
||||||
; param=value ; assign values to parameters
|
|
||||||
|
|
||||||
config_version=5
|
|
||||||
|
|
||||||
[application]
|
|
||||||
|
|
||||||
config/name="3rd_person_camera"
|
|
||||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
|
||||||
config/icon="res://icon.svg"
|
|
||||||
|
|
||||||
[input]
|
|
||||||
|
|
||||||
wheel_up={
|
|
||||||
"deadzone": 0.2,
|
|
||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(726, 8),"global_position":Vector2(735, 56),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
wheel_down={
|
|
||||||
"deadzone": 0.2,
|
|
||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(400, 21),"global_position":Vector2(409, 69),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
toggle_mouse_capture={
|
|
||||||
"deadzone": 0.2,
|
|
||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":4,"position":Vector2(461, 22),"global_position":Vector2(470, 70),"factor":1.0,"button_index":3,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
[physics]
|
|
||||||
|
|
||||||
3d/physics_engine="Jolt Physics"
|
|
||||||
|
|
||||||
[rendering]
|
|
||||||
|
|
||||||
rendering_device/driver.windows="d3d12"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[gd_scene format=3 uid="uid://b7afbx36ltb3f"]
|
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://crtm0xtiql3r8" path="res://Player.tscn" id="1_ulcgi"]
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_nxogm"]
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ulcgi"]
|
|
||||||
|
|
||||||
[node name="scene" type="Node3D" unique_id=22320312]
|
|
||||||
|
|
||||||
[node name="Player" parent="." unique_id=165510044 instance=ExtResource("1_ulcgi")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.3040223, 0)
|
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=818712731]
|
|
||||||
transform = Transform3D(0.030703627, 0.98235905, -0.18446636, -0.1612905, 0.18700416, 0.9690277, 0.9864293, -1.2735805e-08, 0.16418687, -4.08134, 0.3070748, 0.97087634)
|
|
||||||
shadow_enabled = true
|
|
||||||
|
|
||||||
[node name="Node3D" type="Node3D" parent="." unique_id=1081100876]
|
|
||||||
transform = Transform3D(3.5710833, 0, 0, 0, 1, 0, 0, 0, 2.2964425, 0, 0, 0)
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Node3D" unique_id=1193968330]
|
|
||||||
transform = Transform3D(7.6892195, 0, 0, 0, 0.34217757, 0, 0, 0, 8.638922, 0, 0, 0)
|
|
||||||
mesh = SubResource("BoxMesh_nxogm")
|
|
||||||
|
|
||||||
[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=2072095624]
|
|
||||||
transform = Transform3D(1.0000001, 0, 0, 0, 1.0000001, 0, 0, 0, 1.0000001, 0, 0, 0)
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=1444198204]
|
|
||||||
transform = Transform3D(7.6892185, 0, 0, 0, 0.34217754, 0, 0, 0, 8.638921, 0, 0, 0)
|
|
||||||
shape = SubResource("BoxShape3D_ulcgi")
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var anim := get_node_or_null(^"AnimationPlayer") as AnimationPlayer
|
||||||
|
if anim:
|
||||||
|
anim.play(&"Juubeldab")
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
$AnimationPlayer.play("ArmatureAction")
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dio67r8ambe7h"
|
||||||
|
path="res://.godot/imported/arena_placeholder_v01.glb-fc11365d8ec7ae6778e90b13ba508b8a.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/arena_placeholder_v01.glb"
|
||||||
|
dest_files=["res://.godot/imported/arena_placeholder_v01.glb-fc11365d8ec7ae6778e90b13ba508b8a.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={}
|
||||||
|
gltf/naming_version=0
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://qy6qrxdfgx2g"
|
||||||
|
path="res://.godot/imported/bull_v10.fbx-0bb89c99686d56acf88cf41a200f2277.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Assets/bull_v10.fbx"
|
||||||
|
dest_files=["res://.godot/imported/bull_v10.fbx-0bb89c99686d56acf88cf41a200f2277.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=true
|
||||||
|
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={}
|
||||||
|
fbx/importer=0
|
||||||
|
fbx/allow_geometry_helper_nodes=false
|
||||||
|
fbx/embedded_image_handling=1
|
||||||
|
fbx/naming_version=2
|
||||||
Binary file not shown.
@@ -0,0 +1,60 @@
|
|||||||
|
[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.
Binary file not shown.
@@ -0,0 +1,60 @@
|
|||||||
|
[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.
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
Blender 5.1 script: create walk + idle animations for matador_v02.
|
||||||
|
Run: blender --background matador_v02.blend --python create_matador_anims.py
|
||||||
|
"""
|
||||||
|
import bpy, math, os
|
||||||
|
|
||||||
|
|
||||||
|
# ── setup ──────────────────────────────────────────────────────────────────────
|
||||||
|
arm_obj = next(o for o in bpy.data.objects if o.type == 'ARMATURE')
|
||||||
|
bpy.context.view_layer.objects.active = arm_obj
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
|
if arm_obj.animation_data is None:
|
||||||
|
arm_obj.animation_data_create()
|
||||||
|
|
||||||
|
pb = arm_obj.pose.bones
|
||||||
|
|
||||||
|
# Ensure all animated bones use XYZ Euler
|
||||||
|
for bname in ['leg_L', 'leg_R', 'shin_L', 'shin_R',
|
||||||
|
'arm_L', 'arm_R', 'forearm_L', 'forearm_R',
|
||||||
|
'collarbone_L', 'collarbone_R']:
|
||||||
|
pb[bname].rotation_mode = 'XYZ'
|
||||||
|
|
||||||
|
CYCLE = 30 # frames (1 s at 30 fps)
|
||||||
|
LEG_SWING = 25 # degrees
|
||||||
|
KNEE_BEND = 30
|
||||||
|
ARM_SWING = 15
|
||||||
|
ARM_DOWN = 55 # lower arms from T-pose around bone-local Y
|
||||||
|
ELBOW_BEND = 25
|
||||||
|
|
||||||
|
|
||||||
|
# ── WALK action ────────────────────────────────────────────────────────────────
|
||||||
|
# Remove old actions if they exist
|
||||||
|
for old_name in ['walk', 'idle']:
|
||||||
|
if old_name in bpy.data.actions:
|
||||||
|
bpy.data.actions.remove(bpy.data.actions[old_name])
|
||||||
|
|
||||||
|
walk = bpy.data.actions.new(name='walk')
|
||||||
|
walk.use_fake_user = True
|
||||||
|
walk.use_cyclic = True
|
||||||
|
arm_obj.animation_data.action = walk
|
||||||
|
|
||||||
|
# Clear all pose bone transforms first
|
||||||
|
bpy.ops.pose.select_all(action='SELECT')
|
||||||
|
bpy.ops.pose.rot_clear()
|
||||||
|
|
||||||
|
arm_down_l = math.radians(ARM_DOWN)
|
||||||
|
arm_down_r = math.radians(-ARM_DOWN)
|
||||||
|
|
||||||
|
for f in range(0, CYCLE + 1, 2):
|
||||||
|
t = (f / CYCLE) * math.tau
|
||||||
|
|
||||||
|
# Legs – swing forward/back on bone-local X
|
||||||
|
pb['leg_L'].rotation_euler = (math.sin(t) * math.radians(LEG_SWING), 0, 0)
|
||||||
|
pb['leg_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
pb['leg_R'].rotation_euler = (math.sin(t + math.pi) * math.radians(LEG_SWING), 0, 0)
|
||||||
|
pb['leg_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
# Shins – only bend backward when leg is back
|
||||||
|
val_l = max(0.0, -math.sin(t)) * math.radians(KNEE_BEND)
|
||||||
|
val_r = max(0.0, -math.sin(t + math.pi)) * math.radians(KNEE_BEND)
|
||||||
|
pb['shin_L'].rotation_euler = (val_l, 0, 0)
|
||||||
|
pb['shin_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['shin_R'].rotation_euler = (val_r, 0, 0)
|
||||||
|
pb['shin_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
# Arms – lower from T-pose (Y) + swing (X, opposite phase to legs)
|
||||||
|
swing_l = math.sin(t + math.pi) * math.radians(ARM_SWING)
|
||||||
|
swing_r = math.sin(t) * math.radians(ARM_SWING)
|
||||||
|
pb['arm_L'].rotation_euler = (swing_l, arm_down_l, 0)
|
||||||
|
pb['arm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['arm_R'].rotation_euler = (swing_r, arm_down_r, 0)
|
||||||
|
pb['arm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
# Forearms – constant slight bend
|
||||||
|
pb['forearm_L'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
|
||||||
|
pb['forearm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['forearm_R'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
|
||||||
|
pb['forearm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
# Collarbones – identity
|
||||||
|
pb['collarbone_L'].rotation_euler = (0, 0, 0)
|
||||||
|
pb['collarbone_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['collarbone_R'].rotation_euler = (0, 0, 0)
|
||||||
|
pb['collarbone_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
print("Created 'walk' action")
|
||||||
|
|
||||||
|
|
||||||
|
# ── IDLE action ────────────────────────────────────────────────────────────────
|
||||||
|
idle = bpy.data.actions.new(name='idle')
|
||||||
|
idle.use_fake_user = True
|
||||||
|
arm_obj.animation_data.action = idle
|
||||||
|
|
||||||
|
bpy.ops.pose.select_all(action='SELECT')
|
||||||
|
bpy.ops.pose.rot_clear()
|
||||||
|
|
||||||
|
# Arms lowered, everything else at rest – 2 frames for a valid action
|
||||||
|
for f in [0, 1]:
|
||||||
|
pb['arm_L'].rotation_euler = (0, arm_down_l, 0)
|
||||||
|
pb['arm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['arm_R'].rotation_euler = (0, arm_down_r, 0)
|
||||||
|
pb['arm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['forearm_L'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
|
||||||
|
pb['forearm_L'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
pb['forearm_R'].rotation_euler = (math.radians(ELBOW_BEND), 0, 0)
|
||||||
|
pb['forearm_R'].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
for bname in ['collarbone_L', 'collarbone_R', 'leg_L', 'leg_R', 'shin_L', 'shin_R']:
|
||||||
|
pb[bname].rotation_euler = (0, 0, 0)
|
||||||
|
pb[bname].keyframe_insert(data_path='rotation_euler', frame=f)
|
||||||
|
|
||||||
|
print("Created 'idle' action")
|
||||||
|
|
||||||
|
|
||||||
|
# ── NLA tracks for export ─────────────────────────────────────────────────────
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
# Clear existing NLA tracks
|
||||||
|
if arm_obj.animation_data.nla_tracks:
|
||||||
|
for t in list(arm_obj.animation_data.nla_tracks):
|
||||||
|
arm_obj.animation_data.nla_tracks.remove(t)
|
||||||
|
|
||||||
|
tw = arm_obj.animation_data.nla_tracks.new()
|
||||||
|
tw.name = 'walk'
|
||||||
|
sw = tw.strips.new('walk', 0, walk)
|
||||||
|
|
||||||
|
ti = arm_obj.animation_data.nla_tracks.new()
|
||||||
|
ti.name = 'idle'
|
||||||
|
si = ti.strips.new('idle', 0, idle)
|
||||||
|
|
||||||
|
# ── export FBX ─────────────────────────────────────────────────────────────────
|
||||||
|
fbx_path = os.path.join(os.path.dirname(bpy.data.filepath), 'matador_v02.fbx')
|
||||||
|
bpy.ops.export_scene.fbx(
|
||||||
|
filepath=fbx_path,
|
||||||
|
use_selection=False,
|
||||||
|
bake_anim=True,
|
||||||
|
bake_anim_use_all_actions=True,
|
||||||
|
bake_anim_use_nla_strips=False,
|
||||||
|
bake_anim_force_startend_keying=True,
|
||||||
|
bake_anim_step=1.0,
|
||||||
|
bake_anim_simplify_factor=0.0,
|
||||||
|
add_leaf_bones=False,
|
||||||
|
primary_bone_axis='Y',
|
||||||
|
secondary_bone_axis='X',
|
||||||
|
apply_scale_options='FBX_SCALE_ALL',
|
||||||
|
)
|
||||||
|
print('Exported FBX to:', fbx_path)
|
||||||
|
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
|
||||||
|
print('Saved .blend')
|
||||||
Binary file not shown.
@@ -0,0 +1,60 @@
|
|||||||
|
[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.
Binary file not shown.
@@ -0,0 +1,60 @@
|
|||||||
|
[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.
@@ -0,0 +1,44 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://qix7axtc0lf3"
|
||||||
|
path="res://.godot/imported/matador_v02.fbx-220033ca52e262349a727eddc79dcf22.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Blender/matador_v02.fbx"
|
||||||
|
dest_files=["res://.godot/imported/matador_v02.fbx-220033ca52e262349a727eddc79dcf22.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=true
|
||||||
|
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={}
|
||||||
|
fbx/importer=0
|
||||||
|
fbx/allow_geometry_helper_nodes=false
|
||||||
|
fbx/embedded_image_handling=1
|
||||||
|
fbx/naming_version=2
|
||||||
Binary file not shown.
@@ -0,0 +1,60 @@
|
|||||||
|
[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.
@@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
Blender 5.1 script: render preview frames of matador animations to PNG.
|
||||||
|
Run: blender --background matador_v02.blend --python render_anim_preview.py
|
||||||
|
|
||||||
|
Outputs to Blender/preview/ directory:
|
||||||
|
walk_frame_00.png .. walk_frame_09.png
|
||||||
|
idle_frame_00.png
|
||||||
|
|
||||||
|
Use this to visually verify bone orientations BEFORE exporting to Godot.
|
||||||
|
"""
|
||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
|
||||||
|
# ── Configuration ─────────────────────────────────────────────────────────────
|
||||||
|
PREVIEW_DIR = os.path.join(os.path.dirname(bpy.data.filepath), "preview")
|
||||||
|
RENDER_RES_X = 512
|
||||||
|
RENDER_RES_Y = 512
|
||||||
|
WALK_FRAMES = 10 # number of evenly-spaced frames to render from walk cycle
|
||||||
|
CAMERA_DISTANCE = 5.0
|
||||||
|
CAMERA_HEIGHT = 1.0
|
||||||
|
CAMERA_ANGLES = [0, 90] # degrees — front and side views
|
||||||
|
|
||||||
|
os.makedirs(PREVIEW_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# ── Find armature ─────────────────────────────────────────────────────────────
|
||||||
|
arm_obj = next((o for o in bpy.data.objects if o.type == 'ARMATURE'), None)
|
||||||
|
if arm_obj is None:
|
||||||
|
raise RuntimeError("No armature found in scene")
|
||||||
|
|
||||||
|
# ── Setup render settings ─────────────────────────────────────────────────────
|
||||||
|
scene = bpy.context.scene
|
||||||
|
scene.render.resolution_x = RENDER_RES_X
|
||||||
|
scene.render.resolution_y = RENDER_RES_Y
|
||||||
|
scene.render.resolution_percentage = 100
|
||||||
|
scene.render.film_transparent = True
|
||||||
|
scene.render.image_settings.file_format = 'PNG'
|
||||||
|
scene.render.image_settings.color_mode = 'RGBA'
|
||||||
|
scene.render.engine = 'BLENDER_EEVEE'
|
||||||
|
|
||||||
|
# ── Create or reuse camera ────────────────────────────────────────────────────
|
||||||
|
cam_data = bpy.data.cameras.get("PreviewCam") or bpy.data.cameras.new("PreviewCam")
|
||||||
|
cam_data.lens = 50
|
||||||
|
cam_obj = bpy.data.objects.get("PreviewCamObj")
|
||||||
|
if cam_obj is None:
|
||||||
|
cam_obj = bpy.data.objects.new("PreviewCamObj", cam_data)
|
||||||
|
scene.collection.objects.link(cam_obj)
|
||||||
|
scene.camera = cam_obj
|
||||||
|
|
||||||
|
# ── Create or reuse key light ─────────────────────────────────────────────────
|
||||||
|
light_data = bpy.data.lights.get("PreviewLight") or bpy.data.lights.new("PreviewLight", type='SUN')
|
||||||
|
light_data.energy = 3.0
|
||||||
|
light_obj = bpy.data.objects.get("PreviewLightObj")
|
||||||
|
if light_obj is None:
|
||||||
|
light_obj = bpy.data.objects.new("PreviewLightObj", light_data)
|
||||||
|
scene.collection.objects.link(light_obj)
|
||||||
|
light_obj.rotation_euler = (math.radians(45), math.radians(30), 0)
|
||||||
|
|
||||||
|
# ── Create or reuse fill light ────────────────────────────────────────────────
|
||||||
|
fill_data = bpy.data.lights.get("PreviewFill") or bpy.data.lights.new("PreviewFill", type='SUN')
|
||||||
|
fill_data.energy = 1.0
|
||||||
|
fill_obj = bpy.data.objects.get("PreviewFillObj")
|
||||||
|
if fill_obj is None:
|
||||||
|
fill_obj = bpy.data.objects.new("PreviewFillObj", fill_data)
|
||||||
|
scene.collection.objects.link(fill_obj)
|
||||||
|
fill_obj.rotation_euler = (math.radians(60), math.radians(-45), 0)
|
||||||
|
|
||||||
|
# ── Ensure armature is visible ────────────────────────────────────────────────
|
||||||
|
arm_obj.hide_render = False
|
||||||
|
arm_obj.hide_viewport = False
|
||||||
|
for child in arm_obj.children:
|
||||||
|
child.hide_render = False
|
||||||
|
child.hide_viewport = False
|
||||||
|
|
||||||
|
|
||||||
|
def position_camera(angle_deg: float) -> None:
|
||||||
|
"""Place camera at given horizontal angle around the armature center."""
|
||||||
|
rad = math.radians(angle_deg)
|
||||||
|
cam_obj.location = (
|
||||||
|
math.sin(rad) * CAMERA_DISTANCE,
|
||||||
|
CAMERA_HEIGHT,
|
||||||
|
math.cos(rad) * CAMERA_DISTANCE,
|
||||||
|
)
|
||||||
|
# Point camera at armature center (roughly hip height)
|
||||||
|
target = arm_obj.location.copy()
|
||||||
|
target.y = 0.9
|
||||||
|
direction = target - cam_obj.location
|
||||||
|
rot_quat = direction.to_track_quat('-Z', 'Y')
|
||||||
|
cam_obj.rotation_euler = rot_quat.to_euler()
|
||||||
|
|
||||||
|
|
||||||
|
def render_action(action_name: str, frame_count: int) -> None:
|
||||||
|
"""Render frame_count evenly-spaced frames of the named action."""
|
||||||
|
action = bpy.data.actions.get(action_name)
|
||||||
|
if action is None:
|
||||||
|
print(f"WARNING: Action '{action_name}' not found, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Assign action
|
||||||
|
if arm_obj.animation_data is None:
|
||||||
|
arm_obj.animation_data_create()
|
||||||
|
arm_obj.animation_data.action = action
|
||||||
|
|
||||||
|
frame_start = int(action.frame_range[0])
|
||||||
|
frame_end = int(action.frame_range[1])
|
||||||
|
total_frames = max(frame_end - frame_start, 1)
|
||||||
|
|
||||||
|
for angle_deg in CAMERA_ANGLES:
|
||||||
|
position_camera(angle_deg)
|
||||||
|
angle_label = f"{angle_deg:03d}deg"
|
||||||
|
|
||||||
|
for i in range(frame_count):
|
||||||
|
t = i / max(frame_count - 1, 1) if frame_count > 1 else 0
|
||||||
|
frame = frame_start + int(t * total_frames)
|
||||||
|
scene.frame_set(frame)
|
||||||
|
|
||||||
|
filename = f"{action_name}_{angle_label}_frame_{i:02d}.png"
|
||||||
|
filepath = os.path.join(PREVIEW_DIR, filename)
|
||||||
|
scene.render.filepath = filepath
|
||||||
|
bpy.ops.render.render(write_still=True)
|
||||||
|
print(f" Rendered: {filename}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Render all animations ─────────────────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("RENDERING ANIMATION PREVIEWS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
render_action("walk", WALK_FRAMES)
|
||||||
|
render_action("idle", 1)
|
||||||
|
|
||||||
|
# ── Cleanup preview objects (optional — keep them for re-runs) ────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Preview images saved to: {PREVIEW_DIR}")
|
||||||
|
print(f"Total actions rendered: {len([a for a in ['walk', 'idle'] if bpy.data.actions.get(a)])}")
|
||||||
|
print("=" * 60)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Bullosseum — CLAUDE.md
|
||||||
|
|
||||||
|
## About this project
|
||||||
|
|
||||||
|
A 3D arena game built in Godot 4.7 (Forward Plus renderer, Jolt Physics) where the player controls a bull. Currently features third-person camera, bull movement with charge mechanics, and a placeholder arena.
|
||||||
|
|
||||||
|
## AI role
|
||||||
|
|
||||||
|
I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scene architecture, physics, and performance. If a question suggests a suboptimal approach — wrong node type, unnecessary complexity, a pattern that fights the engine — I will say so and explain the better alternative before implementing anything.
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `scene.tscn` | Main scene (arena + player) |
|
||||||
|
| `Player.tscn` | Player scene root (`CharacterBody3D`) |
|
||||||
|
| `player.gd` | Movement, charge, turning logic |
|
||||||
|
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
||||||
|
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
||||||
|
| `matador.gd` | Matador AI (wander / ragdoll state machine) |
|
||||||
|
| `matador_spawn.gd` | Spawns N matadors at random arena positions |
|
||||||
|
| `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) |
|
||||||
|
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
||||||
|
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
||||||
|
| `Blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX |
|
||||||
|
| `Blender/render_anim_preview.py` | Renders animation preview PNGs for visual verification |
|
||||||
|
| `Blender/preview/` | Output directory for animation preview images |
|
||||||
|
| `tests/` | Headless test scripts (screenshot capture, logic validation) |
|
||||||
|
| `addons/rider-plugin/` | JetBrains Rider IDE integration |
|
||||||
|
|
||||||
|
## Tech choices
|
||||||
|
|
||||||
|
- **Physics:** Jolt Physics (not the default Godot Physics)
|
||||||
|
- **Renderer:** Forward Plus
|
||||||
|
- **Input map:** WASD + arrow keys, Shift = charge, Space = jump, scroll = zoom, middle-click = toggle mouse capture
|
||||||
|
- **IDE:** JetBrains Rider via the rider-plugin addon
|
||||||
|
|
||||||
|
## GDScript conventions
|
||||||
|
|
||||||
|
- Typed GDScript everywhere (`var x: float`, return types on functions)
|
||||||
|
- `@onready` for node references; never `get_node()` strings when avoidable
|
||||||
|
- Constants in `SCREAMING_SNAKE_CASE`, variables in `snake_case`
|
||||||
|
- One script per scene root — keep scripts focused
|
||||||
|
- Signal names in `snake_case`; connect via `signal.connect()` not the legacy string form
|
||||||
|
- Prefer `_physics_process` for physics/movement, `_process` for visuals/camera, `_unhandled_input` for input that shouldn't bubble
|
||||||
|
- No comments that restate what the code already says; only comment non-obvious constraints or workarounds
|
||||||
|
|
||||||
|
## Best practices to enforce
|
||||||
|
|
||||||
|
- Use `CharacterBody3D` for player-controlled characters, not `RigidBody3D` (unless the design specifically needs physics simulation)
|
||||||
|
- Use `SpringArm3D` for third-person cameras to get free collision avoidance
|
||||||
|
- Prefer `move_and_slide()` with `velocity` over manual collision queries
|
||||||
|
- Export variables (`@export`) for any value a designer might tune; keep magic numbers out of logic
|
||||||
|
- Scene composition over inheritance — build behaviour from small focused scenes
|
||||||
|
- Use `autoload` (singletons) sparingly: only for truly global state (e.g. GameManager, AudioBus); not as a shortcut for passing data
|
||||||
|
- Keep `_physics_process` deterministic and frame-rate independent (always multiply by `delta`)
|
||||||
|
- Prefer signals over direct node references for decoupling
|
||||||
|
|
||||||
|
## Animation & bone conventions
|
||||||
|
|
||||||
|
### Blender rig (matador_v02)
|
||||||
|
- **Armature bones:** `matador` (root) → `COG` → `chest` → `head`, `collarbone_L/R` → `arm_L/R` → `forearm_L/R` → `hand_L/R`, `leg_L/R` → `shin_L/R` → `foot_L/R`
|
||||||
|
- **Rotation mode:** All animated bones use **XYZ Euler** (set in `create_matador_anims.py`)
|
||||||
|
- **Bone-local axes:** Swing (forward/back) is on **local X**. Arm lowering from T-pose is on **local Y** (positive = left arm down, negative = right arm down). Knee bend is **positive X** (shins only bend backward).
|
||||||
|
- **Walk cycle:** 30 frames at 30 fps = 1 second loop. Legs swing ±25° X, knees bend 0–30° X (only when leg is back), arms swing ±15° X opposite phase to legs, arms lowered 55° Y from T-pose, elbows constant 25° X bend.
|
||||||
|
- **Idle pose:** Arms lowered 55° Y, elbows 25° X, everything else at rest.
|
||||||
|
|
||||||
|
### FBX export settings
|
||||||
|
- `primary_bone_axis = 'Y'`, `secondary_bone_axis = 'X'`
|
||||||
|
- `apply_scale_options = 'FBX_SCALE_ALL'`
|
||||||
|
- `add_leaf_bones = False`
|
||||||
|
- `bake_anim_use_all_actions = True`, `bake_anim_use_nla_strips = False`
|
||||||
|
|
||||||
|
### Godot animation names
|
||||||
|
After FBX import, animations appear as `Armature|walk` and `Armature|idle` in AnimationPlayer.
|
||||||
|
|
||||||
|
### Verification workflow
|
||||||
|
1. Edit bones/animations in Blender (`create_matador_anims.py`)
|
||||||
|
2. Run render preview: `blender --background Blender/matador_v02.blend --python Blender/render_anim_preview.py`
|
||||||
|
3. Inspect PNGs in `Blender/preview/` — check bone orientations **before** exporting to Godot
|
||||||
|
4. Export FBX (done automatically by `create_matador_anims.py`)
|
||||||
|
5. Re-import in Godot and test in-game
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All tests (lint + logic + gameplay assertions):
|
||||||
|
bash run_tests.sh
|
||||||
|
|
||||||
|
# Individual tests:
|
||||||
|
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/gameplay_test.gd # full scene, bone sanity, ~4 s
|
||||||
|
|
||||||
|
# Gameplay test with real rendering (inspect frame strip visually):
|
||||||
|
godot --script tests/gameplay_test.gd # opens a window, saves real screenshots
|
||||||
|
# Output: tests/output/gameplay/motion_00..04.png, ragdoll_trigger.png, ragdoll_result.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### What the gameplay test catches
|
||||||
|
| Check | What it detects |
|
||||||
|
|---|---|
|
||||||
|
| Tail Verlet chain spread | Tail bunching — consecutive nodes collapsed to same position |
|
||||||
|
| Ragdoll bone positions finite + within 15 m | Matador limbs exploding after ragdoll impulse |
|
||||||
|
| Ragdoll Y position > −10 | Bones falling through the floor |
|
||||||
|
| Frame strip (visual, manual) | Leg IK quality, animation glitches, anything that looks wrong in motion |
|
||||||
|
|
||||||
|
Headless screenshots will be blank (Forward Plus has no display). Run without `--headless` to get real frame strips.
|
||||||
|
|
||||||
|
## Godot 4.x specifics
|
||||||
|
|
||||||
|
- `wrapf` / `wrap` instead of manual modulo for angles
|
||||||
|
- `lerp_angle` for smooth rotation interpolation (handles wrap-around correctly)
|
||||||
|
- `move_toward` for speed ramps without overshooting
|
||||||
|
- `PackedStringArray`, `PackedVector3Array`, etc. for performance-sensitive arrays
|
||||||
|
- `@tool` scripts for editor helpers only — don't use in gameplay scripts
|
||||||
|
- Resource (`extends Resource`) for shared data/config; no plain `Dictionary` for structured data
|
||||||
|
- `StringName` (`&"action_name"`) for input action lookups in hot paths
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
# Bullosseum — Game Design Document
|
||||||
|
|
||||||
|
> Living document. `?` marks open questions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Elevator Pitch
|
||||||
|
|
||||||
|
You are the bull. Waves of cowboys, matadors, and gladiators pour into your arena — each with their own tricks to dodge or counter your charge. Survive, wreck things, pick upgrades. Every run is different. Goofy low-poly mayhem.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Art Direction
|
||||||
|
|
||||||
|
- **Style:** Low-poly, chunky geometry, flat or minimal shading
|
||||||
|
- **Tone:** Goofy and physical — think *Gang Beasts* meets *Vampire Survivors* energy
|
||||||
|
- **Camera:** Third-person, spring-arm collision avoidance
|
||||||
|
- **Palette:** Warm sand/terracotta arena, vivid faction colours (red/gold for matadors, brown/blue for cowboys, iron/purple for gladiators)
|
||||||
|
- **Particles:** Stylised cartoon puffs — dust, impact sparks, ragdoll stars
|
||||||
|
- **Audio direction:** ?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Confirmed Mechanics
|
||||||
|
|
||||||
|
- Third-person bull locomotion with momentum (walk, charge)
|
||||||
|
- Wide committed turning arc during charge — charges feel like commitments
|
||||||
|
- Visual turn lag (body leans)
|
||||||
|
- Spring-arm camera with zoom
|
||||||
|
- Sand dust particles at hooves (walk = small, charge = big)
|
||||||
|
- Jolt Physics backend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Run Structure (Vampire Survivors-inspired)
|
||||||
|
|
||||||
|
```
|
||||||
|
Start run → Wave spawns → Kill enemies → Wave cleared → Pick upgrade → Next wave → ... → Die or achieve target
|
||||||
|
```
|
||||||
|
|
||||||
|
- **No hard time limit per wave** — pressure comes from enemy density escalating
|
||||||
|
- **Upgrade picks between waves** — choose 1 of 3 options (stat, ability, passive synergy)
|
||||||
|
- **Fail state is soft** — getting overwhelmed is a death spiral, not a timeout. You stop being able to charge effectively and get cornered
|
||||||
|
- **Run length:** ? (15–25 minutes for a full run feels right for web)
|
||||||
|
- **Meta progression:** ? Unlock new starting abilities or bull cosmetics across runs
|
||||||
|
|
||||||
|
### Wave Escalation — Faction Ramp
|
||||||
|
|
||||||
|
Factions are introduced one at a time so each mechanic gets space to teach itself before mixing begins.
|
||||||
|
|
||||||
|
| Waves | Faction makeup | Boss |
|
||||||
|
|---|---|---|
|
||||||
|
| 1–3 | Matadors only | — |
|
||||||
|
| 4 | Matadors only | El Presidente |
|
||||||
|
| 5–7 | Matadors + Cowboys introduced | — |
|
||||||
|
| 8 | Mixed (matadors + cowboys) | Sheriff |
|
||||||
|
| 9–11 | All three factions introduced | — |
|
||||||
|
| 12 | Mixed (all three) | Centurion |
|
||||||
|
| 13+ | Full chaos — all factions, escalating density | Repeat bosses + ? final boss |
|
||||||
|
|
||||||
|
- Each new faction's first wave is a "pure" wave so the player can learn the pattern before it mixes
|
||||||
|
- Bosses appear solo — the arena clears before a boss spawns, making it feel like a duel
|
||||||
|
- ? After wave 12, does a loop begin (harder repeat) or does a final boss end the run?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enemy Factions
|
||||||
|
|
||||||
|
### Matadors (the classics)
|
||||||
|
The trickiest faction — they're designed around baiting your charge.
|
||||||
|
|
||||||
|
| Enemy | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| Matador | Waves red cape to draw your charge, sidesteps at last moment. Must catch them off-guard or predict the dodge |
|
||||||
|
| Banderillero | Plants slow-flags in the ground — charges into one stuns the bull briefly |
|
||||||
|
| Picador | Mounted on horse, tanky, slow. Horse must be hit first before the rider is vulnerable |
|
||||||
|
| ? El Presidente (boss) | Summons matadors and teleports around the arena in bursts |
|
||||||
|
|
||||||
|
### Cowboys
|
||||||
|
The herd faction — individually easy, dangerous in groups.
|
||||||
|
|
||||||
|
| Enemy | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| Cowboy | Throws lasso to root you for 1–2s, then runs. Easy to hit, just annoying |
|
||||||
|
| Wrangler | Herds other enemies toward you — a force multiplier |
|
||||||
|
| Mounted Cowboy | Circles you on horseback, throwing dynamite. Must cut off the riding arc |
|
||||||
|
| ? Sheriff (boss) | Shotgun blast that staggers the bull; spawns deputies on death |
|
||||||
|
|
||||||
|
### Gladiators
|
||||||
|
The armour faction — charge angles matter.
|
||||||
|
|
||||||
|
| Enemy | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| Legionary | Front shield absorbs charges completely. Must hit from the side or behind |
|
||||||
|
| Net-thrower | Long-range root that holds you in place for 3s |
|
||||||
|
| Archer | Stationary on elevated platform (arena wall edge), fires slowing arrows |
|
||||||
|
| ? Centurion (boss) | Buffs nearby gladiators, has a charge-reflect shield that bounces you back |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ability System
|
||||||
|
|
||||||
|
Three slots total — always the same structure, filled differently every run.
|
||||||
|
|
||||||
|
```
|
||||||
|
[Charge ──────────] [Medium □] [Ultimate □]
|
||||||
|
always on 1 slot 1 slot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slot 1 — Charge (always on)
|
||||||
|
| | Description |
|
||||||
|
|---|---|
|
||||||
|
| **Charge** | Hold to build, release to sprint. Committed arc, hard to steer. The foundation everything else is built around |
|
||||||
|
|
||||||
|
### Slot 2 — Medium Ability
|
||||||
|
Cooldown ~8–15s. Tactical, reactive, fills gaps that charge can't. One equipped at a time.
|
||||||
|
|
||||||
|
| Ability | Description | Good against |
|
||||||
|
|---|---|---|
|
||||||
|
| **Stomp** | Slam front hooves — short-range AoE stun | Matador clusters, anything close |
|
||||||
|
| **Gore** | Upward headbutt — launches a single target | Shielded gladiators (bypasses front shield) |
|
||||||
|
| **Tail Whip** | 360° sweep behind and to the sides | Cowboys lassooing from behind |
|
||||||
|
| **Bull Roar** | Knockback shockwave — no damage, pure space creation | Net-throwers, tight groups |
|
||||||
|
| **Ground Pound** | From a jump, landing creates a shockwave crater | Mounted enemies (dismounts them) |
|
||||||
|
| **Counter** | Narrow window — if you charge as an attack lands, explosive ricochet | Picadors, centurion reflect |
|
||||||
|
|
||||||
|
### Slot 3 — Ultimate Ability
|
||||||
|
Cooldown ~35–50s, or requires a full charge meter. High impact. One equipped at a time.
|
||||||
|
|
||||||
|
| Ability | Description | Good against |
|
||||||
|
|---|---|---|
|
||||||
|
| **Stampede** | ~5s of zero deceleration — can't stop or steer well, flattens everything in the path | Boss phases, dense waves |
|
||||||
|
| **Enrage** | ~8s berserk: charge speed +60%, hitbox wider, ignores all slows and roots | Lasso/net-heavy waves |
|
||||||
|
| **Red Mist** | Every enemy on screen gets a red target marker and is pulled slightly toward you — brief guaranteed hits | Matador dodge phase |
|
||||||
|
| **Thunder Horn** | Next charge is electrified — chains damage to up to 4 nearby enemies on contact | Gladiator formations |
|
||||||
|
| **Call of the Herd** | Summon 3 ghost bulls that charge in parallel alongside you for 6s | Any — pure chaos |
|
||||||
|
|
||||||
|
### Card Pick Screen (between every wave)
|
||||||
|
Game pauses. Three cards shown — pick one.
|
||||||
|
|
||||||
|
| Card type | Examples |
|
||||||
|
|---|---|
|
||||||
|
| **New medium** | Swap current medium for a different one |
|
||||||
|
| **New ultimate** | Swap current ultimate for a different one |
|
||||||
|
| **Stat boost** | +charge speed, +turn rate, −medium cooldown, −ultimate cooldown |
|
||||||
|
| **Passive modifier** | Changes how your *current* ability behaves (see below) |
|
||||||
|
| **Synergy** | Unlocks a cross-ability combo effect |
|
||||||
|
|
||||||
|
**Passive modifier examples:**
|
||||||
|
- Stomp → "Dust Blind" — stun cloud also blocks enemy vision for 2s
|
||||||
|
- Gore → "Pinball" — launched enemy bounces off walls and damages others on landing
|
||||||
|
- Stampede → "Aftershock" — leaves a trembling ground trail that slows enemies for 5s
|
||||||
|
- Enrage → "Bloodlust" — kills during enrage reduce remaining cooldown
|
||||||
|
|
||||||
|
**Synergy examples:**
|
||||||
|
- Charge + Stomp = "Seismic Run" — charging for 2s+ turns your next stomp into a ground wave
|
||||||
|
- Gore + Thunder Horn = "Lightning Rod" — electrified gore sends a chain to the whole group
|
||||||
|
- ? Others to discover during design
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arena Design
|
||||||
|
|
||||||
|
Each faction maps to an arena environment. Mixed-faction waves could transition the arena or blend themes.
|
||||||
|
|
||||||
|
| Faction | Arena | Hazards |
|
||||||
|
|---|---|---|
|
||||||
|
| Matadors | Sand bullring, wooden barrier ring | Flags slow movement, open centre is the danger zone |
|
||||||
|
| Cowboys | Frontier rodeo grounds, hay bales, fences | Lasso posts (rope traps), horse stampede lanes |
|
||||||
|
| Gladiators | Roman colosseum, stone floor, iron gates | Archer towers on walls, spike traps, wet stone (slide) |
|
||||||
|
|
||||||
|
- ? Does the arena change mid-run or is one arena selected at run start?
|
||||||
|
- ? Destructible fence/barrier sections throughout?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Loop Revisited
|
||||||
|
|
||||||
|
```
|
||||||
|
Move → Spot threat pattern → Position approach angle → Charge/Ability → Chain hits → Wave clear
|
||||||
|
↑ ↓
|
||||||
|
Dodge/react to debuffs (lasso, net, slow-flag) Pick upgrade (1 of 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
The **positioning** step is where skill expression lives: which enemy to hit first, what angle to approach a shielded gladiator, whether to charge the matador or fake him out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Meta progression** — cosmetic unlocks only, or do you unlock new starting medium/ultimate options across runs?
|
||||||
|
2. **Arena change** — single arena per run, or does the arena theme shift when a new faction is introduced mid-run?
|
||||||
|
3. **After wave 12** — infinite loop with scaling difficulty, or a final boss that ends the run cleanly?
|
||||||
|
4. **Ability replacement** — when you pick a new medium/ultimate card, does it always replace the slot, or can you upgrade an existing ability through repeated picks (level 1→2→3)?
|
||||||
|
5. **Web scope** — vertical slice (matadors only, 4 waves, one ability tree) or full 12-wave game?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- *Vampire Survivors* — wave escalation, upgrade pick loop, run length
|
||||||
|
- *Gang Beasts* — ragdoll slapstick commitment
|
||||||
|
- *Katamari* / *Donut County* — simple core verb scaled through variety
|
||||||
|
- *Superhot* — charge-as-commitment, every move mattering
|
||||||
|
- *Cult of the Lamb* — faction variety with distinct visual languages
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[gd_scene format=3 uid="uid://matador_scene_v1"]
|
||||||
|
|
||||||
|
[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"]
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
||||||
|
radius = 0.25
|
||||||
|
height = 1.2
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hit"]
|
||||||
|
radius = 0.45
|
||||||
|
height = 1.4
|
||||||
|
|
||||||
|
[node name="Matador" type="CharacterBody3D"]
|
||||||
|
script = ExtResource("1_matgd")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||||
|
shape = SubResource("CapsuleShape3D_body")
|
||||||
|
|
||||||
|
[node name="HitArea" type="Area3D" parent="."]
|
||||||
|
monitoring = true
|
||||||
|
monitorable = false
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
|
||||||
|
shape = SubResource("CapsuleShape3D_hit")
|
||||||
|
|
||||||
|
[node name="matador_v02" parent="." instance=ExtResource("2_matfbx")]
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
[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" 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"]
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_chest"]
|
||||||
|
radius = 0.35
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_belly"]
|
||||||
|
radius = 0.40
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_hips"]
|
||||||
|
radius = 0.30
|
||||||
|
|
||||||
|
[node name="Player" type="CharacterBody3D" unique_id=165510044]
|
||||||
|
collision_layer = 3
|
||||||
|
script = ExtResource("1_xhfnw")
|
||||||
|
|
||||||
|
[node name="CollisionChest" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, -0.2)
|
||||||
|
shape = SubResource("SphereShape3D_chest")
|
||||||
|
|
||||||
|
[node name="CollisionBelly" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.1)
|
||||||
|
shape = SubResource("SphereShape3D_belly")
|
||||||
|
|
||||||
|
[node name="CollisionHips" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.505, 0.4)
|
||||||
|
shape = SubResource("SphereShape3D_hips")
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="." unique_id=1120161087]
|
||||||
|
script = ExtResource("2_camera_iso")
|
||||||
|
|
||||||
|
[node name="bull_v10" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
|
||||||
|
|
||||||
|
[editable path="bull_v10"]
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Contribution
|
||||||
|
|
||||||
|
Hello, thanks for taking time and helping out with the addon!
|
||||||
|
|
||||||
|
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. Please sign the CLA before sending the PR: https://www.jetbrains.com/agreements/cla/.
|
||||||
|
|
||||||
|
#### Local setup
|
||||||
|
|
||||||
|
Open the `godot-editor-plugin/CMakeLists.txt` in Rider 2026.1+, select `rider-gdextension` target in the run configuration selector.
|
||||||
|
There are some more fixes coming in 2026.2, which will allow working on gdscript and cpp files within the same workspace.
|
||||||
|
|
||||||
|
#### Signing the binaries
|
||||||
|
|
||||||
|
Plugin binaries need to be signed to comply with modern operating system security requirements. Unsigned dynamic libraries may fail to load or trigger security warnings:
|
||||||
|
|
||||||
|
- **macOS**: Requires code signing (and often notarization) for `.dylib` files. Unsigned libraries may simply fail to load.
|
||||||
|
- **Windows**: SmartScreen may block unsigned binaries, and corporate environments often enforce signed code only.
|
||||||
|
- **Linux**: Generally allows unsigned `.so` files, but some sandboxed environments (Flatpak, Snap) impose restrictions.
|
||||||
|
|
||||||
|
**How signing is implemented:**
|
||||||
|
|
||||||
|
1. **TeamCity Configuration**: [ijplatform_master_Net_Deploy_Plugins_Public_Godot](https://buildserver.labs.intellij.net/buildConfiguration/ijplatform_master_Net_Deploy_Plugins_Public_Godot) (internal JetBrains link)
|
||||||
|
- Note: The automatic trigger on new tags is currently not working, so manual triggering is required.
|
||||||
|
|
||||||
|
2. **Release Process**:
|
||||||
|
- First, use your GitHub Action to prepare a **pre-release** with a tag
|
||||||
|
- Manually trigger the TeamCity configuration on the necessary branch/tag
|
||||||
|
- The configuration will download assets, sign them, and upload them back (removing the pre-release flag)
|
||||||
|
- The pre-release flag is required at the start; otherwise, the signing configuration will skip the release (protection against double signing)
|
||||||
|
|
||||||
|
3. **Signing existing releases**:
|
||||||
|
- You can run signing on already existing old releases/tags
|
||||||
|
- Mark them as pre-release first, then run the configuration on the necessary tag
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 JetBrains
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# JetBrains Rider Integration – Godot addon
|
||||||
|
|
||||||
|
This addon currently provides two features:
|
||||||
|
1. Finds all Rider installations in the system and provides a selector on the `Text Editor` -> `External` tab in the settings to select one.
|
||||||
|
2. Provides the "Use Rider" toggle in the Godot toolbar and, when enabled, applies a set of editor settings recommended for working with JetBrains Rider. The goal is to make it trivial to switch between Rider‑optimized settings and stock Godot settings with a single click.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Godot 4.2.2+
|
||||||
|
|
||||||
|
Install:
|
||||||
|
1. Inside the Godot editor, it can be installed from the AssetLib view or [downloaded](https://godotengine.org/asset-library/asset/4576)
|
||||||
|
2. [Optional] Change the initial value of `active` in the plugin.cfg
|
||||||
|
3. [Optional] Change the initial values in the presets.json file.
|
||||||
|
4. Enable "JetBrains Rider External Editor" plugin in the Project → Project Settings… → Plugins tab.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- A toolbar toggle named "Use Rider" will appear. Click it to turn the preset On/Off.
|
||||||
|
|
||||||
|
Screenshot:
|
||||||
|

|
||||||
|
|
||||||
|
## What the toggle changes
|
||||||
|
|
||||||
|
The preset values live in `presets.json`.
|
||||||
|
|
||||||
|
When ON:
|
||||||
|
- Write the values from the "on" preset into the Editor Settings.
|
||||||
|
|
||||||
|
When OFF:
|
||||||
|
- Write the values from the "off" preset into the Editor Settings.
|
||||||
|
|
||||||
|
Note: The plugin does not currently auto‑set Rider’s executable path or flags. See Plans below.
|
||||||
|
|
||||||
|
## Setting Rider to be the external editor
|
||||||
|
|
||||||
|
The plugin automatically detects installed Rider versions on your system and provides a convenient dropdown menu to
|
||||||
|
select which installation to use as your external editor.
|
||||||
|
|
||||||
|
- The plugin scans common installation locations for Rider on Windows, macOS, and Linux.
|
||||||
|
- Detected installations appear in the "Select Rider" dropdown in the toolbar.
|
||||||
|
- When you select a Rider installation, the plugin automatically updates the `dotnet/editor/external_editor_path` editor
|
||||||
|
setting.
|
||||||
|
|
||||||
|
## License
|
||||||
|
See `addons/rider-plugin/LICENCE`.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
Created by JetBrains to streamline using Rider with Godot.
|
||||||
|
Initial idea https://github.com/sszigeti/toggle_external_editor
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
[configuration]
|
||||||
|
|
||||||
|
entry_symbol = "rider_library_init"
|
||||||
|
compatibility_minimum = "4.2.2"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
|
||||||
|
macos.single.debug = "./macos/librider-gdextension.macos.editor.dylib"
|
||||||
|
|
||||||
|
windows.arm64.single.debug = "./windows/rider-gdextension.windows.editor.arm64.dll"
|
||||||
|
|
||||||
|
windows.x86_64.single.debug = "./windows/rider-gdextension.windows.editor.x86_64.dll"
|
||||||
|
|
||||||
|
linux.x86_64.single.debug = "./linux/librider-gdextension.linux.editor.x86_64.so"
|
||||||
|
|
||||||
|
linux.arm64.single.debug = "./linux/librider-gdextension.linux.editor.arm64.so"
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,144 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.0"
|
||||||
|
id="katman_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="JetBrains Rider Icon.svg"
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs43" /><sodipodi:namedview
|
||||||
|
id="namedview41"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:pageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.0980553"
|
||||||
|
inkscape:cx="256.36233"
|
||||||
|
inkscape:cy="212.19333"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1027"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="22"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="katman_1" />
|
||||||
|
<style
|
||||||
|
type="text/css"
|
||||||
|
id="style2">
|
||||||
|
.st0{fill:url(#SVGID_1_);}
|
||||||
|
.st1{fill:url(#SVGID_00000130622934180993703410000017420799098261276343_);}
|
||||||
|
.st2{fill:url(#SVGID_00000060739771362873723200000017991140373209755022_);}
|
||||||
|
.st3{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<symbol
|
||||||
|
id="rider"
|
||||||
|
viewBox="-35 -35 70 70">
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_1_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="30.4897"
|
||||||
|
y1="5.1188998"
|
||||||
|
x2="-23.4683"
|
||||||
|
y2="-25.8451">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#DD1265"
|
||||||
|
id="stop4" />
|
||||||
|
<stop
|
||||||
|
offset="0.483"
|
||||||
|
style="stop-color:#DD1265"
|
||||||
|
id="stop6" />
|
||||||
|
<stop
|
||||||
|
offset="0.942"
|
||||||
|
style="stop-color:#FDB60D"
|
||||||
|
id="stop8" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="M 35,-7.7 -14.1,-35 18.8,13.9 25.5,9.5 Z"
|
||||||
|
id="path11" />
|
||||||
|
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_00000169517474296634577950000016895268102491994795_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="-1.5839"
|
||||||
|
y1="-28.888"
|
||||||
|
x2="19.805099"
|
||||||
|
y2="30.174999">
|
||||||
|
<stop
|
||||||
|
offset="0.139"
|
||||||
|
style="stop-color:#087CFA"
|
||||||
|
id="stop13" />
|
||||||
|
<stop
|
||||||
|
offset="0.476"
|
||||||
|
style="stop-color:#DD1265"
|
||||||
|
id="stop15" />
|
||||||
|
<stop
|
||||||
|
offset="0.958"
|
||||||
|
style="stop-color:#087CFA"
|
||||||
|
id="stop17" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
style="fill:url(#SVGID_00000169517474296634577950000016895268102491994795_)"
|
||||||
|
d="M 15.5,-18.9 9.3,-33.9 -4.3,-20.5 1.2,28.1 14.4,35 35,23 Z"
|
||||||
|
id="path20" />
|
||||||
|
|
||||||
|
<linearGradient
|
||||||
|
id="SVGID_00000135668350575375336630000014638141720088491653_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="-17.5865"
|
||||||
|
y1="-27.071199"
|
||||||
|
x2="-1.7875"
|
||||||
|
y2="29.073799">
|
||||||
|
<stop
|
||||||
|
offset="0.278"
|
||||||
|
style="stop-color:#DD1265"
|
||||||
|
id="stop22" />
|
||||||
|
<stop
|
||||||
|
offset="0.968"
|
||||||
|
style="stop-color:#FDB60D"
|
||||||
|
id="stop24" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
style="fill:url(#SVGID_00000135668350575375336630000014638141720088491653_)"
|
||||||
|
d="m -14.1,-35 -20.9,14.1 7.8,48.1 20.1,7.7 26,-21 z"
|
||||||
|
id="path27" />
|
||||||
|
<path
|
||||||
|
d="M 21,-21 H -21 V 21 H 21 Z"
|
||||||
|
id="path29" />
|
||||||
|
<path
|
||||||
|
class="st3"
|
||||||
|
d="m -0.6,13.6 h -15.8 v 2.7 h 15.8 z"
|
||||||
|
id="path31" />
|
||||||
|
<path
|
||||||
|
class="st3"
|
||||||
|
d="m 0.5,-15.8 h 6.2 c 5,0 8.4,3.4 8.4,7.7 0,4.4 -3.4,7.9 -8.4,7.9 L 0.5,0 Z m 3.5,3.2 v 9.5 h 2.7 c 2.8,0 4.8,-1.9 4.8,-4.6 0,-2.8 -1.9,-4.7 -4.8,-4.7 z"
|
||||||
|
id="path33" />
|
||||||
|
<path
|
||||||
|
class="st3"
|
||||||
|
d="m -15.7,-15.8 h 7.2 c 2,0 3.5,0.6 4.6,1.6 0.9,0.9 1.3,2.1 1.3,3.6 v 0.1 c 0,1.3 -0.3,2.3 -0.9,3.1 -0.6,0.8 -1.4,1.4 -2.4,1.8 L -2,0 h -4.1 l -3.3,-4.8 h -2.9 V 0 h -3.5 z m 7,7.7 c 0.8,0 1.5,-0.2 2,-0.6 0.5,-0.4 0.7,-1 0.7,-1.6 v -0.1 c 0,-0.8 -0.2,-1.3 -0.7,-1.7 -0.5,-0.3 -1.1,-0.6 -2,-0.6 h -3.4 v 4.5 h 3.4 z"
|
||||||
|
id="path35" />
|
||||||
|
</symbol>
|
||||||
|
<use
|
||||||
|
xlink:href="#rider"
|
||||||
|
width="70"
|
||||||
|
height="70"
|
||||||
|
x="-35"
|
||||||
|
y="-35"
|
||||||
|
transform="matrix(7.0682519,0,0,7.2481031,256,256)"
|
||||||
|
style="overflow:visible"
|
||||||
|
id="use38" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://wrdrj6wednn8"
|
||||||
|
path="res://.godot/imported/icon.svg-45c914cff7482ba9564963fe65b548e4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/rider-plugin/icons/icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-45c914cff7482ba9564963fe65b548e4.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
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="JetBrains Rider External Editor"
|
||||||
|
description="Provides a toolbar toggle, to switch a set of settings on and off. Default set of settings helps to enable/disable the following settings recommended by the Rider documentation https://www.jetbrains.com/help/rider/Godot.html#optimize-godot-editor-for-rider"
|
||||||
|
author="JetBrains"
|
||||||
|
version="1.0.0"
|
||||||
|
script="rider-plugin.gd"
|
||||||
|
|
||||||
|
[presets]
|
||||||
|
|
||||||
|
active="off"
|
||||||
|
presets="presets.json"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"on": {
|
||||||
|
"text_editor/external/use_external_editor": true,
|
||||||
|
"interface/editor/import_resources_when_unfocused": true,
|
||||||
|
"interface/editor/save_on_focus_loss": true,
|
||||||
|
"text_editor/behavior/files/auto_reload_scripts_on_external_change": true,
|
||||||
|
"run/window_placement/game_embed_mode": -1
|
||||||
|
},
|
||||||
|
"off": {
|
||||||
|
"text_editor/external/use_external_editor": false,
|
||||||
|
"interface/editor/import_resources_when_unfocused": false,
|
||||||
|
"interface/editor/save_on_focus_loss": false,
|
||||||
|
"text_editor/behavior/files/auto_reload_scripts_on_external_change": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
var editor_settings: EditorSettings
|
||||||
|
var checkbutton: CheckButton
|
||||||
|
var _preset_applier: PresetApplier
|
||||||
|
var _settings_service: EditorSettingsService
|
||||||
|
var _locator_service: RiderLocatorService
|
||||||
|
var _plugin_cfg_path: String
|
||||||
|
var _presets_json_path: String
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
editor_settings = EditorInterface.get_editor_settings()
|
||||||
|
var script_path := (get_script() as Script).resource_path
|
||||||
|
var plugin_dir := script_path.get_base_dir()
|
||||||
|
_plugin_cfg_path = plugin_dir + "/plugin.cfg"
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
var err := cfg.load(_plugin_cfg_path)
|
||||||
|
if err != OK:
|
||||||
|
push_warning("Failed to load plugin.cfg: %s" % [err])
|
||||||
|
return
|
||||||
|
var active_str := str(cfg.get_value("presets", "active", "on"))
|
||||||
|
var is_active := active_str == "on"
|
||||||
|
var presets_rel_path := str(cfg.get_value("presets", "presets", "presets.json"))
|
||||||
|
_presets_json_path = plugin_dir + "/" + presets_rel_path
|
||||||
|
|
||||||
|
# Build UI
|
||||||
|
checkbutton = CheckButton.new()
|
||||||
|
checkbutton.text = "Use Rider"
|
||||||
|
checkbutton.tooltip_text = "Shortcut for setting recommended settings"
|
||||||
|
checkbutton.button_pressed = is_active
|
||||||
|
checkbutton.pressed.connect(_on_checkbutton_pressed)
|
||||||
|
add_control_to_container(EditorPlugin.CONTAINER_TOOLBAR, checkbutton)
|
||||||
|
|
||||||
|
# Initialize services and panel
|
||||||
|
_settings_service = EditorSettingsService.new()
|
||||||
|
_locator_service = RiderLocatorService.new()
|
||||||
|
_preset_applier = PresetApplier.new(_presets_json_path)
|
||||||
|
|
||||||
|
_locator_service.add_selector_in_editor_interface(_settings_service)
|
||||||
|
|
||||||
|
# Ensure settings reflect current state on startup
|
||||||
|
_preset_applier.apply_preset(editor_settings, is_active)
|
||||||
|
|
||||||
|
func _on_checkbutton_pressed() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(_plugin_cfg_path) != OK:
|
||||||
|
push_warning("Failed to load plugin.cfg to update state")
|
||||||
|
return
|
||||||
|
var is_active := checkbutton.button_pressed
|
||||||
|
var key := _preset_applier.get_preset_key(is_active)
|
||||||
|
cfg.set_value("presets", "active", key)
|
||||||
|
var save_err := cfg.save(_plugin_cfg_path)
|
||||||
|
if save_err != OK:
|
||||||
|
push_warning("Failed to save plugin.cfg: %s" % [save_err])
|
||||||
|
# Apply selected preset to editor settings
|
||||||
|
_preset_applier.apply_preset(editor_settings, is_active)
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
if checkbutton != null:
|
||||||
|
remove_control_from_container(EditorPlugin.CONTAINER_TOOLBAR, checkbutton)
|
||||||
|
checkbutton.queue_free()
|
||||||
|
|
||||||
|
var args = OS.get_cmdline_args()
|
||||||
|
if "--rider-addon-tests" in args:
|
||||||
|
print("==== rider-addon-tests finished ====")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1x58xm2w1n20
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bro2w3ghx4xt7"
|
||||||
|
path="res://.godot/imported/Toolbar.png-a521e2493bd3c08a829245b3129bb58f.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/rider-plugin/screenshots/Toolbar.png"
|
||||||
|
dest_files=["res://.godot/imported/Toolbar.png-a521e2493bd3c08a829245b3129bb58f.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@tool
|
||||||
|
## Simple JSON helpers for editor/runtime use.
|
||||||
|
## Keeps file and JSON parsing concerns out of feature code.
|
||||||
|
class_name JsonUtils
|
||||||
|
|
||||||
|
static func load_from_file(path: String) -> Variant:
|
||||||
|
# Returns parsed JSON value (Dictionary/Array/etc.) or null on error.
|
||||||
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
push_warning("JsonUtils: Failed to open file: %s" % path)
|
||||||
|
return null
|
||||||
|
var text := file.get_as_text()
|
||||||
|
file.close()
|
||||||
|
var data: Variant = JSON.parse_string(text)
|
||||||
|
if data == null:
|
||||||
|
push_warning("JsonUtils: Invalid JSON in file: %s" % path)
|
||||||
|
return null
|
||||||
|
return data
|
||||||
|
|
||||||
|
static func load_dict_from_file(path: String) -> Dictionary:
|
||||||
|
# Returns Dictionary or empty {} on error.
|
||||||
|
var data := load_from_file(path) as Dictionary
|
||||||
|
return data
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bci4kmk7h4j6a
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
@tool
|
||||||
|
extends RefCounted
|
||||||
|
class_name RiderLocatorService
|
||||||
|
|
||||||
|
var _installations_found: Array = []
|
||||||
|
var _thread: Thread = null
|
||||||
|
|
||||||
|
func get_installations() -> Array:
|
||||||
|
var result: Array = RiderLocator.new().get_installations() # from the gdextension
|
||||||
|
return result
|
||||||
|
|
||||||
|
# todo
|
||||||
|
func fix_external_editor_if_supplied_in_commandline(_settings_service: EditorSettingsService, editor_settings: EditorSettings) -> bool:
|
||||||
|
# When Godot is started from Rider (or vice versa), we may receive the Rider path
|
||||||
|
# via command-line so we can keep Godot's external editor setting in sync.
|
||||||
|
# Supported form (only this one):
|
||||||
|
# --my_rider_path="/absolute/path/to/rider with possible spaces"
|
||||||
|
var args : Array = OS.get_cmdline_args()
|
||||||
|
var provided_rider_path := ""
|
||||||
|
for a_raw in args:
|
||||||
|
var a: String = str(a_raw)
|
||||||
|
if a.begins_with("--my_rider_path="):
|
||||||
|
provided_rider_path = a.substr("--my_rider_path=".length())
|
||||||
|
break
|
||||||
|
|
||||||
|
if provided_rider_path.is_empty():
|
||||||
|
return false
|
||||||
|
|
||||||
|
provided_rider_path = trim_quotes(provided_rider_path)
|
||||||
|
|
||||||
|
# Validate existence (file or dir)
|
||||||
|
var looks_existing := FileAccess.file_exists(provided_rider_path) || DirAccess.dir_exists_absolute(provided_rider_path)
|
||||||
|
if looks_existing:
|
||||||
|
_settings_service.set_external_editor_path(editor_settings, provided_rider_path)
|
||||||
|
print("Rider path provided via CLI (my_rider_path): ", provided_rider_path)
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
push_warning("my_rider_path was provided but does not exist: %s" % [provided_rider_path])
|
||||||
|
return false
|
||||||
|
|
||||||
|
func trim_quotes(s: String) -> String:
|
||||||
|
if s.begins_with('"') and s.ends_with('"') and s.length() >= 2:
|
||||||
|
return s.substr(1, s.length() - 2)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
func add_selector_in_editor_interface(_settings_service: EditorSettingsService):
|
||||||
|
_update_selector(_installations_found)
|
||||||
|
|
||||||
|
if _installations_found.is_empty() and _thread == null:
|
||||||
|
_thread = Thread.new()
|
||||||
|
_thread.start(_load_installations)
|
||||||
|
|
||||||
|
func _load_installations() -> void:
|
||||||
|
var array: Array = get_installations()
|
||||||
|
call_deferred("_on_installations_loaded", array)
|
||||||
|
|
||||||
|
func _on_installations_loaded(array: Array):
|
||||||
|
_installations_found = array
|
||||||
|
if _thread:
|
||||||
|
_thread.wait_to_finish()
|
||||||
|
_thread = null
|
||||||
|
_update_selector(_installations_found)
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_PREDELETE and _thread != null:
|
||||||
|
_thread.wait_to_finish()
|
||||||
|
|
||||||
|
func _update_selector(array: Array):
|
||||||
|
var name := "text_editor/external/editor"
|
||||||
|
var settings := EditorInterface.get_editor_settings()
|
||||||
|
|
||||||
|
if !(settings.has_setting(name)):
|
||||||
|
settings.set(name, 0)
|
||||||
|
|
||||||
|
var installations: Array = ["Custom"]
|
||||||
|
for element in array:
|
||||||
|
var display_name: String = element.get("display", "")
|
||||||
|
# Replace special characters that break PROPERTY_HINT_ENUM format
|
||||||
|
# Comma is the enum delimiter, colon is used for explicit value assignment
|
||||||
|
display_name = display_name.replace(",", " •").replace(":", " -")
|
||||||
|
installations.append(display_name)
|
||||||
|
var options :String = ",".join(installations)
|
||||||
|
|
||||||
|
settings.add_property_info({
|
||||||
|
"name": name,
|
||||||
|
"type":TYPE_INT,
|
||||||
|
"hint":PROPERTY_HINT_ENUM,
|
||||||
|
"hint_string": options
|
||||||
|
})
|
||||||
|
|
||||||
|
# Connect to settings changes to update external editor path when selection changes
|
||||||
|
if not settings.settings_changed.is_connected(_on_selection_changed):
|
||||||
|
settings.settings_changed.connect(_on_selection_changed.bind())
|
||||||
|
|
||||||
|
func _on_selection_changed() -> void:
|
||||||
|
var name := "text_editor/external/editor"
|
||||||
|
var settings := EditorInterface.get_editor_settings()
|
||||||
|
var selected_index: int = settings.get_setting(name)
|
||||||
|
|
||||||
|
# Index 0 is "Custom", so user manages the path manually
|
||||||
|
if selected_index == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Map to actual installation (offset by 1 because of "Custom" at index 0)
|
||||||
|
var installation_index := selected_index - 1
|
||||||
|
var installations_array = _installations_found
|
||||||
|
if installation_index >= 0 and installation_index < installations_array.size():
|
||||||
|
var installation = installations_array[installation_index]
|
||||||
|
var new_path: String = installation.get("path", "")
|
||||||
|
if not new_path.is_empty():
|
||||||
|
EditorSettingsService.new().set_external_editor_path(settings, new_path)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cfsu1rbg0ypem
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@tool
|
||||||
|
extends RefCounted
|
||||||
|
class_name PresetApplier
|
||||||
|
|
||||||
|
var presets_path: String
|
||||||
|
|
||||||
|
func _init(p_path: String) -> void:
|
||||||
|
presets_path = p_path
|
||||||
|
|
||||||
|
func get_preset_key(is_active: bool) -> String:
|
||||||
|
return "on" if is_active else "off"
|
||||||
|
|
||||||
|
func apply_preset(editor_settings: EditorSettings, is_active: bool) -> void:
|
||||||
|
var data: Dictionary = JsonUtils.load_dict_from_file(presets_path)
|
||||||
|
if data.is_empty():
|
||||||
|
push_warning("Failed to load presets: %s" % presets_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
var new_preset_key := get_preset_key(is_active)
|
||||||
|
var previous_preset_key := get_preset_key(not is_active)
|
||||||
|
|
||||||
|
if not data.has(new_preset_key):
|
||||||
|
push_warning("Preset '%s' not found in presets.json" % new_preset_key)
|
||||||
|
return
|
||||||
|
|
||||||
|
var new_preset := data[new_preset_key] as Dictionary
|
||||||
|
# Reset keys from previous preset that are missing in the new preset
|
||||||
|
if data.has(previous_preset_key):
|
||||||
|
var previous_preset := data[previous_preset_key] as Dictionary
|
||||||
|
for key in previous_preset:
|
||||||
|
if not new_preset.has(key):
|
||||||
|
editor_settings.set_setting(str(key), editor_settings.property_get_revert(str(key)))
|
||||||
|
# Apply the new preset
|
||||||
|
for key in new_preset:
|
||||||
|
editor_settings.set_setting(str(key), new_preset[key])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bjiaycxso8h8u
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@tool
|
||||||
|
extends RefCounted
|
||||||
|
class_name EditorSettingsService
|
||||||
|
|
||||||
|
func set_external_editor_path(editor_settings: EditorSettings, path: String) -> void:
|
||||||
|
editor_settings.set_setting("text_editor/external/exec_path", path)
|
||||||
|
|
||||||
|
func has_valid_external_editor_path(editor_settings: EditorSettings) -> bool:
|
||||||
|
var has_setting: bool = editor_settings.has_setting("text_editor/external/exec_path")
|
||||||
|
if not has_setting:
|
||||||
|
return false
|
||||||
|
var path : String = editor_settings.get_setting("text_editor/external/exec_path")
|
||||||
|
var exists := not path.is_empty() && (FileAccess.file_exists(path) || DirAccess.dir_exists_absolute(path))
|
||||||
|
return exists
|
||||||
|
|
||||||
|
func set_use_external_editor(editor_settings: EditorSettings, enabled: bool) -> void:
|
||||||
|
editor_settings.set_setting("text_editor/external/use_external_editor", enabled)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bdiu78ot0rrkc
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
extends AnimationPlayer
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
speed_scale = randf_range(0.5, 1.5)
|
||||||
|
play("ArmatureAction")
|
||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
extends Node
|
||||||
|
## Body wobble and tail simulation. Legs stay in rest pose (no IK, no ragdoll).
|
||||||
|
## Created at runtime by player.gd; call setup() before the first _process tick.
|
||||||
|
|
||||||
|
var _player: CharacterBody3D
|
||||||
|
var _bull: Node3D
|
||||||
|
var _skeleton: Skeleton3D
|
||||||
|
|
||||||
|
# Body wobble
|
||||||
|
var _prev_vel: Vector3 = Vector3.ZERO
|
||||||
|
var _tilt_x: float = 0.0
|
||||||
|
var _tilt_z: float = 0.0
|
||||||
|
var _aerial_pitch: float = 0.0
|
||||||
|
|
||||||
|
var charge_pitch_target: float = 0.0
|
||||||
|
var _charge_pitch_curr: float = 0.0
|
||||||
|
|
||||||
|
# Tail — Verlet chain simulation
|
||||||
|
const TAIL_BONES: Array = [
|
||||||
|
&"tail_1", &"tail_2", &"tail_3", &"tail_4",
|
||||||
|
&"tail_5", &"tail_6",
|
||||||
|
&"tail_tip_1", &"tail_tip_2", &"tail_tip_2_end",
|
||||||
|
]
|
||||||
|
var _tail_idx: Array[int] = []
|
||||||
|
var _tail_world: Array[Vector3] = []
|
||||||
|
var _tail_prev: Array[Vector3] = []
|
||||||
|
var _tail_lengths: Array[float] = []
|
||||||
|
var _tail_rest_dir: Array[Vector3] = []
|
||||||
|
|
||||||
|
|
||||||
|
func setup(player: CharacterBody3D, bull: Node3D) -> void:
|
||||||
|
_player = player
|
||||||
|
_bull = bull
|
||||||
|
_skeleton = _find_skeleton(bull)
|
||||||
|
if not _skeleton:
|
||||||
|
push_error("BullLegs: Skeleton3D not found under bull_v10")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Disable any physical bone simulator
|
||||||
|
var phys_sim := _find_physical_bones(_skeleton)
|
||||||
|
if phys_sim:
|
||||||
|
phys_sim.active = false
|
||||||
|
|
||||||
|
_setup_tail()
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_tail() -> void:
|
||||||
|
for bone_name: StringName in TAIL_BONES:
|
||||||
|
var idx: int = _skeleton.find_bone(bone_name)
|
||||||
|
if idx == -1:
|
||||||
|
push_warning("BullLegs: tail bone '%s' not found" % bone_name)
|
||||||
|
continue
|
||||||
|
_tail_idx.append(idx)
|
||||||
|
|
||||||
|
if _tail_idx.size() < 2:
|
||||||
|
push_error("BullLegs: not enough tail bones found")
|
||||||
|
return
|
||||||
|
|
||||||
|
for idx: int in _tail_idx:
|
||||||
|
var world: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(idx).origin)
|
||||||
|
_tail_world.append(world)
|
||||||
|
_tail_prev.append(world)
|
||||||
|
|
||||||
|
var max_sep: float = 0.0
|
||||||
|
for i in range(_tail_world.size() - 1):
|
||||||
|
max_sep = maxf(max_sep, _tail_world[i].distance_to(_tail_world[i + 1]))
|
||||||
|
if max_sep < 0.01:
|
||||||
|
push_warning("BullLegs: tail bones have zero separation in rest pose, distributing along spine")
|
||||||
|
var root: Vector3 = _tail_world[0]
|
||||||
|
var back: Vector3 = (_skeleton.global_transform.basis.z + Vector3.DOWN * 0.3).normalized()
|
||||||
|
for i in range(_tail_world.size()):
|
||||||
|
_tail_world[i] = root + back * (i * 0.09)
|
||||||
|
_tail_prev[i] = _tail_world[i]
|
||||||
|
|
||||||
|
const MIN_SEG: float = 0.06
|
||||||
|
for i in range(_tail_world.size() - 1):
|
||||||
|
var seg_len: float = _tail_world[i].distance_to(_tail_world[i + 1])
|
||||||
|
_tail_lengths.append(maxf(seg_len, MIN_SEG))
|
||||||
|
|
||||||
|
for i in range(_tail_world.size() - 1):
|
||||||
|
var p1_skel: Vector3 = _skeleton.to_local(_tail_world[i])
|
||||||
|
var p2_skel: Vector3 = _skeleton.to_local(_tail_world[i + 1])
|
||||||
|
var seg_skel: Vector3 = p2_skel - p1_skel
|
||||||
|
_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])
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not _skeleton:
|
||||||
|
return
|
||||||
|
_wobble_body(delta)
|
||||||
|
_update_tail(delta)
|
||||||
|
_apply_tail()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Body Wobble ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _wobble_body(delta: float) -> void:
|
||||||
|
var vel: Vector3 = _player.velocity
|
||||||
|
var accel: Vector3 = (vel - _prev_vel) / maxf(delta, 0.001)
|
||||||
|
_prev_vel = vel
|
||||||
|
|
||||||
|
var la: Vector3 = _bull.global_transform.basis.inverse() * accel
|
||||||
|
|
||||||
|
var tx := clampf(-la.z * DP.f("body_tilt_fwd"), -0.35, 0.35)
|
||||||
|
var tz := clampf(-la.x * DP.f("body_tilt_side"), -0.25, 0.25)
|
||||||
|
|
||||||
|
_tilt_x = lerpf(_tilt_x, tx, delta * 10.0)
|
||||||
|
_tilt_z = lerpf(_tilt_z, tz, delta * 10.0)
|
||||||
|
|
||||||
|
var flat_spd: float = Vector2(vel.x, vel.z).length()
|
||||||
|
var target_pitch: float = 0.0
|
||||||
|
if not _player.is_on_floor() and flat_spd > 0.5:
|
||||||
|
target_pitch = clampf(-atan2(vel.y, flat_spd) * DP.f("aerial_pitch_scale"), -1.4, 1.4)
|
||||||
|
_aerial_pitch = lerpf(_aerial_pitch, target_pitch, delta * DP.f("aerial_pitch_speed"))
|
||||||
|
|
||||||
|
_charge_pitch_curr = lerpf(_charge_pitch_curr, charge_pitch_target, delta * DP.f("charge_head_speed"))
|
||||||
|
_bull.rotation.x = _tilt_x + _aerial_pitch + _charge_pitch_curr
|
||||||
|
_bull.rotation.z = _tilt_z
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tail ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _update_tail(delta: float) -> void:
|
||||||
|
if _tail_world.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
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 anchor: Vector3 = _skeleton.to_global(_skeleton.get_bone_global_rest(_tail_idx[0]).origin)
|
||||||
|
_tail_world[0] = anchor
|
||||||
|
_tail_prev[0] = anchor
|
||||||
|
|
||||||
|
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_freq: float = DP.f("tail_wag_freq")
|
||||||
|
var time: float = Time.get_ticks_msec() / 1000.0
|
||||||
|
var basis_x: Vector3 = _bull.global_transform.basis.x
|
||||||
|
|
||||||
|
for i in range(1, _tail_world.size()):
|
||||||
|
var vel: Vector3 = (_tail_world[i] - _tail_prev[i]) * damping
|
||||||
|
_tail_prev[i] = _tail_world[i]
|
||||||
|
var wag := basis_x * sin(time * wag_freq + i * 0.5) * wag_strength
|
||||||
|
_tail_world[i] = _tail_world[i] + vel + gravity * (delta * delta) + wag * delta
|
||||||
|
|
||||||
|
for i in range(1, _tail_world.size()):
|
||||||
|
var dir: Vector3 = _tail_world[i] - _tail_world[i - 1]
|
||||||
|
var len: float = dir.length()
|
||||||
|
var target_len: float = _tail_lengths[i - 1]
|
||||||
|
if len > 0.0001:
|
||||||
|
_tail_world[i] = _tail_world[i - 1] + dir * (target_len / len)
|
||||||
|
else:
|
||||||
|
_tail_world[i] = _tail_world[i - 1] + Vector3(0.0, -target_len, 0.0)
|
||||||
|
|
||||||
|
var body_radius: float = DP.f("tail_body_radius")
|
||||||
|
for i in range(1, _tail_world.size()):
|
||||||
|
var p := _bull.to_local(_tail_world[i])
|
||||||
|
var closest := Vector3(0.0, 0.0, clampf(p.z, -0.3, 0.5))
|
||||||
|
var diff := p - closest
|
||||||
|
var dist := diff.length()
|
||||||
|
if dist < body_radius and dist > 0.001:
|
||||||
|
p = closest + diff.normalized() * body_radius
|
||||||
|
_tail_world[i] = _bull.to_global(p)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_tail() -> void:
|
||||||
|
if _tail_world.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var n := _tail_world.size()
|
||||||
|
var local_pos: Array[Vector3] = []
|
||||||
|
local_pos.resize(n)
|
||||||
|
for i in n:
|
||||||
|
local_pos[i] = _skeleton.to_local(_tail_world[i])
|
||||||
|
|
||||||
|
for i in n:
|
||||||
|
var idx: int = _tail_idx[i]
|
||||||
|
var pos_skel: Vector3 = local_pos[i]
|
||||||
|
|
||||||
|
var point_dir_skel: Vector3
|
||||||
|
if i < n - 1:
|
||||||
|
point_dir_skel = (local_pos[i + 1] - pos_skel).normalized()
|
||||||
|
else:
|
||||||
|
point_dir_skel = (pos_skel - local_pos[i - 1]).normalized()
|
||||||
|
|
||||||
|
var rest_dir: Vector3 = _tail_rest_dir[i]
|
||||||
|
var rest_t: Transform3D = _skeleton.get_bone_global_rest(idx)
|
||||||
|
var new_basis: Basis
|
||||||
|
if rest_dir.length_squared() > 0.0001 and point_dir_skel.length_squared() > 0.0001:
|
||||||
|
new_basis = Basis(Quaternion(rest_dir, point_dir_skel)) * rest_t.basis
|
||||||
|
else:
|
||||||
|
new_basis = rest_t.basis
|
||||||
|
|
||||||
|
_skeleton.set_bone_global_pose_override(idx, Transform3D(new_basis, pos_skel), 1.0, false)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Utility ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _find_skeleton(node: Node) -> Skeleton3D:
|
||||||
|
if node is Skeleton3D:
|
||||||
|
return node as Skeleton3D
|
||||||
|
for child in node.get_children():
|
||||||
|
var r := _find_skeleton(child)
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _find_physical_bones(node: Node) -> PhysicalBoneSimulator3D:
|
||||||
|
for child in node.get_children():
|
||||||
|
if child is PhysicalBoneSimulator3D:
|
||||||
|
return child as PhysicalBoneSimulator3D
|
||||||
|
return null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bvtw8s41wrrx
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
extends Camera3D
|
||||||
|
|
||||||
|
|
||||||
|
@export var spring_arm: Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
var lerp_power: float = DP.f("cam_lerp_power")
|
||||||
|
position = position.lerp(spring_arm.position, delta * lerp_power)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
extends Camera3D
|
||||||
|
|
||||||
|
@export var height: float = 18.0
|
||||||
|
@export var horizontal_distance: float = 18.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
|
||||||
|
|
||||||
|
var _shake_amount: float = 0.0
|
||||||
|
var _shake_decay: float = 0.0
|
||||||
|
var _flash_timer: float = 0.0
|
||||||
|
var _flash_rect: ColorRect = null
|
||||||
|
var _target_size: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_as_top_level(true)
|
||||||
|
projection = PROJECTION_ORTHOGONAL
|
||||||
|
_target_size = ortho_size
|
||||||
|
size = ortho_size
|
||||||
|
_setup_flash()
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_flash() -> void:
|
||||||
|
var canvas := CanvasLayer.new()
|
||||||
|
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:
|
||||||
|
if event.is_action_pressed("zoom_in"):
|
||||||
|
_target_size = clamp(_target_size / 1.2, ortho_size, ortho_size_max)
|
||||||
|
if event.is_action_pressed("zoom_out"):
|
||||||
|
_target_size = clamp(_target_size * 1.2, ortho_size, ortho_size_max)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
size = lerpf(size, _target_size, delta * 10.0)
|
||||||
|
var angle_rad := deg_to_rad(horizontal_angle_deg)
|
||||||
|
var offset := Vector3(
|
||||||
|
sin(angle_rad) * horizontal_distance,
|
||||||
|
height,
|
||||||
|
cos(angle_rad) * horizontal_distance,
|
||||||
|
)
|
||||||
|
var target := get_parent() as Node3D
|
||||||
|
global_position = target.global_position + offset
|
||||||
|
look_at(target.global_position, Vector3.UP)
|
||||||
|
|
||||||
|
if _shake_amount > 0.005:
|
||||||
|
global_position += Vector3(
|
||||||
|
randf_range(-_shake_amount, _shake_amount),
|
||||||
|
randf_range(-_shake_amount * 0.25, _shake_amount * 0.25),
|
||||||
|
randf_range(-_shake_amount, _shake_amount),
|
||||||
|
)
|
||||||
|
_shake_amount = move_toward(_shake_amount, 0.0, _shake_decay * delta)
|
||||||
|
|
||||||
|
if _flash_timer > 0.0:
|
||||||
|
_flash_timer = maxf(0.0, _flash_timer - delta)
|
||||||
|
_flash_rect.color.a = (_flash_timer / _FLASH_DURATION) * 0.6
|
||||||
|
else:
|
||||||
|
_flash_rect.color.a = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func trigger_hit(strength: float) -> void:
|
||||||
|
_shake_amount = strength * 0.5
|
||||||
|
_shake_decay = _shake_amount / 0.4
|
||||||
|
_flash_timer = _FLASH_DURATION
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
@onready var spring_arm: SpringArm3D = $SpringArm3D
|
||||||
|
var _desired_length: float = 7.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
|
_desired_length = DP.f("cam_spring_length")
|
||||||
|
spring_arm.spring_length = _desired_length
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
var sensitivity: float = DP.f("cam_sensitivity")
|
||||||
|
var min_vert: float = deg_to_rad(DP.f("cam_min_vert_angle"))
|
||||||
|
var max_vert: float = deg_to_rad(DP.f("cam_max_vert_angle"))
|
||||||
|
var min_zoom: float = DP.f("cam_min_zoom")
|
||||||
|
var max_zoom: float = DP.f("cam_max_zoom")
|
||||||
|
|
||||||
|
if event is InputEventMouseMotion:
|
||||||
|
rotation.y -= event.relative.x * sensitivity
|
||||||
|
rotation.y = wrapf(rotation.y, 0.0, TAU)
|
||||||
|
rotation.x -= event.relative.y * sensitivity
|
||||||
|
rotation.x = clamp(rotation.x, min_vert, max_vert)
|
||||||
|
|
||||||
|
if event.is_action_pressed("zoom_in"):
|
||||||
|
_desired_length = clamp(_desired_length / 1.2, min_zoom, max_zoom)
|
||||||
|
if event.is_action_pressed("zoom_out"):
|
||||||
|
_desired_length = clamp(_desired_length * 1.2, min_zoom, max_zoom)
|
||||||
|
|
||||||
|
if event.is_action_pressed("toggle_mouse_capture"):
|
||||||
|
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
|
else:
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
spring_arm.spring_length = _desired_length
|
||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
extends Node
|
||||||
|
## Runtime debug panel. Toggle with F1.
|
||||||
|
## Reads DP.get_all() to build controls — adding a param to DP is all that's needed to show it here.
|
||||||
|
|
||||||
|
var _panel: Control
|
||||||
|
var _status_label: Label
|
||||||
|
var _prev_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
var canvas := CanvasLayer.new()
|
||||||
|
canvas.layer = 128
|
||||||
|
add_child(canvas)
|
||||||
|
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
_panel.anchor_left = 1.0
|
||||||
|
_panel.anchor_right = 1.0
|
||||||
|
_panel.anchor_top = 0.0
|
||||||
|
_panel.anchor_bottom = 1.0
|
||||||
|
_panel.offset_left = -390.0
|
||||||
|
_panel.offset_right = 0.0
|
||||||
|
_panel.offset_top = 0.0
|
||||||
|
_panel.offset_bottom = 0.0
|
||||||
|
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
_panel.visible = false
|
||||||
|
|
||||||
|
var bg := StyleBoxFlat.new()
|
||||||
|
bg.bg_color = Color(0.08, 0.08, 0.10, 0.95)
|
||||||
|
_panel.add_theme_stylebox_override("panel", bg)
|
||||||
|
canvas.add_child(_panel)
|
||||||
|
|
||||||
|
var root_vbox := VBoxContainer.new()
|
||||||
|
root_vbox.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
_panel.add_child(root_vbox)
|
||||||
|
|
||||||
|
# ── Toolbar ───────────────────────────────────────────────────────────────
|
||||||
|
var toolbar_margin := MarginContainer.new()
|
||||||
|
toolbar_margin.add_theme_constant_override("margin_left", 8)
|
||||||
|
toolbar_margin.add_theme_constant_override("margin_right", 8)
|
||||||
|
toolbar_margin.add_theme_constant_override("margin_top", 6)
|
||||||
|
toolbar_margin.add_theme_constant_override("margin_bottom", 4)
|
||||||
|
root_vbox.add_child(toolbar_margin)
|
||||||
|
|
||||||
|
var toolbar := HBoxContainer.new()
|
||||||
|
toolbar.add_theme_constant_override("separation", 6)
|
||||||
|
toolbar_margin.add_child(toolbar)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "DEBUG [F1]"
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
toolbar.add_child(title)
|
||||||
|
|
||||||
|
_status_label = Label.new()
|
||||||
|
_status_label.modulate = Color(0.4, 1.0, 0.5)
|
||||||
|
toolbar.add_child(_status_label)
|
||||||
|
|
||||||
|
var save_btn := Button.new()
|
||||||
|
save_btn.text = "Save"
|
||||||
|
save_btn.pressed.connect(_on_save)
|
||||||
|
toolbar.add_child(save_btn)
|
||||||
|
|
||||||
|
var rand_all_btn := Button.new()
|
||||||
|
rand_all_btn.text = "Randomize All"
|
||||||
|
rand_all_btn.pressed.connect(_on_randomize_all)
|
||||||
|
toolbar.add_child(rand_all_btn)
|
||||||
|
|
||||||
|
var reset_btn := Button.new()
|
||||||
|
reset_btn.text = "Reset All"
|
||||||
|
reset_btn.pressed.connect(DP.reset_all)
|
||||||
|
toolbar.add_child(reset_btn)
|
||||||
|
|
||||||
|
root_vbox.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
# ── Scrollable content ────────────────────────────────────────────────────
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
scroll.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
root_vbox.add_child(scroll)
|
||||||
|
|
||||||
|
var content_margin := MarginContainer.new()
|
||||||
|
content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
content_margin.add_theme_constant_override("margin_left", 8)
|
||||||
|
content_margin.add_theme_constant_override("margin_right", 8)
|
||||||
|
content_margin.add_theme_constant_override("margin_top", 4)
|
||||||
|
content_margin.add_theme_constant_override("margin_bottom", 8)
|
||||||
|
scroll.add_child(content_margin)
|
||||||
|
|
||||||
|
var content := VBoxContainer.new()
|
||||||
|
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
content_margin.add_child(content)
|
||||||
|
|
||||||
|
_build_params(content)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_params(content: VBoxContainer) -> void:
|
||||||
|
# Group keys by section preserving registration order
|
||||||
|
var sections: Dictionary = {}
|
||||||
|
for key: String in DP.get_all():
|
||||||
|
var sec: String = DP.get_all()[key]["section"]
|
||||||
|
if not sections.has(sec):
|
||||||
|
sections[sec] = []
|
||||||
|
sections[sec].append(key)
|
||||||
|
|
||||||
|
for section: String in sections:
|
||||||
|
content.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
var header := Label.new()
|
||||||
|
header.text = section.to_upper()
|
||||||
|
header.modulate = Color(0.75, 0.85, 1.0)
|
||||||
|
content.add_child(header)
|
||||||
|
|
||||||
|
for key: String in sections[section]:
|
||||||
|
var meta: Dictionary = DP.get_all()[key]
|
||||||
|
if meta["type"] == TYPE_FLOAT:
|
||||||
|
_add_float_row(content, key, meta)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_float_row(parent: VBoxContainer, key: String, meta: Dictionary) -> void:
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
parent.add_child(row)
|
||||||
|
|
||||||
|
var lbl := Label.new()
|
||||||
|
lbl.text = key.replace("_", " ")
|
||||||
|
lbl.custom_minimum_size.x = 140
|
||||||
|
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
row.add_child(lbl)
|
||||||
|
|
||||||
|
var slider := HSlider.new()
|
||||||
|
slider.min_value = meta["min"]
|
||||||
|
slider.max_value = meta["max"]
|
||||||
|
slider.step = meta["step"]
|
||||||
|
slider.value = meta["value"]
|
||||||
|
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
slider.custom_minimum_size.x = 60
|
||||||
|
row.add_child(slider)
|
||||||
|
|
||||||
|
var spin := SpinBox.new()
|
||||||
|
spin.min_value = meta["min"]
|
||||||
|
spin.max_value = meta["max"]
|
||||||
|
spin.step = meta["step"]
|
||||||
|
spin.value = meta["value"]
|
||||||
|
spin.custom_minimum_size.x = 85
|
||||||
|
row.add_child(spin)
|
||||||
|
|
||||||
|
var rand_btn := Button.new()
|
||||||
|
rand_btn.text = "🎲"
|
||||||
|
rand_btn.tooltip_text = "Randomize"
|
||||||
|
rand_btn.pressed.connect(func() -> void:
|
||||||
|
var v := randf_range(meta["min"], meta["max"])
|
||||||
|
v = snappedf(v, meta["step"])
|
||||||
|
slider.value = v
|
||||||
|
)
|
||||||
|
row.add_child(rand_btn)
|
||||||
|
|
||||||
|
# Slider → SpinBox + DP (use no_signal to avoid echo)
|
||||||
|
slider.value_changed.connect(func(v: float) -> void:
|
||||||
|
spin.set_value_no_signal(v)
|
||||||
|
DP.set_value(key, v)
|
||||||
|
)
|
||||||
|
# SpinBox → Slider + DP
|
||||||
|
spin.value_changed.connect(func(v: float) -> void:
|
||||||
|
slider.set_value_no_signal(v)
|
||||||
|
DP.set_value(key, v)
|
||||||
|
)
|
||||||
|
# External changes (reset, load) → sync both controls
|
||||||
|
DP.any_changed.connect(func(changed_key: String, _val: Variant) -> void:
|
||||||
|
if changed_key != key and changed_key != "__all__":
|
||||||
|
return
|
||||||
|
var v := DP.f(key)
|
||||||
|
slider.set_value_no_signal(v)
|
||||||
|
spin.set_value_no_signal(v)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _on_save() -> void:
|
||||||
|
DP.save()
|
||||||
|
_status_label.text = "Saved!"
|
||||||
|
get_tree().create_timer(1.5).timeout.connect(func() -> void:
|
||||||
|
_status_label.text = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if _panel.visible and Input.is_key_pressed(KEY_CTRL) and event is InputEventMouseMotion:
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not (event is InputEventKey and event.pressed and not event.echo):
|
||||||
|
return
|
||||||
|
if (event as InputEventKey).keycode != KEY_F1:
|
||||||
|
return
|
||||||
|
_panel.visible = not _panel.visible
|
||||||
|
if _panel.visible:
|
||||||
|
_prev_mouse_mode = Input.mouse_mode
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
else:
|
||||||
|
Input.mouse_mode = _prev_mouse_mode
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _on_randomize_all() -> void:
|
||||||
|
for key: String in DP.get_all():
|
||||||
|
var meta: Dictionary = DP.get_all()[key]
|
||||||
|
if meta["type"] == TYPE_FLOAT:
|
||||||
|
var v := snappedf(randf_range(meta["min"], meta["max"]), meta["step"])
|
||||||
|
DP.set_value(key, v)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://via815ow6p5r
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
extends Node
|
||||||
|
## Central registry for all runtime-tunable parameters.
|
||||||
|
## Add a new param with one _reg_f() call in _register_all() — the menu picks it up automatically.
|
||||||
|
## Access: DP.f("key") • Persist: DP.save() / DP.load_saved() • Reset: DP.reset_all()
|
||||||
|
|
||||||
|
signal any_changed(key: String, value: Variant)
|
||||||
|
|
||||||
|
const SAVE_PATH := "user://debug_params.cfg"
|
||||||
|
|
||||||
|
var _params: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_register_all()
|
||||||
|
load_saved()
|
||||||
|
|
||||||
|
|
||||||
|
func _register_all() -> void:
|
||||||
|
# ── Movement ──────────────────────────────────────────────────────────────
|
||||||
|
_reg_f("Movement", "thrust_impulse", 18.0, 1.0, 200.0)
|
||||||
|
_reg_f("Movement", "thrust_vertical", 2.0, 0.0, 20.0)
|
||||||
|
_reg_f("Movement", "thrust_min_angle", 15.0, 0.0, 90.0, 1.0)
|
||||||
|
_reg_f("Movement", "thrust_max_angle", 90.0, 0.0, 180.0, 1.0)
|
||||||
|
_reg_f("Movement", "thrust_charge_time", 0.5, 0.05, 2.0, 0.05)
|
||||||
|
_reg_f("Movement", "max_speed", 50.0, 2.0, 1000.0)
|
||||||
|
_reg_f("Movement", "roll_damping", 0.15, 0.01, 0.99, 0.01)
|
||||||
|
_reg_f("Movement", "jump_velocity", 4.5, 1.0, 200.0)
|
||||||
|
_reg_f("Movement", "visual_turn_speed", 12.0, 1.0, 200.0)
|
||||||
|
_reg_f("Movement", "bounce_restitution", 0.55, 0.0, 2.0, 0.05)
|
||||||
|
_reg_f("Movement", "bounce_min_impact", 1.0, 0.0, 20.0)
|
||||||
|
_reg_f("Movement", "wall_bounce_restitution", 0.6, 0.0, 1.5, 0.05)
|
||||||
|
_reg_f("Movement", "wall_min_bounce_speed", 6.0, 0.0, 30.0)
|
||||||
|
_reg_f("Movement", "kick_impulse", 8.0, 1.0, 50.0)
|
||||||
|
_reg_f("Movement", "kick_interval", 0.25, 0.05, 1.0, 0.01)
|
||||||
|
_reg_f("Movement", "kick_vertical", 0.0, 0.0, 15.0)
|
||||||
|
_reg_f("Movement", "kick_force_var", 0.2, 0.0, 1.0, 0.01)
|
||||||
|
_reg_f("Movement", "kick_spread", 10.0, 0.0, 45.0, 1.0)
|
||||||
|
_reg_f("Movement", "charge_kick_mult", 2.0, 1.0, 5.0)
|
||||||
|
# ── Dust speed thresholds ─────────────────────────────────────────────────
|
||||||
|
_reg_f("Dust", "dust_walk_spd", 1.5, 0.1, 10.0)
|
||||||
|
_reg_f("Dust", "dust_charge_spd", 7.0, 1.0, 20.0)
|
||||||
|
# ── Dust ──────────────────────────────────────────────────────────────────
|
||||||
|
_reg_f("Dust", "dust_lifetime", 0.9, 0.1, 4.0)
|
||||||
|
_reg_f("Dust", "dust_explosiveness", 0.35, 0.0, 1.0)
|
||||||
|
_reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0)
|
||||||
|
_reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0)
|
||||||
|
_reg_f("Dust", "walk_scale_min", 0.35, 0.05, 3.0)
|
||||||
|
_reg_f("Dust", "walk_scale_max", 0.65, 0.05, 3.0)
|
||||||
|
_reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0)
|
||||||
|
_reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0)
|
||||||
|
_reg_f("Dust", "charge_scale_min", 1.0, 0.05, 6.0)
|
||||||
|
_reg_f("Dust", "charge_scale_max", 2.2, 0.05, 6.0)
|
||||||
|
_reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0)
|
||||||
|
_reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0)
|
||||||
|
# ── Tail ──────────────────────────────────────────────────────────────────
|
||||||
|
_reg_f("Tail", "tail_gravity", 9.8, 0.0, 30.0)
|
||||||
|
_reg_f("Tail", "tail_damping", 0.08, 0.0, 0.5, 0.01)
|
||||||
|
_reg_f("Tail", "tail_body_radius", 0.35, 0.05, 2.0, 0.05)
|
||||||
|
_reg_f("Tail", "tail_wag_freq", 4.0, 0.5, 15.0)
|
||||||
|
_reg_f("Tail", "tail_wag_idle", 0.3, 0.0, 2.0, 0.05)
|
||||||
|
# ── Charge (both mouse buttons) ──────────────────────────────────────────
|
||||||
|
_reg_f("Charge", "bull_charge_accel", 80.0, 1.0, 400.0)
|
||||||
|
_reg_f("Charge", "bull_charge_max_speed", 76.0, 5.0, 200.0)
|
||||||
|
_reg_f("Charge", "charge_ramp_time", 0.75, 0.1, 5.0, 0.05)
|
||||||
|
_reg_f("Charge", "charge_head_pitch", 0.3, 0.0, 1.2, 0.01)
|
||||||
|
_reg_f("Charge", "charge_head_speed", 5.0, 0.5, 20.0)
|
||||||
|
# ── Body ──────────────────────────────────────────────────────────────────
|
||||||
|
_reg_f("Body", "body_tilt_fwd", 0.015, 0.0, 0.1, 0.001)
|
||||||
|
_reg_f("Body", "body_tilt_side", 0.01, 0.0, 0.1, 0.001)
|
||||||
|
_reg_f("Body", "aerial_pitch_scale", 0.6, 0.0, 2.0)
|
||||||
|
_reg_f("Body", "aerial_pitch_speed", 5.0, 0.5, 20.0)
|
||||||
|
# ── Legs ──────────────────────────────────────────────────────────────────
|
||||||
|
_reg_f("Legs", "leg_step_threshold", 0.15, 0.01, 1.0, 0.01)
|
||||||
|
_reg_f("Legs", "leg_step_duration", 0.12, 0.02, 0.5, 0.01)
|
||||||
|
_reg_f("Legs", "leg_step_height", 0.15, 0.0, 0.5)
|
||||||
|
_reg_f("Legs", "leg_step_overshoot", 0.3, 0.0, 1.0)
|
||||||
|
_reg_f("Legs", "gait_freq", 1.5, 0.1, 5.0)
|
||||||
|
_reg_f("Legs", "launch_hold_time", 0.15, 0.0, 1.0, 0.01)
|
||||||
|
_reg_f("Legs", "ragdoll_wobble_amp", 0.15, 0.0, 1.0)
|
||||||
|
_reg_f("Legs", "tuck_apex_threshold", 0.5, -5.0, 5.0)
|
||||||
|
_reg_f("Legs", "jump_tuck_speed", 8.0, 1.0, 30.0)
|
||||||
|
_reg_f("Legs", "jump_tuck_up", 0.2, 0.0, 1.0)
|
||||||
|
_reg_f("Legs", "jump_tuck_fwd", -0.1, -0.5, 0.5)
|
||||||
|
_reg_f("Legs", "jump_land_threshold", 1.5, 0.5, 5.0)
|
||||||
|
# ── Camera ────────────────────────────────────────────────────────────────
|
||||||
|
_reg_f("Camera", "cam_lerp_power", 6.0, 0.5, 30.0)
|
||||||
|
_reg_f("Camera", "cam_sensitivity", 0.005, 0.001, 0.05, 0.001)
|
||||||
|
_reg_f("Camera", "cam_min_zoom", 3.5, 0.5, 20.0)
|
||||||
|
_reg_f("Camera", "cam_max_zoom", 40.0, 2.0, 80.0)
|
||||||
|
_reg_f("Camera", "cam_spring_length", 7.0, 1.0, 30.0)
|
||||||
|
_reg_f("Camera", "cam_min_vert_angle", -60.0, -90.0, 0.0, 1.0)
|
||||||
|
_reg_f("Camera", "cam_max_vert_angle", 45.0, 0.0, 90.0, 1.0)
|
||||||
|
# ── Matador ───────────────────────────────────────────────────────────────
|
||||||
|
_reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0)
|
||||||
|
_reg_f("Matador", "mat_walk_speed", 2.5, 0.5, 10.0)
|
||||||
|
_reg_f("Matador", "mat_wander_radius", 22.0, 5.0, 50.0)
|
||||||
|
_reg_f("Matador", "mat_idle_min", 0.5, 0.0, 5.0, 0.1)
|
||||||
|
_reg_f("Matador", "mat_idle_max", 2.5, 0.5, 10.0, 0.1)
|
||||||
|
_reg_f("Matador", "mat_hit_threshold", 4.0, 1.0, 30.0)
|
||||||
|
_reg_f("Matador", "mat_ragdoll_impulse", 6.0, 0.5, 20.0)
|
||||||
|
_reg_f("Matador", "mat_flee_range", 3.5, 2.0, 40.0)
|
||||||
|
_reg_f("Matador", "mat_flee_speed", 2.5, 0.5, 12.0)
|
||||||
|
_reg_f("Matador", "mat_charge_speed", 10.0, 1.0, 30.0)
|
||||||
|
_reg_f("Matador", "mat_brace_range", 5.0, 2.0, 20.0)
|
||||||
|
_reg_f("Matador", "mat_brace_duration", 0.7, 0.05, 1.5, 0.05)
|
||||||
|
_reg_f("Matador", "mat_commit_dist", 2.5, 0.5, 6.0, 0.1)
|
||||||
|
_reg_f("Matador", "mat_step_speed", 3.5, 2.0, 20.0)
|
||||||
|
_reg_f("Matador", "mat_step_duration", 0.35, 0.1, 1.0, 0.05)
|
||||||
|
_reg_f("Matador", "mat_dodge_cooldown", 5.0, 0.5, 12.0, 0.25)
|
||||||
|
_reg_f("Matador", "mat_dodge_chance", 0.25, 0.0, 1.0, 0.05)
|
||||||
|
# ── Debug ─────────────────────────────────────────────────────────────────
|
||||||
|
_reg_b("Debug", "show_collisions", false)
|
||||||
|
|
||||||
|
|
||||||
|
func _reg_f(section: String, key: String, default: float,
|
||||||
|
min_val: float, max_val: float, step: float = 0.05) -> void:
|
||||||
|
_params[key] = {
|
||||||
|
"value": default,
|
||||||
|
"default": default,
|
||||||
|
"section": section,
|
||||||
|
"type": TYPE_FLOAT,
|
||||||
|
"min": min_val,
|
||||||
|
"max": max_val,
|
||||||
|
"step": step,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _reg_b(section: String, key: String, default: bool) -> void:
|
||||||
|
_params[key] = {
|
||||||
|
"value": default,
|
||||||
|
"default": default,
|
||||||
|
"section": section,
|
||||||
|
"type": TYPE_BOOL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Read a float param. Panics on unknown key — intentional: typos should be loud.
|
||||||
|
func f(key: String) -> float:
|
||||||
|
return _params[key]["value"] as float
|
||||||
|
|
||||||
|
|
||||||
|
## Read a bool param.
|
||||||
|
func b(key: String) -> bool:
|
||||||
|
return _params[key]["value"] as bool
|
||||||
|
|
||||||
|
|
||||||
|
func set_value(key: String, value: Variant) -> void:
|
||||||
|
if not _params.has(key):
|
||||||
|
return
|
||||||
|
_params[key]["value"] = value
|
||||||
|
any_changed.emit(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the full metadata dict — used by DebugMenu to build UI.
|
||||||
|
func get_all() -> Dictionary:
|
||||||
|
return _params
|
||||||
|
|
||||||
|
|
||||||
|
func save() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
for key: String in _params:
|
||||||
|
var p: Dictionary = _params[key]
|
||||||
|
cfg.set_value(p["section"], key, p["value"])
|
||||||
|
cfg.save(SAVE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
func load_saved() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(SAVE_PATH) != OK:
|
||||||
|
return
|
||||||
|
for section: String in cfg.get_sections():
|
||||||
|
for key: String in cfg.get_section_keys(section):
|
||||||
|
if not _params.has(key):
|
||||||
|
continue
|
||||||
|
var p: Dictionary = _params[key]
|
||||||
|
var val: Variant = cfg.get_value(section, key)
|
||||||
|
if p["type"] == TYPE_FLOAT:
|
||||||
|
val = clampf(val as float, p["min"] as float, p["max"] as float)
|
||||||
|
_params[key]["value"] = val
|
||||||
|
|
||||||
|
|
||||||
|
func reset_all() -> void:
|
||||||
|
for key: String in _params:
|
||||||
|
_params[key]["value"] = _params[key]["default"]
|
||||||
|
any_changed.emit("__all__", null)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b3ix5yi7b3ynh
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SERVER_USER="richard"
|
||||||
|
SERVER_HOST="192.168.1.168"
|
||||||
|
SERVER_PATH="/opt/bulloseum/data"
|
||||||
|
|
||||||
|
BUILD_DIR="build/web"
|
||||||
|
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
godot --headless --export-release "Web" "$BUILD_DIR/index.html"
|
||||||
|
|
||||||
|
gzip -kf "$BUILD_DIR/index.wasm"
|
||||||
|
|
||||||
|
scp "$BUILD_DIR/index.html" \
|
||||||
|
"$BUILD_DIR/index.js" \
|
||||||
|
"$BUILD_DIR/index.wasm.gz" \
|
||||||
|
"$BUILD_DIR/index.pck" \
|
||||||
|
"$BUILD_DIR/index.audio.worklet.js" \
|
||||||
|
"$BUILD_DIR/index.audio.position.worklet.js" \
|
||||||
|
"$BUILD_DIR/index.icon.png" \
|
||||||
|
"$BUILD_DIR/index.apple-touch-icon.png" \
|
||||||
|
"$BUILD_DIR/index.png" \
|
||||||
|
"$SERVER_USER@$SERVER_HOST:$SERVER_PATH/"
|
||||||
|
|
||||||
|
echo "deployed to $SERVER_HOST:$SERVER_PATH"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[preset.0]
|
||||||
|
|
||||||
|
name="Web"
|
||||||
|
platform="Web"
|
||||||
|
runnable=true
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="build/web/index.html"
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
|
||||||
|
[preset.0.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
variant/extensions_support=false
|
||||||
|
variant/thread_support=true
|
||||||
|
vram_texture_compression/for_desktop=true
|
||||||
|
vram_texture_compression/for_mobile=false
|
||||||
|
html/export_icon=true
|
||||||
|
html/custom_html_shell=""
|
||||||
|
html/head_include=""
|
||||||
|
html/canvas_resize_policy=2
|
||||||
|
html/focus_canvas_on_start=true
|
||||||
|
html/experimental_virtual_keyboard=false
|
||||||
|
progressive_web_app/enabled=false
|
||||||
|
progressive_web_app/offline_page=""
|
||||||
|
progressive_web_app/display=1
|
||||||
|
progressive_web_app/orientation=0
|
||||||
|
progressive_web_app/icon_144x144=""
|
||||||
|
progressive_web_app/icon_180x180=""
|
||||||
|
progressive_web_app/icon_512x512=""
|
||||||
|
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
hello
|
|
||||||
xd
|
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
const _GOLD := Color(1.00, 0.78, 0.22)
|
||||||
|
const _CREAM := Color(0.88, 0.83, 0.72)
|
||||||
|
const _MUTED := Color(0.65, 0.60, 0.48)
|
||||||
|
const _BG := Color(0.06, 0.04, 0.02, 0.78)
|
||||||
|
const _BADGE := Color(0.16, 0.10, 0.03, 0.90)
|
||||||
|
const _BORDER := Color(0.65, 0.48, 0.15, 0.55)
|
||||||
|
|
||||||
|
const ROUND_DURATION := 60.0
|
||||||
|
|
||||||
|
var _controls_visible: bool = true
|
||||||
|
var _timer_remaining: float = ROUND_DURATION
|
||||||
|
var _score: int = 0
|
||||||
|
var _round_active: bool = true
|
||||||
|
|
||||||
|
var _controls_panel: PanelContainer
|
||||||
|
var _timer_label: Label
|
||||||
|
var _score_label: Label
|
||||||
|
var _toggle_btn: Button
|
||||||
|
var _game_over_panel: Control
|
||||||
|
var _final_score_label: Label
|
||||||
|
var _spawner: Node
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = 11
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
_build()
|
||||||
|
_find_spawner.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _find_spawner() -> void:
|
||||||
|
var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
|
||||||
|
if not spawners.is_empty():
|
||||||
|
_spawner = spawners[0]
|
||||||
|
_spawner.matador_killed.connect(_on_matador_killed)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not _round_active:
|
||||||
|
return
|
||||||
|
_timer_remaining = maxf(0.0, _timer_remaining - delta)
|
||||||
|
_update_timer_display()
|
||||||
|
if _timer_remaining <= 0.0:
|
||||||
|
_end_round()
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
|
if event.physical_keycode == KEY_H:
|
||||||
|
_toggle_controls()
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_controls() -> void:
|
||||||
|
_controls_visible = not _controls_visible
|
||||||
|
_controls_panel.visible = _controls_visible
|
||||||
|
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_matador_killed() -> void:
|
||||||
|
if _round_active:
|
||||||
|
_score += 1
|
||||||
|
_update_score_display()
|
||||||
|
|
||||||
|
|
||||||
|
func _end_round() -> void:
|
||||||
|
_round_active = false
|
||||||
|
_final_score_label.text = "You killed %d matadors!" % _score
|
||||||
|
_game_over_panel.visible = true
|
||||||
|
get_tree().paused = true
|
||||||
|
|
||||||
|
|
||||||
|
func _on_continue_pressed() -> void:
|
||||||
|
_on_reset_pressed()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_reset_pressed() -> void:
|
||||||
|
if get_tree().paused:
|
||||||
|
get_tree().paused = false
|
||||||
|
_game_over_panel.visible = false
|
||||||
|
_timer_remaining = ROUND_DURATION
|
||||||
|
_score = 0
|
||||||
|
_round_active = true
|
||||||
|
_update_timer_display()
|
||||||
|
_update_score_display()
|
||||||
|
if _spawner:
|
||||||
|
_spawner.reset_spawn()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_timer_display() -> void:
|
||||||
|
var s := int(_timer_remaining)
|
||||||
|
_timer_label.text = "%d:%02d" % [s / 60, s % 60]
|
||||||
|
|
||||||
|
|
||||||
|
func _update_score_display() -> void:
|
||||||
|
_score_label.text = "Kills: %d" % _score
|
||||||
|
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
_build_controls_panel()
|
||||||
|
_build_score_panel()
|
||||||
|
_build_game_over_overlay()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_controls_panel() -> void:
|
||||||
|
_controls_panel = PanelContainer.new()
|
||||||
|
|
||||||
|
var bg := StyleBoxFlat.new()
|
||||||
|
bg.bg_color = _BG
|
||||||
|
bg.corner_radius_top_left = 7
|
||||||
|
bg.corner_radius_top_right = 7
|
||||||
|
bg.corner_radius_bottom_left = 7
|
||||||
|
bg.corner_radius_bottom_right = 7
|
||||||
|
bg.border_width_left = 1
|
||||||
|
bg.border_width_right = 1
|
||||||
|
bg.border_width_top = 1
|
||||||
|
bg.border_width_bottom = 1
|
||||||
|
bg.border_color = _BORDER
|
||||||
|
bg.content_margin_left = 14.0
|
||||||
|
bg.content_margin_right = 14.0
|
||||||
|
bg.content_margin_top = 10.0
|
||||||
|
bg.content_margin_bottom = 10.0
|
||||||
|
_controls_panel.add_theme_stylebox_override("panel", bg)
|
||||||
|
|
||||||
|
_controls_panel.anchor_left = 0.0
|
||||||
|
_controls_panel.anchor_top = 1.0
|
||||||
|
_controls_panel.anchor_right = 0.0
|
||||||
|
_controls_panel.anchor_bottom = 1.0
|
||||||
|
_controls_panel.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||||
|
_controls_panel.offset_left = 20.0
|
||||||
|
_controls_panel.offset_bottom = -20.0
|
||||||
|
add_child(_controls_panel)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.add_theme_constant_override("separation", 5)
|
||||||
|
_controls_panel.add_child(vbox)
|
||||||
|
|
||||||
|
_row(vbox, "WASD / Arrows", "Move (not intended)", _MUTED)
|
||||||
|
_row(vbox, "LMB (hold)", "Right horn", _CREAM)
|
||||||
|
_row(vbox, "RMB (hold)", "Left horn", _CREAM)
|
||||||
|
_row(vbox, "LMB + RMB", "Bull charge", _CREAM)
|
||||||
|
_row(vbox, "Scroll", "Zoom", _CREAM)
|
||||||
|
_row(vbox, "R", "Respawn matadors (cooldown)", _MUTED)
|
||||||
|
_row(vbox, "H", "Toggle controls", _MUTED)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_score_panel() -> void:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
|
||||||
|
var bg := StyleBoxFlat.new()
|
||||||
|
bg.bg_color = _BG
|
||||||
|
bg.corner_radius_top_left = 7
|
||||||
|
bg.corner_radius_top_right = 7
|
||||||
|
bg.corner_radius_bottom_left = 7
|
||||||
|
bg.corner_radius_bottom_right = 7
|
||||||
|
bg.border_width_left = 1
|
||||||
|
bg.border_width_right = 1
|
||||||
|
bg.border_width_top = 1
|
||||||
|
bg.border_width_bottom = 1
|
||||||
|
bg.border_color = _BORDER
|
||||||
|
bg.content_margin_left = 16.0
|
||||||
|
bg.content_margin_right = 16.0
|
||||||
|
bg.content_margin_top = 12.0
|
||||||
|
bg.content_margin_bottom = 12.0
|
||||||
|
panel.add_theme_stylebox_override("panel", bg)
|
||||||
|
|
||||||
|
panel.anchor_left = 1.0
|
||||||
|
panel.anchor_top = 0.0
|
||||||
|
panel.anchor_right = 1.0
|
||||||
|
panel.anchor_bottom = 0.0
|
||||||
|
panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||||
|
panel.grow_vertical = Control.GROW_DIRECTION_END
|
||||||
|
panel.offset_right = -20.0
|
||||||
|
panel.offset_top = 20.0
|
||||||
|
add_child(panel)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.add_theme_constant_override("separation", 8)
|
||||||
|
panel.add_child(vbox)
|
||||||
|
|
||||||
|
_timer_label = Label.new()
|
||||||
|
_timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_timer_label.add_theme_color_override("font_color", _GOLD)
|
||||||
|
_timer_label.add_theme_font_size_override("font_size", 32)
|
||||||
|
vbox.add_child(_timer_label)
|
||||||
|
|
||||||
|
_score_label = Label.new()
|
||||||
|
_score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_score_label.add_theme_color_override("font_color", _CREAM)
|
||||||
|
_score_label.add_theme_font_size_override("font_size", 16)
|
||||||
|
vbox.add_child(_score_label)
|
||||||
|
|
||||||
|
var sep := HSeparator.new()
|
||||||
|
var sep_style := StyleBoxFlat.new()
|
||||||
|
sep_style.bg_color = Color(0.65, 0.48, 0.15, 0.30)
|
||||||
|
sep_style.content_margin_top = 2.0
|
||||||
|
sep_style.content_margin_bottom = 2.0
|
||||||
|
sep.add_theme_stylebox_override("separator", sep_style)
|
||||||
|
vbox.add_child(sep)
|
||||||
|
|
||||||
|
var hbox := HBoxContainer.new()
|
||||||
|
hbox.add_theme_constant_override("separation", 8)
|
||||||
|
vbox.add_child(hbox)
|
||||||
|
|
||||||
|
_toggle_btn = _make_button("Hide controls")
|
||||||
|
_toggle_btn.pressed.connect(_toggle_controls)
|
||||||
|
hbox.add_child(_toggle_btn)
|
||||||
|
|
||||||
|
var reset_btn := _make_button("Reset")
|
||||||
|
reset_btn.pressed.connect(_on_reset_pressed)
|
||||||
|
hbox.add_child(reset_btn)
|
||||||
|
|
||||||
|
_update_timer_display()
|
||||||
|
_update_score_display()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_game_over_overlay() -> void:
|
||||||
|
_game_over_panel = Control.new()
|
||||||
|
_game_over_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_game_over_panel.visible = false
|
||||||
|
_game_over_panel.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
add_child(_game_over_panel)
|
||||||
|
|
||||||
|
var backdrop := ColorRect.new()
|
||||||
|
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
backdrop.color = Color(0.0, 0.0, 0.0, 0.65)
|
||||||
|
_game_over_panel.add_child(backdrop)
|
||||||
|
|
||||||
|
var card := PanelContainer.new()
|
||||||
|
var card_bg := StyleBoxFlat.new()
|
||||||
|
card_bg.bg_color = Color(0.08, 0.05, 0.02, 0.95)
|
||||||
|
card_bg.corner_radius_top_left = 12
|
||||||
|
card_bg.corner_radius_top_right = 12
|
||||||
|
card_bg.corner_radius_bottom_left = 12
|
||||||
|
card_bg.corner_radius_bottom_right = 12
|
||||||
|
card_bg.border_width_left = 2
|
||||||
|
card_bg.border_width_right = 2
|
||||||
|
card_bg.border_width_top = 2
|
||||||
|
card_bg.border_width_bottom = 2
|
||||||
|
card_bg.border_color = _GOLD
|
||||||
|
card_bg.content_margin_left = 48.0
|
||||||
|
card_bg.content_margin_right = 48.0
|
||||||
|
card_bg.content_margin_top = 36.0
|
||||||
|
card_bg.content_margin_bottom = 36.0
|
||||||
|
card.add_theme_stylebox_override("panel", card_bg)
|
||||||
|
card.anchor_left = 0.5
|
||||||
|
card.anchor_top = 0.5
|
||||||
|
card.anchor_right = 0.5
|
||||||
|
card.anchor_bottom = 0.5
|
||||||
|
card.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||||
|
card.grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||||
|
_game_over_panel.add_child(card)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.add_theme_constant_override("separation", 18)
|
||||||
|
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
card.add_child(vbox)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "TIME'S UP!"
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
title.add_theme_color_override("font_color", _GOLD)
|
||||||
|
title.add_theme_font_size_override("font_size", 48)
|
||||||
|
vbox.add_child(title)
|
||||||
|
|
||||||
|
_final_score_label = Label.new()
|
||||||
|
_final_score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_final_score_label.add_theme_color_override("font_color", _CREAM)
|
||||||
|
_final_score_label.add_theme_font_size_override("font_size", 24)
|
||||||
|
vbox.add_child(_final_score_label)
|
||||||
|
|
||||||
|
var continue_btn := _make_button("Continue")
|
||||||
|
continue_btn.custom_minimum_size = Vector2(160.0, 44.0)
|
||||||
|
continue_btn.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
continue_btn.pressed.connect(_on_continue_pressed)
|
||||||
|
vbox.add_child(continue_btn)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_button(label_text: String) -> Button:
|
||||||
|
var btn := Button.new()
|
||||||
|
btn.text = label_text
|
||||||
|
btn.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
|
||||||
|
var normal := StyleBoxFlat.new()
|
||||||
|
normal.bg_color = Color(0.18, 0.12, 0.04, 0.90)
|
||||||
|
normal.corner_radius_top_left = 5
|
||||||
|
normal.corner_radius_top_right = 5
|
||||||
|
normal.corner_radius_bottom_left = 5
|
||||||
|
normal.corner_radius_bottom_right = 5
|
||||||
|
normal.border_width_left = 1
|
||||||
|
normal.border_width_right = 1
|
||||||
|
normal.border_width_top = 1
|
||||||
|
normal.border_width_bottom = 1
|
||||||
|
normal.border_color = _BORDER
|
||||||
|
normal.content_margin_left = 12.0
|
||||||
|
normal.content_margin_right = 12.0
|
||||||
|
normal.content_margin_top = 6.0
|
||||||
|
normal.content_margin_bottom = 6.0
|
||||||
|
|
||||||
|
var hover := StyleBoxFlat.new()
|
||||||
|
hover.bg_color = Color(0.30, 0.20, 0.06, 0.95)
|
||||||
|
hover.corner_radius_top_left = 5
|
||||||
|
hover.corner_radius_top_right = 5
|
||||||
|
hover.corner_radius_bottom_left = 5
|
||||||
|
hover.corner_radius_bottom_right = 5
|
||||||
|
hover.border_width_left = 1
|
||||||
|
hover.border_width_right = 1
|
||||||
|
hover.border_width_top = 1
|
||||||
|
hover.border_width_bottom = 1
|
||||||
|
hover.border_color = _GOLD
|
||||||
|
hover.content_margin_left = 12.0
|
||||||
|
hover.content_margin_right = 12.0
|
||||||
|
hover.content_margin_top = 6.0
|
||||||
|
hover.content_margin_bottom = 6.0
|
||||||
|
|
||||||
|
btn.add_theme_stylebox_override("normal", normal)
|
||||||
|
btn.add_theme_stylebox_override("hover", hover)
|
||||||
|
btn.add_theme_stylebox_override("pressed", hover)
|
||||||
|
btn.add_theme_color_override("font_color", _GOLD)
|
||||||
|
btn.add_theme_color_override("font_hover_color", _GOLD)
|
||||||
|
btn.add_theme_font_size_override("font_size", 12)
|
||||||
|
|
||||||
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
func _row(parent: VBoxContainer, key: String, desc: String, desc_color: Color) -> void:
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 10)
|
||||||
|
parent.add_child(row)
|
||||||
|
|
||||||
|
var badge := PanelContainer.new()
|
||||||
|
var bs := StyleBoxFlat.new()
|
||||||
|
bs.bg_color = _BADGE
|
||||||
|
bs.corner_radius_top_left = 3
|
||||||
|
bs.corner_radius_top_right = 3
|
||||||
|
bs.corner_radius_bottom_left = 3
|
||||||
|
bs.corner_radius_bottom_right = 3
|
||||||
|
bs.content_margin_left = 6.0
|
||||||
|
bs.content_margin_right = 6.0
|
||||||
|
bs.content_margin_top = 2.0
|
||||||
|
bs.content_margin_bottom = 2.0
|
||||||
|
badge.add_theme_stylebox_override("panel", bs)
|
||||||
|
badge.custom_minimum_size = Vector2(100.0, 0.0)
|
||||||
|
row.add_child(badge)
|
||||||
|
|
||||||
|
var key_lbl := Label.new()
|
||||||
|
key_lbl.text = key
|
||||||
|
key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
key_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
key_lbl.add_theme_color_override("font_color", _GOLD)
|
||||||
|
key_lbl.add_theme_font_size_override("font_size", 12)
|
||||||
|
badge.add_child(key_lbl)
|
||||||
|
|
||||||
|
var desc_lbl := Label.new()
|
||||||
|
desc_lbl.text = desc
|
||||||
|
desc_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
desc_lbl.add_theme_color_override("font_color", desc_color)
|
||||||
|
desc_lbl.add_theme_font_size_override("font_size", 12)
|
||||||
|
row.add_child(desc_lbl)
|
||||||
|
Before Width: | Height: | Size: 995 B After Width: | Height: | Size: 995 B |
+474
@@ -0,0 +1,474 @@
|
|||||||
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
# WANDER → bull outside flee range, roaming freely
|
||||||
|
# FLEE → bull within range, matador backs away
|
||||||
|
# BRACE → charge detected: plant feet, face the bull (cite phase)
|
||||||
|
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
|
||||||
|
# RAGDOLL → hit by bull at speed
|
||||||
|
enum State { WANDER, FLEE, BRACE, SIDESTEP, RAGDOLL }
|
||||||
|
|
||||||
|
signal killed
|
||||||
|
|
||||||
|
var _state: State = State.WANDER
|
||||||
|
var _skeleton: Skeleton3D = null
|
||||||
|
var _sim: PhysicalBoneSimulator3D = null
|
||||||
|
var _anim_player: AnimationPlayer = null
|
||||||
|
var _wander_target: Vector3 = Vector3.ZERO
|
||||||
|
var _idle_timer: float = 0.0
|
||||||
|
var _bull: CharacterBody3D = null
|
||||||
|
var _step_dir: Vector3 = Vector3.ZERO
|
||||||
|
var _brace_timer: float = 0.0
|
||||||
|
var _step_timer: float = 0.0
|
||||||
|
var _dodge_cd: float = 0.0
|
||||||
|
var _will_dodge: bool = true
|
||||||
|
var _ole_player: AudioStreamPlayer = null
|
||||||
|
var _death_player: AudioStreamPlayer = null
|
||||||
|
var _blood_burst: CPUParticles3D = null
|
||||||
|
|
||||||
|
@onready var _mesh: Node3D = $matador_v02
|
||||||
|
@onready var _hit_area: Area3D = $HitArea
|
||||||
|
@onready var _body_col: CollisionShape3D = $CollisionShape3D
|
||||||
|
|
||||||
|
const _WALK_ANIM: StringName = &"Armature|walk"
|
||||||
|
const _IDLE_ANIM: StringName = &"Armature|idle"
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group(&"matador")
|
||||||
|
_skeleton = _find_skeleton(_mesh)
|
||||||
|
_anim_player = _find_anim_player(_mesh)
|
||||||
|
if _skeleton:
|
||||||
|
_sim = _setup_physical_bones()
|
||||||
|
if _anim_player:
|
||||||
|
_ensure_loop(_WALK_ANIM)
|
||||||
|
if _anim_player.has_animation(_IDLE_ANIM):
|
||||||
|
_anim_player.play(_IDLE_ANIM)
|
||||||
|
else:
|
||||||
|
push_warning("Matador: idle animation not found. Available: %s" %
|
||||||
|
str(_anim_player.get_animation_list()))
|
||||||
|
_hit_area.body_entered.connect(_on_body_entered)
|
||||||
|
_pick_wander_target()
|
||||||
|
_setup_blood_burst()
|
||||||
|
if ResourceLoader.exists("res://sounds/ole.ogg"):
|
||||||
|
_ole_player = AudioStreamPlayer.new()
|
||||||
|
_ole_player.stream = load("res://sounds/ole.ogg")
|
||||||
|
_ole_player.volume_db = 0.0
|
||||||
|
add_child(_ole_player)
|
||||||
|
if ResourceLoader.exists("res://sounds/matador_death.ogg"):
|
||||||
|
_death_player = AudioStreamPlayer.new()
|
||||||
|
_death_player.stream = load("res://sounds/matador_death.ogg")
|
||||||
|
_death_player.volume_db = 2.0
|
||||||
|
add_child(_death_player)
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if _bull == null:
|
||||||
|
var players := get_tree().get_nodes_in_group(&"player")
|
||||||
|
if not players.is_empty():
|
||||||
|
_bull = players[0] as CharacterBody3D
|
||||||
|
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity += get_gravity() * delta
|
||||||
|
|
||||||
|
_dodge_cd = maxf(0.0, _dodge_cd - delta)
|
||||||
|
|
||||||
|
if _state != State.RAGDOLL and _bull != null:
|
||||||
|
_update_ai_state()
|
||||||
|
|
||||||
|
match _state:
|
||||||
|
State.WANDER: _tick_wander(delta)
|
||||||
|
State.FLEE: _tick_flee(delta)
|
||||||
|
State.BRACE: _tick_brace(delta)
|
||||||
|
State.SIDESTEP: _tick_sidestep(delta)
|
||||||
|
State.RAGDOLL: _tick_ragdoll(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_ai_state() -> void:
|
||||||
|
var dist := global_position.distance_to(_bull.global_position)
|
||||||
|
match _state:
|
||||||
|
State.WANDER:
|
||||||
|
if dist < DP.f("mat_flee_range"):
|
||||||
|
_state = State.FLEE
|
||||||
|
State.FLEE:
|
||||||
|
if _dodge_cd <= 0.0 and (dist < DP.f("mat_commit_dist") or _is_charge_incoming()):
|
||||||
|
_start_brace()
|
||||||
|
elif dist > DP.f("mat_flee_range") * 1.3:
|
||||||
|
_state = State.WANDER
|
||||||
|
_pick_wander_target()
|
||||||
|
State.BRACE:
|
||||||
|
pass # brace timer drives transition
|
||||||
|
State.SIDESTEP:
|
||||||
|
pass # step timer drives transition
|
||||||
|
|
||||||
|
|
||||||
|
# Bull is moving fast and aimed within ~30° of the matador
|
||||||
|
func _is_charge_incoming() -> bool:
|
||||||
|
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||||
|
if bull_flat.length() < DP.f("mat_charge_speed"):
|
||||||
|
return false
|
||||||
|
var to_me := (global_position - _bull.global_position)
|
||||||
|
to_me.y = 0.0
|
||||||
|
if to_me.length() > DP.f("mat_brace_range"):
|
||||||
|
return false
|
||||||
|
return bull_flat.normalized().dot(to_me.normalized()) > 0.85
|
||||||
|
|
||||||
|
|
||||||
|
func _tick_wander(delta: float) -> void:
|
||||||
|
if _idle_timer > 0.0:
|
||||||
|
_idle_timer -= delta
|
||||||
|
velocity.x = move_toward(velocity.x, 0.0, 10.0 * delta)
|
||||||
|
velocity.z = move_toward(velocity.z, 0.0, 10.0 * delta)
|
||||||
|
_play_anim(_IDLE_ANIM)
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
|
||||||
|
var dx: float = _wander_target.x - global_position.x
|
||||||
|
var dz: float = _wander_target.z - global_position.z
|
||||||
|
if Vector2(dx, dz).length() < 0.8:
|
||||||
|
_idle_timer = randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
|
||||||
|
_pick_wander_target()
|
||||||
|
else:
|
||||||
|
var dir := Vector3(dx, 0.0, dz).normalized()
|
||||||
|
var spd := DP.f("mat_walk_speed")
|
||||||
|
velocity.x = dir.x * spd
|
||||||
|
velocity.z = dir.z * spd
|
||||||
|
_mesh.rotation.y = lerp_angle(
|
||||||
|
_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
||||||
|
_play_anim(_WALK_ANIM)
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
|
func _tick_flee(delta: float) -> void:
|
||||||
|
var away := (global_position - _bull.global_position)
|
||||||
|
away.y = 0.0
|
||||||
|
if away.length() < 0.01:
|
||||||
|
away = Vector3(randf() - 0.5, 0.0, randf() - 0.5)
|
||||||
|
away = away.normalized()
|
||||||
|
var spd := DP.f("mat_flee_speed")
|
||||||
|
velocity.x = away.x * spd
|
||||||
|
velocity.z = away.z * spd
|
||||||
|
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
||||||
|
_play_anim(_WALK_ANIM)
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
|
# Cite phase: plant feet, face the bull — wait for it to commit
|
||||||
|
func _start_brace() -> void:
|
||||||
|
_state = State.BRACE
|
||||||
|
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
||||||
|
# Use bull velocity if it's moving, else use bull→matador direction as proxy
|
||||||
|
var charge_dir: Vector3
|
||||||
|
if bull_flat.length() > 1.0:
|
||||||
|
charge_dir = bull_flat.normalized()
|
||||||
|
else:
|
||||||
|
charge_dir = (_bull.global_position - global_position)
|
||||||
|
charge_dir.y = 0.0
|
||||||
|
charge_dir = charge_dir.normalized()
|
||||||
|
# Decide whether to dodge at all and which side — fixed before the bull arrives
|
||||||
|
_will_dodge = randf() < DP.f("mat_dodge_chance")
|
||||||
|
var side := 1.0 if randf() > 0.5 else -1.0
|
||||||
|
_step_dir = charge_dir.rotated(Vector3.UP, PI * 0.5 * side)
|
||||||
|
_brace_timer = DP.f("mat_brace_duration")
|
||||||
|
|
||||||
|
|
||||||
|
func _tick_brace(delta: float) -> void:
|
||||||
|
# Decelerate to a stop
|
||||||
|
velocity.x = move_toward(velocity.x, 0.0, 18.0 * delta)
|
||||||
|
velocity.z = move_toward(velocity.z, 0.0, 18.0 * delta)
|
||||||
|
# Face the bull
|
||||||
|
var to_bull := (_bull.global_position - global_position)
|
||||||
|
to_bull.y = 0.0
|
||||||
|
if to_bull.length() > 0.1:
|
||||||
|
_mesh.rotation.y = lerp_angle(
|
||||||
|
_mesh.rotation.y, atan2(to_bull.x, to_bull.z), delta * 14.0)
|
||||||
|
_play_anim(_IDLE_ANIM)
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
_brace_timer -= delta
|
||||||
|
var dist := global_position.distance_to(_bull.global_position)
|
||||||
|
# Execute the step when the bull is almost on top or the brace window expires
|
||||||
|
if dist < DP.f("mat_commit_dist") or _brace_timer <= 0.0:
|
||||||
|
if _will_dodge:
|
||||||
|
_state = State.SIDESTEP
|
||||||
|
_step_timer = DP.f("mat_step_duration")
|
||||||
|
if _ole_player:
|
||||||
|
_ole_player.pitch_scale = randf_range(0.88, 1.12)
|
||||||
|
_ole_player.play()
|
||||||
|
else:
|
||||||
|
# Stand and eat it — no juke
|
||||||
|
_state = State.FLEE
|
||||||
|
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||||
|
|
||||||
|
|
||||||
|
# Pase phase: sharp lateral step as the bull commits to its line
|
||||||
|
func _tick_sidestep(delta: float) -> void:
|
||||||
|
_step_timer -= delta
|
||||||
|
if _step_timer <= 0.0:
|
||||||
|
_state = State.FLEE
|
||||||
|
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||||
|
return
|
||||||
|
var spd := DP.f("mat_step_speed")
|
||||||
|
velocity.x = _step_dir.x * spd
|
||||||
|
velocity.z = _step_dir.z * spd
|
||||||
|
# Snap facing quickly to sell the movement
|
||||||
|
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 20.0)
|
||||||
|
_play_anim(_WALK_ANIM)
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
|
func _tick_ragdoll(_delta: float) -> void:
|
||||||
|
velocity.x = 0.0
|
||||||
|
velocity.z = 0.0
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node3D) -> void:
|
||||||
|
if _state == State.RAGDOLL:
|
||||||
|
return
|
||||||
|
if not body.is_in_group(&"player"):
|
||||||
|
return
|
||||||
|
var player := body as CharacterBody3D
|
||||||
|
if player.velocity.length() < DP.f("mat_hit_threshold"):
|
||||||
|
return
|
||||||
|
var flat_vel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||||
|
var hit_dir := flat_vel.normalized() if flat_vel.length() > 0.5 else \
|
||||||
|
(global_position - player.global_position).normalized()
|
||||||
|
_enter_ragdoll(hit_dir, player.velocity.length())
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||||
|
_state = State.RAGDOLL
|
||||||
|
killed.emit()
|
||||||
|
var _t := get_tree().create_timer(4.0)
|
||||||
|
_t.timeout.connect(func() -> void:
|
||||||
|
if is_instance_valid(self):
|
||||||
|
queue_free()
|
||||||
|
)
|
||||||
|
hit_dir.y = 0.0
|
||||||
|
hit_dir = hit_dir.normalized()
|
||||||
|
# Arc upward so the body lifts before falling — feels like a real goring throw
|
||||||
|
var throw_dir := (hit_dir + Vector3(0.0, 0.5, 0.0)).normalized()
|
||||||
|
|
||||||
|
_spawn_blood_burst(hit_dir)
|
||||||
|
if _death_player:
|
||||||
|
_death_player.pitch_scale = randf_range(0.9, 1.1)
|
||||||
|
_death_player.play()
|
||||||
|
|
||||||
|
var cam := _bull.get_node_or_null("Camera3D")
|
||||||
|
if cam:
|
||||||
|
cam.call(&"trigger_hit", clampf(bull_speed / 15.0, 0.4, 1.0))
|
||||||
|
|
||||||
|
# Keep the CharacterBody3D stationary: physics bones simulate relative to the
|
||||||
|
# skeleton root, so sliding the root makes every bone pose diverge.
|
||||||
|
velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
# Remove from collision layer so the bull passes through, but keep the
|
||||||
|
# shape enabled — move_and_slide() still needs it to detect the floor.
|
||||||
|
collision_layer = 0
|
||||||
|
|
||||||
|
if _anim_player:
|
||||||
|
_anim_player.pause() # pause keeps current frame; stop() resets to T-pose
|
||||||
|
|
||||||
|
if not _sim:
|
||||||
|
return
|
||||||
|
# Teleport each physics body to its current bone world transform, then
|
||||||
|
# enable per-bone simulation. _sim.active alone does NOT set simulate_physics;
|
||||||
|
# without it _process_modification() skips every bone and writes nothing.
|
||||||
|
for child: Node in _sim.get_children():
|
||||||
|
if not (child is PhysicalBone3D):
|
||||||
|
continue
|
||||||
|
var pb := child as PhysicalBone3D
|
||||||
|
var bone_idx := _skeleton.find_bone(pb.bone_name)
|
||||||
|
if bone_idx >= 0:
|
||||||
|
pb.global_transform = _skeleton.global_transform * _skeleton.get_bone_global_pose(bone_idx)
|
||||||
|
_sim.active = true
|
||||||
|
_sim.physical_bones_start_simulation()
|
||||||
|
var strength := DP.f("mat_ragdoll_impulse") * clampf(bull_speed / 15.0, 0.4, 1.3)
|
||||||
|
var right := throw_dir.cross(Vector3.UP).normalized()
|
||||||
|
for child: Node in _sim.get_children():
|
||||||
|
if not (child is PhysicalBone3D):
|
||||||
|
continue
|
||||||
|
var pb := child as PhysicalBone3D
|
||||||
|
var impulse := Vector3.ZERO
|
||||||
|
match pb.bone_name:
|
||||||
|
"COG", "matador":
|
||||||
|
# Core of mass takes the full hit
|
||||||
|
impulse = throw_dir * strength
|
||||||
|
"chest":
|
||||||
|
impulse = (throw_dir + Vector3.UP * 0.3).normalized() * strength * 0.9
|
||||||
|
"head":
|
||||||
|
impulse = (throw_dir + Vector3.UP * 0.5).normalized() * strength * 0.7
|
||||||
|
"leg_L":
|
||||||
|
impulse = (throw_dir * 0.4 + right * 0.6 + Vector3.UP * 0.4).normalized() * strength * 0.6
|
||||||
|
"leg_R":
|
||||||
|
impulse = (throw_dir * 0.4 - right * 0.6 + Vector3.UP * 0.4).normalized() * strength * 0.6
|
||||||
|
"arm_L":
|
||||||
|
impulse = (throw_dir * 0.3 + right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
|
||||||
|
"arm_R":
|
||||||
|
impulse = (throw_dir * 0.3 - right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
|
||||||
|
_:
|
||||||
|
# Distal bones (forearms, shins, hands, feet) get no direct impulse;
|
||||||
|
# they trail naturally from the joints above them
|
||||||
|
continue
|
||||||
|
# Small random noise per bone so no two look identical
|
||||||
|
var noise := Vector3(randf_range(-0.15, 0.15), randf_range(-0.08, 0.15), randf_range(-0.15, 0.15))
|
||||||
|
pb.apply_central_impulse(impulse + noise * strength * 0.2)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Blood burst ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _setup_blood_burst() -> 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))
|
||||||
|
|
||||||
|
_blood_burst = CPUParticles3D.new()
|
||||||
|
_blood_burst.mesh = sphere
|
||||||
|
_blood_burst.color_ramp = ramp
|
||||||
|
_blood_burst.scale_amount_curve = scale_curve
|
||||||
|
_blood_burst.one_shot = true
|
||||||
|
_blood_burst.explosiveness = 0.9
|
||||||
|
_blood_burst.amount = 25
|
||||||
|
_blood_burst.lifetime = 1.1
|
||||||
|
_blood_burst.randomness = 0.4
|
||||||
|
_blood_burst.local_coords = false
|
||||||
|
_blood_burst.spread = 80.0
|
||||||
|
_blood_burst.gravity = Vector3(0.0, -7.0, 0.0)
|
||||||
|
_blood_burst.initial_velocity_min = 4.0
|
||||||
|
_blood_burst.initial_velocity_max = 11.0
|
||||||
|
_blood_burst.scale_amount_min = 0.5
|
||||||
|
_blood_burst.scale_amount_max = 1.6
|
||||||
|
_blood_burst.emitting = false
|
||||||
|
add_child(_blood_burst)
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_blood_burst(hit_dir: Vector3) -> void:
|
||||||
|
_blood_burst.global_position = global_position + Vector3(0.0, 0.9, 0.0)
|
||||||
|
_blood_burst.direction = (hit_dir * 0.6 + Vector3.UP * 0.4).normalized()
|
||||||
|
_blood_burst.restart()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Animation helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _ensure_loop(anim_name: StringName) -> void:
|
||||||
|
if _anim_player.has_animation(anim_name):
|
||||||
|
var anim := _anim_player.get_animation(anim_name)
|
||||||
|
anim.loop_mode = Animation.LOOP_LINEAR
|
||||||
|
|
||||||
|
|
||||||
|
func _play_anim(anim_name: StringName) -> void:
|
||||||
|
if not _anim_player:
|
||||||
|
return
|
||||||
|
if not _anim_player.has_animation(anim_name):
|
||||||
|
return
|
||||||
|
if _anim_player.current_animation != anim_name:
|
||||||
|
_anim_player.play(anim_name)
|
||||||
|
|
||||||
|
|
||||||
|
func _pick_wander_target() -> void:
|
||||||
|
var radius := DP.f("mat_wander_radius")
|
||||||
|
var angle := randf() * TAU
|
||||||
|
var dist := randf_range(2.0, radius)
|
||||||
|
_wander_target = Vector3(cos(angle) * dist, 0.0, sin(angle) * dist)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Ragdoll setup ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
||||||
|
for child: Node in _skeleton.get_children():
|
||||||
|
if child is PhysicalBoneSimulator3D:
|
||||||
|
(child as PhysicalBoneSimulator3D).active = false
|
||||||
|
return child as PhysicalBoneSimulator3D
|
||||||
|
|
||||||
|
var sim := PhysicalBoneSimulator3D.new()
|
||||||
|
sim.active = false
|
||||||
|
_skeleton.add_child(sim)
|
||||||
|
|
||||||
|
const BONES: Array = [
|
||||||
|
["matador", 0.18],
|
||||||
|
["COG", 0.12],
|
||||||
|
["chest", 0.13, 0.30],
|
||||||
|
["head", 0.14],
|
||||||
|
["collarbone_L", 0.06, 0.18],
|
||||||
|
["collarbone_R", 0.06, 0.18],
|
||||||
|
["arm_L", 0.07, 0.22],
|
||||||
|
["arm_R", 0.07, 0.22],
|
||||||
|
["forearm_L", 0.06, 0.22],
|
||||||
|
["forearm_R", 0.06, 0.22],
|
||||||
|
["hand_L", 0.05, 0.12],
|
||||||
|
["hand_R", 0.05, 0.12],
|
||||||
|
["leg_L", 0.10, 0.32],
|
||||||
|
["leg_R", 0.10, 0.32],
|
||||||
|
["shin_L", 0.08, 0.28],
|
||||||
|
["shin_R", 0.08, 0.28],
|
||||||
|
["foot_L", 0.07, 0.18],
|
||||||
|
["foot_R", 0.07, 0.18],
|
||||||
|
]
|
||||||
|
|
||||||
|
for entry: Array in BONES:
|
||||||
|
var bname: String = entry[0]
|
||||||
|
if _skeleton.find_bone(bname) == -1:
|
||||||
|
continue
|
||||||
|
var pb := PhysicalBone3D.new()
|
||||||
|
pb.bone_name = bname
|
||||||
|
pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
|
||||||
|
pb.mass = 0.4
|
||||||
|
pb.linear_damp = 0.1
|
||||||
|
pb.angular_damp = 0.2
|
||||||
|
pb.collision_layer = 1
|
||||||
|
pb.collision_mask = 1
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
var use_capsule: bool = entry.size() >= 3
|
||||||
|
if use_capsule:
|
||||||
|
var cap := CapsuleShape3D.new()
|
||||||
|
cap.radius = entry[1]
|
||||||
|
cap.height = entry[2]
|
||||||
|
cs.shape = cap
|
||||||
|
else:
|
||||||
|
var sph := SphereShape3D.new()
|
||||||
|
sph.radius = entry[1]
|
||||||
|
cs.shape = sph
|
||||||
|
pb.add_child(cs)
|
||||||
|
sim.add_child(pb)
|
||||||
|
|
||||||
|
return sim
|
||||||
|
|
||||||
|
|
||||||
|
# ── Utility ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _find_skeleton(node: Node) -> Skeleton3D:
|
||||||
|
if node is Skeleton3D:
|
||||||
|
return node as Skeleton3D
|
||||||
|
for child: Node in node.get_children():
|
||||||
|
var r := _find_skeleton(child)
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _find_anim_player(node: Node) -> AnimationPlayer:
|
||||||
|
if node is AnimationPlayer:
|
||||||
|
return node as AnimationPlayer
|
||||||
|
for child: Node in node.get_children():
|
||||||
|
var r := _find_anim_player(child)
|
||||||
|
if r:
|
||||||
|
return r
|
||||||
|
return null
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
signal matador_killed
|
||||||
|
|
||||||
|
const MATADOR := preload("res://Matador.tscn")
|
||||||
|
const RESPAWN_COOLDOWN := 8.0
|
||||||
|
|
||||||
|
var _respawn_cooldown: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group(&"matador_spawn")
|
||||||
|
_spawn.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_respawn_cooldown = maxf(0.0, _respawn_cooldown - delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
|
if event.physical_keycode == KEY_R and _respawn_cooldown <= 0.0:
|
||||||
|
_respawn_cooldown = RESPAWN_COOLDOWN
|
||||||
|
reset_spawn()
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn() -> void:
|
||||||
|
var count: int = int(DP.f("mat_spawn_count"))
|
||||||
|
for i: int in count:
|
||||||
|
_spawn_one()
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_one() -> void:
|
||||||
|
var m: Node3D = MATADOR.instantiate()
|
||||||
|
m.position = Vector3(0.0, 1.0, 0.0)
|
||||||
|
get_parent().add_child(m)
|
||||||
|
m.killed.connect(_on_matador_killed)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_matador_killed() -> void:
|
||||||
|
matador_killed.emit()
|
||||||
|
_spawn_one.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func reset_spawn() -> void:
|
||||||
|
for m: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||||
|
m.queue_free()
|
||||||
|
await get_tree().process_frame
|
||||||
|
_spawn()
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
# Hoof positions in bull_v10 local space — dust emitter offsets
|
||||||
|
const HOOF_OFFSETS: Array = [
|
||||||
|
Vector3(-0.30, -0.25, -0.50),
|
||||||
|
Vector3( 0.30, -0.25, -0.50),
|
||||||
|
Vector3(-0.30, -0.25, 0.60),
|
||||||
|
Vector3( 0.30, -0.25, 0.60),
|
||||||
|
]
|
||||||
|
|
||||||
|
@onready var camera_pivot: Node3D = $Camera3D
|
||||||
|
@onready var cube_guy: Node3D = $bull_v10
|
||||||
|
|
||||||
|
var _hoof_emitters: Array[CPUParticles3D] = []
|
||||||
|
var _legs: Node
|
||||||
|
|
||||||
|
var _kick_timer: float = 0.0
|
||||||
|
var _was_on_floor: bool = false
|
||||||
|
var _pre_slide_vel_y: float = 0.0
|
||||||
|
var _charge_dir: Vector3 = Vector3.ZERO
|
||||||
|
var _charge_hold_time: float = 0.0
|
||||||
|
var _both_held: bool = false
|
||||||
|
var _thrust_left_charge: float = 0.0
|
||||||
|
var _thrust_right_charge: float = 0.0
|
||||||
|
var _charge_was_full: bool = false
|
||||||
|
|
||||||
|
var _charge_overlay: ColorRect = null
|
||||||
|
var _charge_ready_emitter: CPUParticles3D = null
|
||||||
|
|
||||||
|
var _huff_player: AudioStreamPlayer = null
|
||||||
|
var _crowd_player: AudioStreamPlayer = null
|
||||||
|
var _huff_timer: float = 0.0
|
||||||
|
var _roll_damping: float = 0.15
|
||||||
|
|
||||||
|
enum DustState { NONE, WALK, CHARGE }
|
||||||
|
var _dust_state: DustState = DustState.NONE
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group(&"player")
|
||||||
|
_setup_hoof_dust()
|
||||||
|
_setup_legs()
|
||||||
|
_setup_charge_indicators()
|
||||||
|
_setup_audio()
|
||||||
|
DP.any_changed.connect(_on_dp_changed)
|
||||||
|
_roll_damping = DP.f("roll_damping")
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_legs() -> void:
|
||||||
|
_legs = Node.new()
|
||||||
|
_legs.set_script(load("res://bull_legs.gd"))
|
||||||
|
add_child(_legs)
|
||||||
|
_legs.call(&"setup", self, cube_guy)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_hoof_dust() -> 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.10
|
||||||
|
sphere.height = 0.20
|
||||||
|
sphere.radial_segments = 4
|
||||||
|
sphere.rings = 2
|
||||||
|
sphere.material = mat
|
||||||
|
|
||||||
|
var ramp := Gradient.new()
|
||||||
|
ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0))
|
||||||
|
ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0))
|
||||||
|
|
||||||
|
var scale_curve := Curve.new()
|
||||||
|
scale_curve.add_point(Vector2(0.0, 0.2))
|
||||||
|
scale_curve.add_point(Vector2(0.4, 1.0))
|
||||||
|
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||||
|
|
||||||
|
for offset: Vector3 in HOOF_OFFSETS:
|
||||||
|
var p := CPUParticles3D.new()
|
||||||
|
p.position = offset
|
||||||
|
p.emitting = false
|
||||||
|
p.amount = 12
|
||||||
|
p.lifetime = DP.f("dust_lifetime")
|
||||||
|
p.explosiveness = DP.f("dust_explosiveness")
|
||||||
|
p.randomness = 0.6
|
||||||
|
p.local_coords = false
|
||||||
|
p.direction = Vector3(0.0, 1.0, 0.0)
|
||||||
|
p.spread = DP.f("dust_spread")
|
||||||
|
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
||||||
|
p.initial_velocity_min = DP.f("walk_vel_min")
|
||||||
|
p.initial_velocity_max = DP.f("walk_vel_max")
|
||||||
|
p.scale_amount_min = DP.f("walk_scale_min")
|
||||||
|
p.scale_amount_max = DP.f("walk_scale_max")
|
||||||
|
p.mesh = sphere
|
||||||
|
p.color_ramp = ramp
|
||||||
|
p.scale_amount_curve = scale_curve
|
||||||
|
cube_guy.add_child(p)
|
||||||
|
_hoof_emitters.append(p)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_charge_indicators() -> 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
|
||||||
|
|
||||||
|
# Screen overlay — red tint while charge-ahead (both buttons) is active
|
||||||
|
var ui := CanvasLayer.new()
|
||||||
|
add_child(ui)
|
||||||
|
_charge_overlay = ColorRect.new()
|
||||||
|
_charge_overlay.color = Color(0.7, 0.05, 0.0, 0.0)
|
||||||
|
_charge_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_charge_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
ui.add_child(_charge_overlay)
|
||||||
|
|
||||||
|
# Orange spark burst when single-button charge hits max
|
||||||
|
var burst_ramp := Gradient.new()
|
||||||
|
burst_ramp.set_color(0, Color(1.0, 0.55, 0.0, 1.0))
|
||||||
|
burst_ramp.set_color(1, Color(1.0, 0.15, 0.0, 0.0))
|
||||||
|
|
||||||
|
var burst_scale := Curve.new()
|
||||||
|
burst_scale.add_point(Vector2(0.0, 1.0))
|
||||||
|
burst_scale.add_point(Vector2(1.0, 0.0))
|
||||||
|
|
||||||
|
_charge_ready_emitter = CPUParticles3D.new()
|
||||||
|
_charge_ready_emitter.position = Vector3(0.0, 0.3, 0.0)
|
||||||
|
_charge_ready_emitter.emitting = false
|
||||||
|
_charge_ready_emitter.one_shot = true
|
||||||
|
_charge_ready_emitter.amount = 16
|
||||||
|
_charge_ready_emitter.lifetime = 0.5
|
||||||
|
_charge_ready_emitter.explosiveness = 1.0
|
||||||
|
_charge_ready_emitter.randomness = 0.4
|
||||||
|
_charge_ready_emitter.local_coords = false
|
||||||
|
_charge_ready_emitter.direction = Vector3.UP
|
||||||
|
_charge_ready_emitter.spread = 120.0
|
||||||
|
_charge_ready_emitter.initial_velocity_min = 2.5
|
||||||
|
_charge_ready_emitter.initial_velocity_max = 5.0
|
||||||
|
_charge_ready_emitter.scale_amount_min = 0.2
|
||||||
|
_charge_ready_emitter.scale_amount_max = 0.5
|
||||||
|
_charge_ready_emitter.mesh = sphere
|
||||||
|
_charge_ready_emitter.color_ramp = burst_ramp
|
||||||
|
_charge_ready_emitter.scale_amount_curve = burst_scale
|
||||||
|
cube_guy.add_child(_charge_ready_emitter)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_audio() -> void:
|
||||||
|
if ResourceLoader.exists("res://sounds/bull_huff.ogg"):
|
||||||
|
_huff_player = AudioStreamPlayer.new()
|
||||||
|
_huff_player.stream = load("res://sounds/bull_huff.ogg")
|
||||||
|
_huff_player.volume_db = 0.0
|
||||||
|
add_child(_huff_player)
|
||||||
|
if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"):
|
||||||
|
_crowd_player = AudioStreamPlayer.new()
|
||||||
|
var s := load("res://sounds/crowd_ambient.ogg") as AudioStreamOggVorbis
|
||||||
|
if s:
|
||||||
|
s.loop = true
|
||||||
|
_crowd_player.stream = s
|
||||||
|
_crowd_player.volume_db = -14.0
|
||||||
|
_crowd_player.autoplay = true
|
||||||
|
add_child(_crowd_player)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_dust_state(new_state: DustState) -> void:
|
||||||
|
if new_state == _dust_state:
|
||||||
|
return
|
||||||
|
_dust_state = new_state
|
||||||
|
for p: CPUParticles3D in _hoof_emitters:
|
||||||
|
p.lifetime = DP.f("dust_lifetime")
|
||||||
|
p.explosiveness = DP.f("dust_explosiveness")
|
||||||
|
p.spread = DP.f("dust_spread")
|
||||||
|
p.gravity = Vector3(0.0, DP.f("dust_gravity_y"), 0.0)
|
||||||
|
match new_state:
|
||||||
|
DustState.NONE:
|
||||||
|
p.emitting = false
|
||||||
|
DustState.WALK:
|
||||||
|
p.scale_amount_min = DP.f("walk_scale_min")
|
||||||
|
p.scale_amount_max = DP.f("walk_scale_max")
|
||||||
|
p.initial_velocity_min = DP.f("walk_vel_min")
|
||||||
|
p.initial_velocity_max = DP.f("walk_vel_max")
|
||||||
|
p.emitting = true
|
||||||
|
DustState.CHARGE:
|
||||||
|
p.scale_amount_min = DP.f("charge_scale_min")
|
||||||
|
p.scale_amount_max = DP.f("charge_scale_max")
|
||||||
|
p.initial_velocity_min = DP.f("charge_vel_min")
|
||||||
|
p.initial_velocity_max = DP.f("charge_vel_max")
|
||||||
|
p.emitting = true
|
||||||
|
|
||||||
|
|
||||||
|
func _on_dp_changed(_key: String, _val: Variant) -> void:
|
||||||
|
_roll_damping = DP.f("roll_damping")
|
||||||
|
if _hoof_emitters.is_empty() or _dust_state == DustState.NONE:
|
||||||
|
return
|
||||||
|
var prev := _dust_state
|
||||||
|
_dust_state = DustState.NONE
|
||||||
|
_set_dust_state(prev)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_thrust(dir: Vector3) -> void:
|
||||||
|
var impulse := DP.f("thrust_impulse")
|
||||||
|
velocity.x += dir.x * impulse
|
||||||
|
velocity.z += dir.z * impulse
|
||||||
|
if is_on_floor():
|
||||||
|
velocity.y += DP.f("thrust_vertical")
|
||||||
|
var flat := Vector2(velocity.x, velocity.z)
|
||||||
|
var spd := flat.length()
|
||||||
|
if spd > DP.f("max_speed"):
|
||||||
|
var s := DP.f("max_speed") / spd
|
||||||
|
velocity.x *= s
|
||||||
|
velocity.z *= s
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_kick(direction: Vector3) -> void:
|
||||||
|
var force := DP.f("kick_impulse") * randf_range(1.0 - DP.f("kick_force_var"), 1.0 + DP.f("kick_force_var"))
|
||||||
|
var spread := deg_to_rad(randf_range(-DP.f("kick_spread"), DP.f("kick_spread")))
|
||||||
|
var kick_dir := direction.rotated(Vector3.UP, spread)
|
||||||
|
velocity.x += kick_dir.x * force
|
||||||
|
velocity.z += kick_dir.z * force
|
||||||
|
if is_on_floor():
|
||||||
|
velocity.y += DP.f("kick_vertical") * randf_range(0.75, 1.25)
|
||||||
|
var flat := Vector2(velocity.x, velocity.z)
|
||||||
|
var spd := flat.length()
|
||||||
|
if spd > DP.f("max_speed"):
|
||||||
|
var s := DP.f("max_speed") / spd
|
||||||
|
velocity.x *= s
|
||||||
|
velocity.z *= s
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
# ── Gravity ───────────────────────────────────────────────────────────────
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity += get_gravity() * delta
|
||||||
|
|
||||||
|
# ── Camera-relative input direction ───────────────────────────────────────
|
||||||
|
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||||
|
var cam_basis := camera_pivot.global_transform.basis
|
||||||
|
var flat_forward := -Vector3(cam_basis.z.x, 0.0, cam_basis.z.z).normalized()
|
||||||
|
var flat_right := Vector3(cam_basis.x.x, 0.0, cam_basis.x.z).normalized()
|
||||||
|
var direction := (flat_forward * -input_dir.y + flat_right * input_dir.x).normalized()
|
||||||
|
|
||||||
|
var on_floor := is_on_floor()
|
||||||
|
|
||||||
|
# ── Mouse button actions ──────────────────────────────────────────────────
|
||||||
|
# Both held → charge: lock direction, ramp speed, lower head
|
||||||
|
# Left only → thrust NE (forward-right diagonal)
|
||||||
|
# Right only → thrust NW (forward-left diagonal)
|
||||||
|
var left_held := Input.is_action_pressed(&"thrust_left")
|
||||||
|
var right_held := Input.is_action_pressed(&"thrust_right")
|
||||||
|
var both_held := left_held and right_held
|
||||||
|
|
||||||
|
if both_held and not _both_held:
|
||||||
|
var raw := cube_guy.global_transform.basis.z
|
||||||
|
_charge_dir = Vector3(raw.x, 0.0, raw.z).normalized()
|
||||||
|
if _huff_player:
|
||||||
|
_huff_player.pitch_scale = randf_range(0.9, 1.1)
|
||||||
|
_huff_player.play()
|
||||||
|
_huff_timer = randf_range(1.4, 2.2)
|
||||||
|
|
||||||
|
if both_held:
|
||||||
|
_thrust_left_charge = 0.0
|
||||||
|
_thrust_right_charge = 0.0
|
||||||
|
_charge_hold_time += delta
|
||||||
|
_huff_timer -= delta
|
||||||
|
if _huff_timer <= 0.0 and _huff_player and not _huff_player.playing:
|
||||||
|
_huff_player.pitch_scale = randf_range(0.9, 1.1)
|
||||||
|
_huff_player.play()
|
||||||
|
_huff_timer = randf_range(1.4, 2.2)
|
||||||
|
var ramp := minf(_charge_hold_time / DP.f("charge_ramp_time"), 1.0)
|
||||||
|
var accel := DP.f("bull_charge_accel") * lerpf(0.2, 1.0, ramp)
|
||||||
|
velocity.x += _charge_dir.x * accel * delta
|
||||||
|
velocity.z += _charge_dir.z * accel * delta
|
||||||
|
var flat_c := Vector2(velocity.x, velocity.z)
|
||||||
|
var spd_c := flat_c.length()
|
||||||
|
if spd_c > DP.f("bull_charge_max_speed"):
|
||||||
|
velocity.x *= DP.f("bull_charge_max_speed") / spd_c
|
||||||
|
velocity.z *= DP.f("bull_charge_max_speed") / spd_c
|
||||||
|
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
|
||||||
|
else:
|
||||||
|
_charge_hold_time = 0.0
|
||||||
|
_legs.set(&"charge_pitch_target", 0.0)
|
||||||
|
|
||||||
|
if left_held and not right_held:
|
||||||
|
_thrust_left_charge = minf(_thrust_left_charge + delta, DP.f("thrust_charge_time"))
|
||||||
|
elif Input.is_action_just_released(&"thrust_left") and _thrust_left_charge > 0.0:
|
||||||
|
var t := _thrust_left_charge / DP.f("thrust_charge_time")
|
||||||
|
var angle := lerpf(deg_to_rad(DP.f("thrust_min_angle")), deg_to_rad(DP.f("thrust_max_angle")), t)
|
||||||
|
var raw := cube_guy.global_transform.basis.z
|
||||||
|
_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, -angle))
|
||||||
|
_thrust_left_charge = 0.0
|
||||||
|
|
||||||
|
if right_held and not left_held:
|
||||||
|
_thrust_right_charge = minf(_thrust_right_charge + delta, DP.f("thrust_charge_time"))
|
||||||
|
elif Input.is_action_just_released(&"thrust_right") and _thrust_right_charge > 0.0:
|
||||||
|
var t := _thrust_right_charge / DP.f("thrust_charge_time")
|
||||||
|
var angle := lerpf(deg_to_rad(DP.f("thrust_min_angle")), deg_to_rad(DP.f("thrust_max_angle")), t)
|
||||||
|
var raw := cube_guy.global_transform.basis.z
|
||||||
|
_apply_thrust(Vector3(raw.x, 0.0, raw.z).normalized().rotated(Vector3.UP, angle))
|
||||||
|
_thrust_right_charge = 0.0
|
||||||
|
|
||||||
|
# Fire spark burst the frame either single-button charge first hits max
|
||||||
|
var max_charge := DP.f("thrust_charge_time")
|
||||||
|
var either_full := _thrust_left_charge >= max_charge or _thrust_right_charge >= max_charge
|
||||||
|
if either_full and not _charge_was_full:
|
||||||
|
_charge_ready_emitter.restart()
|
||||||
|
_charge_was_full = either_full
|
||||||
|
|
||||||
|
# Fade red overlay in while charge-ahead, out when released
|
||||||
|
var target_alpha := 0.30 if both_held else 0.0
|
||||||
|
var oc := _charge_overlay.color
|
||||||
|
_charge_overlay.color = Color(oc.r, oc.g, oc.b, lerpf(oc.a, target_alpha, delta * 10.0))
|
||||||
|
|
||||||
|
_both_held = both_held
|
||||||
|
|
||||||
|
# Reset kick timer on landing so the first kick after a bounce is immediate
|
||||||
|
if on_floor and not _was_on_floor and direction:
|
||||||
|
_kick_timer = 0.0
|
||||||
|
_was_on_floor = on_floor
|
||||||
|
|
||||||
|
# ── Periodic kick ─────────────────────────────────────────────────────────
|
||||||
|
_kick_timer = maxf(0.0, _kick_timer - delta)
|
||||||
|
if direction and _kick_timer <= 0.0 and not both_held:
|
||||||
|
_apply_kick(direction)
|
||||||
|
_kick_timer = DP.f("kick_interval")
|
||||||
|
|
||||||
|
# ── Rolling friction (exponential, frame-rate independent) ────────────────
|
||||||
|
var retain := pow(_roll_damping, delta)
|
||||||
|
velocity.x *= retain
|
||||||
|
velocity.z *= retain
|
||||||
|
|
||||||
|
# ── Visual rotation follows velocity direction ─────────────────────────────
|
||||||
|
var flat_speed := Vector2(velocity.x, velocity.z).length()
|
||||||
|
if flat_speed > 0.4:
|
||||||
|
var target_angle := atan2(velocity.x, velocity.z)
|
||||||
|
cube_guy.rotation.y = lerp_angle(
|
||||||
|
cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))
|
||||||
|
|
||||||
|
_pre_slide_vel_y = velocity.y
|
||||||
|
var pre_wall_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
# ── Wall bounce — no sliding, guaranteed exit ────────────────────────────
|
||||||
|
# pre_wall_vel is captured before move_and_slide strips the into-wall component,
|
||||||
|
# so into_spd correctly reflects how hard the bull hit the wall even when stationary.
|
||||||
|
for i in get_slide_collision_count():
|
||||||
|
var col := get_slide_collision(i)
|
||||||
|
var normal := col.get_normal()
|
||||||
|
if abs(normal.y) < 0.5:
|
||||||
|
var flat_n := Vector3(normal.x, 0.0, normal.z).normalized()
|
||||||
|
var into_spd := -pre_wall_vel.dot(flat_n)
|
||||||
|
var exit_spd := maxf(into_spd * DP.f("wall_bounce_restitution"),
|
||||||
|
DP.f("wall_min_bounce_speed"))
|
||||||
|
velocity.x = flat_n.x * exit_spd
|
||||||
|
velocity.z = flat_n.z * exit_spd
|
||||||
|
pre_wall_vel = Vector3(velocity.x, 0.0, velocity.z)
|
||||||
|
if both_held and _charge_dir.dot(flat_n) < 0.0:
|
||||||
|
_charge_dir = _charge_dir.reflect(flat_n)
|
||||||
|
|
||||||
|
# ── Bounce on landing ─────────────────────────────────────────────────────
|
||||||
|
if is_on_floor() and not _was_on_floor:
|
||||||
|
var impact := -_pre_slide_vel_y
|
||||||
|
if impact > DP.f("bounce_min_impact"):
|
||||||
|
velocity.y = impact * DP.f("bounce_restitution")
|
||||||
|
|
||||||
|
# ── Dust ──────────────────────────────────────────────────────────────────
|
||||||
|
if flat_speed > DP.f("dust_charge_spd") and on_floor:
|
||||||
|
_set_dust_state(DustState.CHARGE)
|
||||||
|
elif flat_speed > DP.f("dust_walk_spd") and on_floor:
|
||||||
|
_set_dust_state(DustState.WALK)
|
||||||
|
else:
|
||||||
|
_set_dust_state(DustState.NONE)
|
||||||
|
|
||||||
|
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user