From 174b14fa619726710725f5e6022b08189b5a7a05 Mon Sep 17 00:00:00 2001 From: Richard Aasa Date: Mon, 25 May 2026 20:10:16 +0300 Subject: [PATCH] R to reset matador --- Player.tscn | 8 +- debug_params.gd | 19 +++-- matador.gd | 216 ++++++++++++++++++++++++++++++++++++++--------- matador_spawn.gd | 4 +- 4 files changed, 187 insertions(+), 60 deletions(-) diff --git a/Player.tscn b/Player.tscn index 9c0b02a..3204e1e 100644 --- a/Player.tscn +++ b/Player.tscn @@ -2,7 +2,6 @@ [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"] [ext_resource type="PackedScene" uid="uid://qy6qrxdfgx2g" path="res://Assets/bull_v10.fbx" id="4_e80uo"] [sub_resource type="SphereShape3D" id="SphereShape3D_kne1u"] @@ -43,13 +42,8 @@ shape = SubResource("SphereShape3D_kne1u") spring_length = 7.0 margin = 0.5 -[node name="SpringPosition" type="Node3D" parent="SpringArmPivot/SpringArm3D" unique_id=1307131677] - -[node name="Camera3D" type="Camera3D" parent="SpringArmPivot" unique_id=1120161087 node_paths=PackedStringArray("spring_arm")] +[node name="Camera3D" type="Camera3D" parent="SpringArmPivot/SpringArm3D" unique_id=1120161087] keep_aspect = 0 -script = ExtResource("3_kne1u") -spring_arm = NodePath("../SpringArm3D/SpringPosition") -lerp_power = 6.0 [node name="bull_v10" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23) diff --git a/debug_params.gd b/debug_params.gd index 3262361..ed1a1db 100644 --- a/debug_params.gd +++ b/debug_params.gd @@ -80,19 +80,22 @@ func _register_all() -> void: _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", 8.0, 1.0, 30.0, 1.0) + _reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0) _reg_f("Matador", "mat_walk_speed", 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", 3.0, 0.5, 20.0) - _reg_f("Matador", "mat_flee_range", 10.0, 2.0, 40.0) - _reg_f("Matador", "mat_flee_speed", 4.0, 0.5, 12.0) - _reg_f("Matador", "mat_dodge_range", 3.5, 1.0, 10.0) - _reg_f("Matador", "mat_dodge_speed", 7.0, 1.0, 20.0) - _reg_f("Matador", "mat_dodge_duration", 0.5, 0.1, 2.0, 0.05) - _reg_f("Matador", "mat_dodge_cooldown", 1.5, 0.0, 5.0, 0.1) + _reg_f("Matador", "mat_ragdoll_impulse", 4.0, 0.5, 20.0) + _reg_f("Matador", "mat_flee_range", 10.0, 2.0, 40.0) + _reg_f("Matador", "mat_flee_speed", 4.0, 0.5, 12.0) + _reg_f("Matador", "mat_charge_speed", 6.0, 1.0, 30.0) + _reg_f("Matador", "mat_brace_range", 8.0, 2.0, 20.0) + _reg_f("Matador", "mat_brace_duration", 0.4, 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", 9.0, 2.0, 20.0) + _reg_f("Matador", "mat_step_duration", 0.35, 0.1, 1.0, 0.05) + _reg_f("Matador", "mat_dodge_cooldown", 3.0, 0.5, 8.0, 0.25) # ── Debug ───────────────────────────────────────────────────────────────── _reg_b("Debug", "show_collisions", false) diff --git a/matador.gd b/matador.gd index 7481863..7d2a69c 100644 --- a/matador.gd +++ b/matador.gd @@ -1,6 +1,11 @@ extends CharacterBody3D -enum State { WANDER, FLEE, DODGE, RAGDOLL } +# 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 } var _state: State = State.WANDER var _skeleton: Skeleton3D = null @@ -9,12 +14,13 @@ var _anim_player: AnimationPlayer = null var _wander_target: Vector3 = Vector3.ZERO var _idle_timer: float = 0.0 var _bull: CharacterBody3D = null -var _dodge_dir: Vector3 = Vector3.ZERO -var _dodge_timer: float = 0.0 +var _step_dir: Vector3 = Vector3.ZERO +var _brace_timer: float = 0.0 +var _step_timer: float = 0.0 var _dodge_cd: float = 0.0 -@onready var _mesh: Node3D = $matador_v02 -@onready var _hit_area: Area3D = $HitArea +@onready var _mesh: Node3D = $matador_v02 +@onready var _hit_area: Area3D = $HitArea @onready var _body_col: CollisionShape3D = $CollisionShape3D const _WALK_ANIM: StringName = &"Armature|walk" @@ -53,10 +59,11 @@ func _physics_process(delta: float) -> void: _update_ai_state() match _state: - State.WANDER: _tick_wander(delta) - State.FLEE: _tick_flee(delta) - State.DODGE: _tick_dodge(delta) - State.RAGDOLL: _tick_ragdoll(delta) + 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: @@ -66,13 +73,27 @@ func _update_ai_state() -> void: if dist < DP.f("mat_flee_range"): _state = State.FLEE State.FLEE: - if dist < DP.f("mat_dodge_range") and _dodge_cd <= 0.0: - _start_dodge() + 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.DODGE: - pass + State.BRACE: + pass # brace timer drives transition + State.SIDESTEP: + pass # step timer drives transition + + +# Bull is moving fast and aimed within ~50° 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.65 func _tick_wander(delta: float) -> void: @@ -114,33 +135,57 @@ func _tick_flee(delta: float) -> void: move_and_slide() -func _start_dodge() -> void: - _state = State.DODGE - var bull_vel_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z) - var bull_fwd: Vector3 - if bull_vel_flat.length() > 1.0: - bull_fwd = bull_vel_flat.normalized() +# 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: - bull_fwd = (_bull.global_position - global_position) - bull_fwd.y = 0.0 - bull_fwd = bull_fwd.normalized() - # Step sideways relative to the bull's travel direction, biased slightly backward + charge_dir = (_bull.global_position - global_position) + charge_dir.y = 0.0 + charge_dir = charge_dir.normalized() + # Decide which side to step to now, so the choice is fixed before the bull arrives var side := 1.0 if randf() > 0.5 else -1.0 - var perp := bull_fwd.rotated(Vector3.UP, PI * 0.5 * side) - _dodge_dir = (perp * 0.7 + (-bull_fwd) * 0.3).normalized() - _dodge_timer = DP.f("mat_dodge_duration") + _step_dir = charge_dir.rotated(Vector3.UP, PI * 0.5 * side) + _brace_timer = DP.f("mat_brace_duration") -func _tick_dodge(delta: float) -> void: - _dodge_timer -= delta - if _dodge_timer <= 0.0: +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: + _state = State.SIDESTEP + _step_timer = DP.f("mat_step_duration") + + +# 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_dodge_speed") - velocity.x = _dodge_dir.x * spd - velocity.z = _dodge_dir.z * spd - _mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 12.0) + 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() @@ -151,6 +196,36 @@ func _tick_ragdoll(_delta: float) -> void: move_and_slide() +func _unhandled_input(event: InputEvent) -> void: + if not (event is InputEventKey): + return + var key := event as InputEventKey + if key.keycode == KEY_R and key.pressed and not key.echo: + _reset() + + +func _reset() -> void: + _state = State.WANDER + velocity = Vector3.ZERO + collision_layer = 1 + global_position = Vector3(0.0, 1.0, 0.0) + + if _sim: + _sim.physical_bones_stop_simulation() + _sim.active = false + + if _anim_player and _anim_player.has_animation(_IDLE_ANIM): + _anim_player.stop() + _anim_player.play(_IDLE_ANIM) + _anim_player.seek(0.0, true) + + _dodge_cd = 0.0 + _brace_timer = 0.0 + _step_timer = 0.0 + _idle_timer = 0.0 + _pick_wander_target() + + func _on_body_entered(body: Node3D) -> void: if _state == State.RAGDOLL: return @@ -159,7 +234,9 @@ func _on_body_entered(body: Node3D) -> void: var player := body as CharacterBody3D if player.velocity.length() < DP.f("mat_hit_threshold"): return - var hit_dir := (global_position - player.global_position).normalized() + 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()) @@ -167,6 +244,10 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void: _state = State.RAGDOLL 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) # Keep the CharacterBody3D stationary: physics bones simulate relative to the # skeleton root, so sliding the root makes every bone pose diverge. @@ -191,17 +272,67 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void: 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) - pb.simulate_physics = true _sim.active = true - var strength := DP.f("mat_ragdoll_impulse") + _sim.physical_bones_start_simulation() + var strength := DP.f("mat_ragdoll_impulse") * clampf(bull_speed / 15.0, 0.4, 1.3) for child: Node in _sim.get_children(): if child is PhysicalBone3D: var scatter := Vector3( - randf_range(-0.4, 0.4), - randf_range(0.1, 0.5), - randf_range(-0.4, 0.4)) + randf_range(-0.12, 0.12), + randf_range(-0.05, 0.08), + randf_range(-0.12, 0.12)) (child as PhysicalBone3D).apply_central_impulse( - (hit_dir + scatter).normalized() * strength) + (throw_dir + scatter).normalized() * strength) + + +# ── Blood burst ─────────────────────────────────────────────────────────────── + +func _spawn_blood_burst(hit_dir: Vector3) -> void: + var p := CPUParticles3D.new() + get_tree().current_scene.add_child(p) + p.global_position = global_position + Vector3(0.0, 0.9, 0.0) + + 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)) + + p.mesh = sphere + p.color_ramp = ramp + p.scale_amount_curve = scale_curve + p.one_shot = true + p.explosiveness = 0.9 + p.amount = 60 + p.lifetime = 1.1 + p.randomness = 0.4 + p.local_coords = false + p.direction = (hit_dir * 0.6 + Vector3.UP * 0.4).normalized() + p.spread = 80.0 + p.gravity = Vector3(0.0, -7.0, 0.0) + p.initial_velocity_min = 4.0 + p.initial_velocity_max = 11.0 + p.scale_amount_min = 0.5 + p.scale_amount_max = 1.6 + p.emitting = true + + get_tree().create_timer(p.lifetime + 0.3).timeout.connect(p.queue_free) # ── Animation helpers ───────────────────────────────────────────────────────── @@ -273,8 +404,9 @@ func _setup_physical_bones() -> PhysicalBoneSimulator3D: var pb := PhysicalBone3D.new() pb.bone_name = bname pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN - pb.linear_damp = 3.0 - pb.angular_damp = 4.0 + pb.mass = 0.4 + pb.linear_damp = 0.4 + pb.angular_damp = 0.8 pb.collision_layer = 1 pb.collision_mask = 1 var cs := CollisionShape3D.new() diff --git a/matador_spawn.gd b/matador_spawn.gd index becea9a..9e20d69 100644 --- a/matador_spawn.gd +++ b/matador_spawn.gd @@ -11,7 +11,5 @@ func _spawn() -> void: var count: int = int(DP.f("mat_spawn_count")) for i: int in count: var m: Node3D = MATADOR.instantiate() - var angle := randf() * TAU - var dist := randf_range(6.0, DP.f("mat_wander_radius")) - m.position = Vector3(cos(angle) * dist, 1.0, sin(angle) * dist) + m.position = Vector3(0.0, 1.0, 0.0) get_parent().add_child(m)