scanlines, sounds, bumper walls, score keep, control legend, timer
This commit is contained in:
+94
-72
@@ -7,6 +7,8 @@ extends CharacterBody3D
|
||||
# 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
|
||||
@@ -18,6 +20,10 @@ 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
|
||||
@@ -42,6 +48,17 @@ func _ready() -> void:
|
||||
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:
|
||||
@@ -84,7 +101,7 @@ func _update_ai_state() -> void:
|
||||
pass # step timer drives transition
|
||||
|
||||
|
||||
# Bull is moving fast and aimed within ~50° of the matador
|
||||
# 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"):
|
||||
@@ -93,7 +110,7 @@ func _is_charge_incoming() -> bool:
|
||||
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
|
||||
return bull_flat.normalized().dot(to_me.normalized()) > 0.85
|
||||
|
||||
|
||||
func _tick_wander(delta: float) -> void:
|
||||
@@ -147,7 +164,8 @@ func _start_brace() -> void:
|
||||
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
|
||||
# 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")
|
||||
@@ -170,8 +188,16 @@ func _tick_brace(delta: float) -> void:
|
||||
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")
|
||||
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
|
||||
@@ -196,36 +222,6 @@ 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
|
||||
@@ -242,12 +238,21 @@ func _on_body_entered(body: Node3D) -> void:
|
||||
|
||||
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:
|
||||
@@ -279,23 +284,40 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||
_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 child is PhysicalBone3D:
|
||||
var scatter := Vector3(
|
||||
randf_range(-0.12, 0.12),
|
||||
randf_range(-0.05, 0.08),
|
||||
randf_range(-0.12, 0.12))
|
||||
(child as PhysicalBone3D).apply_central_impulse(
|
||||
(throw_dir + scatter).normalized() * strength)
|
||||
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 _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)
|
||||
|
||||
func _setup_blood_burst() -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
@@ -318,25 +340,30 @@ func _spawn_blood_burst(hit_dir: Vector3) -> void:
|
||||
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
|
||||
_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)
|
||||
|
||||
get_tree().create_timer(p.lifetime + 0.3).timeout.connect(p.queue_free)
|
||||
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
@@ -356,11 +383,6 @@ func _play_anim(anim_name: StringName) -> void:
|
||||
_anim_player.play(anim_name)
|
||||
|
||||
|
||||
func _reset_bone_poses() -> void:
|
||||
for i: int in _skeleton.get_bone_count():
|
||||
_skeleton.set_bone_pose_rotation(i, Quaternion.IDENTITY)
|
||||
|
||||
|
||||
func _pick_wander_target() -> void:
|
||||
var radius := DP.f("mat_wander_radius")
|
||||
var angle := randf() * TAU
|
||||
@@ -409,8 +431,8 @@ func _setup_physical_bones() -> PhysicalBoneSimulator3D:
|
||||
pb.bone_name = bname
|
||||
pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
|
||||
pb.mass = 0.4
|
||||
pb.linear_damp = 0.4
|
||||
pb.angular_damp = 0.8
|
||||
pb.linear_damp = 0.1
|
||||
pb.angular_damp = 0.2
|
||||
pb.collision_layer = 1
|
||||
pb.collision_mask = 1
|
||||
var cs := CollisionShape3D.new()
|
||||
|
||||
Reference in New Issue
Block a user