Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d538679697 | |||
| f6355605cd |
@@ -108,6 +108,12 @@ func _register_all() -> void:
|
|||||||
_reg_f("Matador", "mat_step_duration", 0.35, 0.1, 1.0, 0.05)
|
_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_cooldown", 5.0, 0.5, 12.0, 0.25)
|
||||||
_reg_f("Matador", "mat_dodge_chance", 0.25, 0.0, 1.0, 0.05)
|
_reg_f("Matador", "mat_dodge_chance", 0.25, 0.0, 1.0, 0.05)
|
||||||
|
_reg_f("Matador", "mat_attack_range", 8.0, 2.0, 20.0)
|
||||||
|
_reg_f("Matador", "mat_attack_speed", 5.5, 1.0, 12.0)
|
||||||
|
_reg_f("Matador", "mat_attack_duration", 1.8, 0.5, 5.0, 0.1)
|
||||||
|
_reg_f("Matador", "mat_attack_min_dist", 2.5, 0.5, 6.0, 0.1)
|
||||||
|
_reg_f("Matador", "mat_roll_chance", 0.35, 0.0, 1.0, 0.05)
|
||||||
|
_reg_f("Matador", "mat_roll_duration", 0.45, 0.1, 1.5, 0.05)
|
||||||
# ── Debug ─────────────────────────────────────────────────────────────────
|
# ── Debug ─────────────────────────────────────────────────────────────────
|
||||||
_reg_b("Debug", "show_collisions", false)
|
_reg_b("Debug", "show_collisions", false)
|
||||||
|
|
||||||
|
|||||||
@@ -7,26 +7,16 @@ const _BG := Color(0.06, 0.04, 0.02, 0.78)
|
|||||||
const _BADGE := Color(0.16, 0.10, 0.03, 0.90)
|
const _BADGE := Color(0.16, 0.10, 0.03, 0.90)
|
||||||
const _BORDER := Color(0.65, 0.48, 0.15, 0.55)
|
const _BORDER := Color(0.65, 0.48, 0.15, 0.55)
|
||||||
|
|
||||||
const ROUND_DURATION := 60.0
|
|
||||||
|
|
||||||
var _controls_visible: bool = true
|
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 _controls_panel: PanelContainer
|
||||||
var _timer_label: Label
|
|
||||||
var _score_label: Label
|
|
||||||
var _toggle_btn: Button
|
var _toggle_btn: Button
|
||||||
var _game_over_panel: Control
|
|
||||||
var _final_score_label: Label
|
|
||||||
var _spawner: Node
|
var _spawner: Node
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
layer = 11
|
layer = 11
|
||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
_build()
|
_build_controls_panel()
|
||||||
_find_spawner.call_deferred()
|
_find_spawner.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
@@ -34,16 +24,6 @@ func _find_spawner() -> void:
|
|||||||
var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
|
var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
|
||||||
if not spawners.is_empty():
|
if not spawners.is_empty():
|
||||||
_spawner = spawners[0]
|
_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:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
@@ -58,51 +38,13 @@ func _toggle_controls() -> void:
|
|||||||
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
|
_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:
|
func _on_reset_pressed() -> void:
|
||||||
if get_tree().paused:
|
if get_tree().paused:
|
||||||
get_tree().paused = false
|
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:
|
if _spawner:
|
||||||
_spawner.reset_spawn()
|
_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:
|
func _build_controls_panel() -> void:
|
||||||
_controls_panel = PanelContainer.new()
|
_controls_panel = PanelContainer.new()
|
||||||
|
|
||||||
@@ -144,58 +86,11 @@ func _build_controls_panel() -> void:
|
|||||||
_row(vbox, "R", "Respawn matadors (cooldown)", _MUTED)
|
_row(vbox, "R", "Respawn matadors (cooldown)", _MUTED)
|
||||||
_row(vbox, "H", "Toggle controls", _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 := HSeparator.new()
|
||||||
var sep_style := StyleBoxFlat.new()
|
var sep_style := StyleBoxFlat.new()
|
||||||
sep_style.bg_color = Color(0.65, 0.48, 0.15, 0.30)
|
sep_style.bg_color = Color(0.65, 0.48, 0.15, 0.30)
|
||||||
sep_style.content_margin_top = 2.0
|
sep_style.content_margin_top = 4.0
|
||||||
sep_style.content_margin_bottom = 2.0
|
sep_style.content_margin_bottom = 4.0
|
||||||
sep.add_theme_stylebox_override("separator", sep_style)
|
sep.add_theme_stylebox_override("separator", sep_style)
|
||||||
vbox.add_child(sep)
|
vbox.add_child(sep)
|
||||||
|
|
||||||
@@ -211,71 +106,6 @@ func _build_score_panel() -> void:
|
|||||||
reset_btn.pressed.connect(_on_reset_pressed)
|
reset_btn.pressed.connect(_on_reset_pressed)
|
||||||
hbox.add_child(reset_btn)
|
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:
|
func _make_button(label_text: String) -> Button:
|
||||||
var btn := Button.new()
|
var btn := Button.new()
|
||||||
|
|||||||
+133
-36
@@ -1,11 +1,13 @@
|
|||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
|
|
||||||
# WANDER → bull outside flee range, roaming freely
|
# WANDER → bull outside flee range, roaming freely
|
||||||
# FLEE → bull within range, matador backs away
|
# FLEE → bull within range, matador backs away (raycast-steered around walls)
|
||||||
# BRACE → charge detected: plant feet, face the bull (cite phase)
|
# BRACE → charge detected: plant feet, face the bull (cite phase)
|
||||||
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
|
# SIDESTEP→ bull commits: sharp lateral step to let it pass (pase phase)
|
||||||
# RAGDOLL → hit by bull at speed
|
# ATTACK → post-dodge or mid-range: chase and strike
|
||||||
enum State { WANDER, FLEE, BRACE, SIDESTEP, RAGDOLL }
|
# ROLL → hit by bull but roll chance fires: survive and recover
|
||||||
|
# RAGDOLL → hit by bull, no roll chance
|
||||||
|
enum State { WANDER, FLEE, BRACE, SIDESTEP, ATTACK, ROLL, RAGDOLL }
|
||||||
|
|
||||||
signal killed
|
signal killed
|
||||||
|
|
||||||
@@ -21,6 +23,11 @@ var _brace_timer: float = 0.0
|
|||||||
var _step_timer: float = 0.0
|
var _step_timer: float = 0.0
|
||||||
var _dodge_cd: float = 0.0
|
var _dodge_cd: float = 0.0
|
||||||
var _will_dodge: bool = true
|
var _will_dodge: bool = true
|
||||||
|
var _attack_timer: float = 0.0
|
||||||
|
var _roll_timer: float = 0.0
|
||||||
|
var _roll_dir: Vector3 = Vector3.ZERO
|
||||||
|
var _steer_dir: Vector3 = Vector3.ZERO
|
||||||
|
var _steer_cd: float = 0.0
|
||||||
var _ole_player: AudioStreamPlayer = null
|
var _ole_player: AudioStreamPlayer = null
|
||||||
var _death_player: AudioStreamPlayer = null
|
var _death_player: AudioStreamPlayer = null
|
||||||
var _blood_burst: CPUParticles3D = null
|
var _blood_burst: CPUParticles3D = null
|
||||||
@@ -34,6 +41,11 @@ const _ANIM_IDLE: StringName = &"Taunt"
|
|||||||
const _ANIM_ATTACK: StringName = &"Attack"
|
const _ANIM_ATTACK: StringName = &"Attack"
|
||||||
const _ANIM_ROLL: StringName = &"Roll"
|
const _ANIM_ROLL: StringName = &"Roll"
|
||||||
|
|
||||||
|
# Candidate steering offsets (radians) tried in order when a wall is ahead
|
||||||
|
const _STEER_ANGLES: Array = [0.0, -0.35, 0.35, -0.7, 0.7, -1.1, 1.1, PI]
|
||||||
|
const _STEER_LOOKAHEAD: float = 3.5 # longer = turns before hitting wall
|
||||||
|
const _STEER_INTERVAL: float = 0.12 # recompute direction at most ~8×/sec
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group(&"matador")
|
add_to_group(&"matador")
|
||||||
@@ -82,6 +94,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
State.FLEE: _tick_flee(delta)
|
State.FLEE: _tick_flee(delta)
|
||||||
State.BRACE: _tick_brace(delta)
|
State.BRACE: _tick_brace(delta)
|
||||||
State.SIDESTEP: _tick_sidestep(delta)
|
State.SIDESTEP: _tick_sidestep(delta)
|
||||||
|
State.ATTACK: _tick_attack(delta)
|
||||||
|
State.ROLL: _tick_roll(delta)
|
||||||
State.RAGDOLL: _tick_ragdoll(delta)
|
State.RAGDOLL: _tick_ragdoll(delta)
|
||||||
|
|
||||||
|
|
||||||
@@ -91,16 +105,25 @@ func _update_ai_state() -> void:
|
|||||||
State.WANDER:
|
State.WANDER:
|
||||||
if dist < DP.f("mat_flee_range"):
|
if dist < DP.f("mat_flee_range"):
|
||||||
_state = State.FLEE
|
_state = State.FLEE
|
||||||
|
elif _dodge_cd <= 0.0 and dist < DP.f("mat_attack_range") and not _is_charge_incoming():
|
||||||
|
_steer_cd = 0.0
|
||||||
|
_start_attack()
|
||||||
State.FLEE:
|
State.FLEE:
|
||||||
if _dodge_cd <= 0.0 and (dist < DP.f("mat_commit_dist") or _is_charge_incoming()):
|
if _dodge_cd <= 0.0 and (dist < DP.f("mat_commit_dist") or _is_charge_incoming()):
|
||||||
_start_brace()
|
_start_brace()
|
||||||
elif dist > DP.f("mat_flee_range") * 1.3:
|
elif dist > DP.f("mat_flee_range") * 1.3:
|
||||||
_state = State.WANDER
|
_state = State.WANDER
|
||||||
|
_steer_cd = 0.0
|
||||||
_pick_wander_target()
|
_pick_wander_target()
|
||||||
State.BRACE:
|
State.BRACE:
|
||||||
pass # brace timer drives transition
|
pass # brace timer drives transition
|
||||||
State.SIDESTEP:
|
State.SIDESTEP:
|
||||||
pass # step timer drives transition
|
pass # step timer drives transition
|
||||||
|
State.ATTACK:
|
||||||
|
if _attack_timer <= 0.0 or dist > DP.f("mat_attack_range") * 1.5:
|
||||||
|
_state = State.FLEE
|
||||||
|
State.ROLL:
|
||||||
|
pass # roll timer drives transition
|
||||||
|
|
||||||
|
|
||||||
# Bull is moving fast and aimed within ~30° of the matador
|
# Bull is moving fast and aimed within ~30° of the matador
|
||||||
@@ -115,6 +138,37 @@ func _is_charge_incoming() -> bool:
|
|||||||
return bull_flat.normalized().dot(to_me.normalized()) > 0.85
|
return bull_flat.normalized().dot(to_me.normalized()) > 0.85
|
||||||
|
|
||||||
|
|
||||||
|
# Returns a wall-clear direction, recomputed at most every _STEER_INTERVAL seconds.
|
||||||
|
# Randomises among available clear angles so juke direction is unpredictable.
|
||||||
|
func _steer_clear(desired_dir: Vector3, delta: float) -> Vector3:
|
||||||
|
_steer_cd -= delta
|
||||||
|
if _steer_cd > 0.0 and _steer_dir != Vector3.ZERO:
|
||||||
|
return _steer_dir
|
||||||
|
_steer_cd = _STEER_INTERVAL
|
||||||
|
var space := get_world_3d().direct_space_state
|
||||||
|
var origin := global_position + Vector3.UP * 0.6
|
||||||
|
var params := PhysicsRayQueryParameters3D.new()
|
||||||
|
params.exclude = [get_rid()]
|
||||||
|
params.collision_mask = 1
|
||||||
|
# Try straight ahead first; if blocked, pick randomly from the side candidates.
|
||||||
|
params.from = origin
|
||||||
|
params.to = origin + desired_dir * _STEER_LOOKAHEAD
|
||||||
|
if space.intersect_ray(params).is_empty():
|
||||||
|
_steer_dir = desired_dir
|
||||||
|
return desired_dir
|
||||||
|
var candidates: Array = [-0.35, 0.35, -0.7, 0.7, -1.1, 1.1, PI]
|
||||||
|
candidates.shuffle()
|
||||||
|
for angle: float in candidates:
|
||||||
|
var dir := desired_dir.rotated(Vector3.UP, angle)
|
||||||
|
params.from = origin
|
||||||
|
params.to = origin + dir * _STEER_LOOKAHEAD
|
||||||
|
if space.intersect_ray(params).is_empty():
|
||||||
|
_steer_dir = dir
|
||||||
|
return dir
|
||||||
|
_steer_dir = desired_dir
|
||||||
|
return desired_dir
|
||||||
|
|
||||||
|
|
||||||
func _tick_wander(delta: float) -> void:
|
func _tick_wander(delta: float) -> void:
|
||||||
if _idle_timer > 0.0:
|
if _idle_timer > 0.0:
|
||||||
_idle_timer -= delta
|
_idle_timer -= delta
|
||||||
@@ -130,10 +184,10 @@ func _tick_wander(delta: float) -> void:
|
|||||||
_idle_timer = randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
|
_idle_timer = randf_range(DP.f("mat_idle_min"), DP.f("mat_idle_max"))
|
||||||
_pick_wander_target()
|
_pick_wander_target()
|
||||||
else:
|
else:
|
||||||
var dir := Vector3(dx, 0.0, dz).normalized()
|
var dir := _steer_clear(Vector3(dx, 0.0, dz).normalized(), delta)
|
||||||
var spd := DP.f("mat_walk_speed")
|
var spd := DP.f("mat_walk_speed")
|
||||||
velocity.x = dir.x * spd
|
velocity.x = move_toward(velocity.x, dir.x * spd, spd * 8.0 * delta)
|
||||||
velocity.z = dir.z * spd
|
velocity.z = move_toward(velocity.z, dir.z * spd, spd * 8.0 * delta)
|
||||||
_mesh.rotation.y = lerp_angle(
|
_mesh.rotation.y = lerp_angle(
|
||||||
_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
||||||
_play_anim(_ANIM_RUN)
|
_play_anim(_ANIM_RUN)
|
||||||
@@ -145,10 +199,10 @@ func _tick_flee(delta: float) -> void:
|
|||||||
away.y = 0.0
|
away.y = 0.0
|
||||||
if away.length() < 0.01:
|
if away.length() < 0.01:
|
||||||
away = Vector3(randf() - 0.5, 0.0, randf() - 0.5)
|
away = Vector3(randf() - 0.5, 0.0, randf() - 0.5)
|
||||||
away = away.normalized()
|
var dir := _steer_clear(away.normalized(), delta)
|
||||||
var spd := DP.f("mat_flee_speed")
|
var spd := DP.f("mat_flee_speed")
|
||||||
velocity.x = away.x * spd
|
velocity.x = move_toward(velocity.x, dir.x * spd, spd * 8.0 * delta)
|
||||||
velocity.z = away.z * spd
|
velocity.z = move_toward(velocity.z, dir.z * spd, spd * 8.0 * delta)
|
||||||
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 8.0)
|
||||||
_play_anim(_ANIM_RUN)
|
_play_anim(_ANIM_RUN)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
@@ -156,9 +210,9 @@ func _tick_flee(delta: float) -> void:
|
|||||||
|
|
||||||
# Cite phase: plant feet, face the bull — wait for it to commit
|
# Cite phase: plant feet, face the bull — wait for it to commit
|
||||||
func _start_brace() -> void:
|
func _start_brace() -> void:
|
||||||
|
_steer_cd = 0.0
|
||||||
_state = State.BRACE
|
_state = State.BRACE
|
||||||
var bull_flat := Vector3(_bull.velocity.x, 0.0, _bull.velocity.z)
|
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
|
var charge_dir: Vector3
|
||||||
if bull_flat.length() > 1.0:
|
if bull_flat.length() > 1.0:
|
||||||
charge_dir = bull_flat.normalized()
|
charge_dir = bull_flat.normalized()
|
||||||
@@ -166,7 +220,6 @@ func _start_brace() -> void:
|
|||||||
charge_dir = (_bull.global_position - global_position)
|
charge_dir = (_bull.global_position - global_position)
|
||||||
charge_dir.y = 0.0
|
charge_dir.y = 0.0
|
||||||
charge_dir = charge_dir.normalized()
|
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")
|
_will_dodge = randf() < DP.f("mat_dodge_chance")
|
||||||
var side := 1.0 if randf() > 0.5 else -1.0
|
var side := 1.0 if randf() > 0.5 else -1.0
|
||||||
_step_dir = charge_dir.rotated(Vector3.UP, PI * 0.5 * side)
|
_step_dir = charge_dir.rotated(Vector3.UP, PI * 0.5 * side)
|
||||||
@@ -174,10 +227,8 @@ func _start_brace() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _tick_brace(delta: float) -> void:
|
func _tick_brace(delta: float) -> void:
|
||||||
# Decelerate to a stop
|
|
||||||
velocity.x = move_toward(velocity.x, 0.0, 18.0 * delta)
|
velocity.x = move_toward(velocity.x, 0.0, 18.0 * delta)
|
||||||
velocity.z = move_toward(velocity.z, 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)
|
var to_bull := (_bull.global_position - global_position)
|
||||||
to_bull.y = 0.0
|
to_bull.y = 0.0
|
||||||
if to_bull.length() > 0.1:
|
if to_bull.length() > 0.1:
|
||||||
@@ -188,7 +239,6 @@ func _tick_brace(delta: float) -> void:
|
|||||||
|
|
||||||
_brace_timer -= delta
|
_brace_timer -= delta
|
||||||
var dist := global_position.distance_to(_bull.global_position)
|
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 dist < DP.f("mat_commit_dist") or _brace_timer <= 0.0:
|
||||||
if _will_dodge:
|
if _will_dodge:
|
||||||
_state = State.SIDESTEP
|
_state = State.SIDESTEP
|
||||||
@@ -197,22 +247,70 @@ func _tick_brace(delta: float) -> void:
|
|||||||
_ole_player.pitch_scale = randf_range(0.88, 1.12)
|
_ole_player.pitch_scale = randf_range(0.88, 1.12)
|
||||||
_ole_player.play()
|
_ole_player.play()
|
||||||
else:
|
else:
|
||||||
# Stand and eat it — no juke
|
|
||||||
_state = State.FLEE
|
_state = State.FLEE
|
||||||
_dodge_cd = DP.f("mat_dodge_cooldown")
|
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||||
|
|
||||||
|
|
||||||
# Pase phase: sharp lateral step as the bull commits to its line
|
# Pase phase: sharp lateral step, then immediately exploit with an attack
|
||||||
func _tick_sidestep(delta: float) -> void:
|
func _tick_sidestep(delta: float) -> void:
|
||||||
_step_timer -= delta
|
_step_timer -= delta
|
||||||
if _step_timer <= 0.0:
|
if _step_timer <= 0.0:
|
||||||
_state = State.FLEE
|
_start_attack()
|
||||||
_dodge_cd = DP.f("mat_dodge_cooldown")
|
|
||||||
return
|
return
|
||||||
var spd := DP.f("mat_step_speed")
|
var spd := DP.f("mat_step_speed")
|
||||||
velocity.x = _step_dir.x * spd
|
velocity.x = _step_dir.x * spd
|
||||||
velocity.z = _step_dir.z * 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(_ANIM_ROLL)
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
|
func _start_attack() -> void:
|
||||||
|
_state = State.ATTACK
|
||||||
|
_attack_timer = DP.f("mat_attack_duration")
|
||||||
|
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||||
|
|
||||||
|
|
||||||
|
func _tick_attack(delta: float) -> void:
|
||||||
|
_attack_timer -= delta
|
||||||
|
var to_bull := (_bull.global_position - global_position)
|
||||||
|
to_bull.y = 0.0
|
||||||
|
var dist := to_bull.length()
|
||||||
|
|
||||||
|
if dist > 0.1:
|
||||||
|
_mesh.rotation.y = lerp_angle(
|
||||||
|
_mesh.rotation.y, atan2(to_bull.x, to_bull.z), delta * 10.0)
|
||||||
|
|
||||||
|
if dist > DP.f("mat_attack_min_dist"):
|
||||||
|
var dir := _steer_clear(to_bull.normalized(), delta)
|
||||||
|
var spd := DP.f("mat_attack_speed")
|
||||||
|
velocity.x = move_toward(velocity.x, dir.x * spd, spd * 8.0 * delta)
|
||||||
|
velocity.z = move_toward(velocity.z, dir.z * spd, spd * 8.0 * delta)
|
||||||
|
else:
|
||||||
|
velocity.x = move_toward(velocity.x, 0.0, 14.0 * delta)
|
||||||
|
velocity.z = move_toward(velocity.z, 0.0, 14.0 * delta)
|
||||||
|
|
||||||
|
_play_anim(_ANIM_ATTACK)
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_roll(hit_dir: Vector3) -> void:
|
||||||
|
_state = State.ROLL
|
||||||
|
_roll_timer = DP.f("mat_roll_duration")
|
||||||
|
hit_dir.y = 0.0
|
||||||
|
var side := 1.0 if randf() > 0.5 else -1.0
|
||||||
|
_roll_dir = hit_dir.normalized().rotated(Vector3.UP, PI * 0.5 * side)
|
||||||
|
|
||||||
|
|
||||||
|
func _tick_roll(delta: float) -> void:
|
||||||
|
_roll_timer -= delta
|
||||||
|
if _roll_timer <= 0.0:
|
||||||
|
_state = State.FLEE
|
||||||
|
_dodge_cd = DP.f("mat_dodge_cooldown")
|
||||||
|
return
|
||||||
|
var spd := DP.f("mat_step_speed")
|
||||||
|
velocity.x = _roll_dir.x * spd
|
||||||
|
velocity.z = _roll_dir.z * spd
|
||||||
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 20.0)
|
_mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(velocity.x, velocity.z), delta * 20.0)
|
||||||
_play_anim(_ANIM_ROLL)
|
_play_anim(_ANIM_ROLL)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
@@ -225,7 +323,7 @@ func _tick_ragdoll(_delta: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_body_entered(body: Node3D) -> void:
|
func _on_body_entered(body: Node3D) -> void:
|
||||||
if _state == State.RAGDOLL:
|
if _state == State.RAGDOLL or _state == State.ROLL:
|
||||||
return
|
return
|
||||||
if not body.is_in_group(&"player"):
|
if not body.is_in_group(&"player"):
|
||||||
return
|
return
|
||||||
@@ -235,20 +333,23 @@ func _on_body_entered(body: Node3D) -> void:
|
|||||||
var flat_vel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
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 \
|
var hit_dir := flat_vel.normalized() if flat_vel.length() > 0.5 else \
|
||||||
(global_position - player.global_position).normalized()
|
(global_position - player.global_position).normalized()
|
||||||
|
|
||||||
|
if randf() < DP.f("mat_roll_chance"):
|
||||||
|
_enter_roll(hit_dir)
|
||||||
|
else:
|
||||||
_enter_ragdoll(hit_dir, player.velocity.length())
|
_enter_ragdoll(hit_dir, player.velocity.length())
|
||||||
|
|
||||||
|
|
||||||
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
||||||
_state = State.RAGDOLL
|
_state = State.RAGDOLL
|
||||||
killed.emit()
|
killed.emit()
|
||||||
var _t := get_tree().create_timer(4.0)
|
var cleanup_timer := get_tree().create_timer(4.0)
|
||||||
_t.timeout.connect(func() -> void:
|
cleanup_timer.timeout.connect(func() -> void:
|
||||||
if is_instance_valid(self):
|
if is_instance_valid(self):
|
||||||
queue_free()
|
queue_free()
|
||||||
)
|
)
|
||||||
hit_dir.y = 0.0
|
hit_dir.y = 0.0
|
||||||
hit_dir = hit_dir.normalized()
|
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()
|
var throw_dir := (hit_dir + Vector3(0.0, 0.5, 0.0)).normalized()
|
||||||
|
|
||||||
_spawn_blood_burst(hit_dir)
|
_spawn_blood_burst(hit_dir)
|
||||||
@@ -260,22 +361,14 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
|||||||
if cam:
|
if cam:
|
||||||
cam.call(&"trigger_hit", clampf(bull_speed / 15.0, 0.4, 1.0))
|
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
|
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
|
collision_layer = 0
|
||||||
|
|
||||||
if _anim_player:
|
if _anim_player:
|
||||||
_anim_player.pause() # pause keeps current frame; stop() resets to T-pose
|
_anim_player.pause()
|
||||||
|
|
||||||
if not _sim:
|
if not _sim:
|
||||||
return
|
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():
|
for child: Node in _sim.get_children():
|
||||||
if not (child is PhysicalBone3D):
|
if not (child is PhysicalBone3D):
|
||||||
continue
|
continue
|
||||||
@@ -294,7 +387,6 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
|||||||
var impulse := Vector3.ZERO
|
var impulse := Vector3.ZERO
|
||||||
match pb.bone_name:
|
match pb.bone_name:
|
||||||
"COG", "matador":
|
"COG", "matador":
|
||||||
# Core of mass takes the full hit
|
|
||||||
impulse = throw_dir * strength
|
impulse = throw_dir * strength
|
||||||
"chest":
|
"chest":
|
||||||
impulse = (throw_dir + Vector3.UP * 0.3).normalized() * strength * 0.9
|
impulse = (throw_dir + Vector3.UP * 0.3).normalized() * strength * 0.9
|
||||||
@@ -309,10 +401,7 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
|
|||||||
"arm_R":
|
"arm_R":
|
||||||
impulse = (throw_dir * 0.3 - right * 0.8 + Vector3.UP * 0.5).normalized() * strength * 0.5
|
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
|
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))
|
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)
|
pb.apply_central_impulse(impulse + noise * strength * 0.2)
|
||||||
|
|
||||||
@@ -385,6 +474,14 @@ func _play_anim(anim_name: StringName) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _pick_wander_target() -> void:
|
func _pick_wander_target() -> void:
|
||||||
|
# 35% chance: pick a point orbiting close to the bull for a daring pass
|
||||||
|
if _bull != null and randf() < 0.35:
|
||||||
|
var angle := randf() * TAU
|
||||||
|
var r := randf_range(1.5, 4.0)
|
||||||
|
var bull_flat := _bull.global_position
|
||||||
|
bull_flat.y = 0.0
|
||||||
|
_wander_target = bull_flat + Vector3(cos(angle) * r, 0.0, sin(angle) * r)
|
||||||
|
return
|
||||||
var radius := DP.f("mat_wander_radius")
|
var radius := DP.f("mat_wander_radius")
|
||||||
var angle := randf() * TAU
|
var angle := randf() * TAU
|
||||||
var dist := randf_range(2.0, radius)
|
var dist := randf_range(2.0, radius)
|
||||||
|
|||||||
Reference in New Issue
Block a user