Hit effect

This commit is contained in:
2026-05-25 20:56:08 +03:00
parent 6abf5355c2
commit d98a5ba0ac
3 changed files with 130 additions and 1 deletions
+42 -1
View File
@@ -5,12 +5,33 @@ extends Camera3D
@export var horizontal_angle_deg: float = 45.0
@export var ortho_size: float = 15.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
func _ready() -> void:
set_as_top_level(true)
projection = PROJECTION_ORTHOGONAL
size = ortho_size
_setup_flash()
func _process(_delta: float) -> void:
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 _process(delta: float) -> void:
var angle_rad := deg_to_rad(horizontal_angle_deg)
var offset := Vector3(
sin(angle_rad) * horizontal_distance,
@@ -20,3 +41,23 @@ func _process(_delta: float) -> void:
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
+4
View File
@@ -249,6 +249,10 @@ func _enter_ragdoll(hit_dir: Vector3, bull_speed: float) -> void:
_spawn_blood_burst(hit_dir)
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
+84
View File
@@ -7,6 +7,7 @@ const HOOF_OFFSETS: Array = [
Vector3(-0.30, -0.25, 0.60),
Vector3( 0.30, -0.25, 0.60),
]
const SNOUT_OFFSET: Vector3 = Vector3(0.0, 0.5, -1.0)
@onready var camera_pivot: Node3D = $Camera3D
@onready var cube_guy: Node3D = $bull_v10
@@ -21,6 +22,10 @@ var _charge_dir: Vector3 = Vector3.ZERO
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 _snort_emitter: CPUParticles3D = null
var _charge_ready_emitter: CPUParticles3D = null
enum DustState { NONE, WALK, CHARGE }
var _dust_state: DustState = DustState.NONE
@@ -30,6 +35,7 @@ func _ready() -> void:
add_to_group(&"player")
_setup_hoof_dust()
_setup_legs()
_setup_charge_indicators()
DP.any_changed.connect(_on_dp_changed)
@@ -86,6 +92,73 @@ func _setup_hoof_dust() -> void:
_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
# Snort steam from nose during charge-ahead (both buttons held)
var snort_ramp := Gradient.new()
snort_ramp.set_color(0, Color(0.95, 0.95, 0.95, 0.9))
snort_ramp.set_color(1, Color(0.80, 0.80, 0.80, 0.0))
_snort_emitter = CPUParticles3D.new()
_snort_emitter.position = SNOUT_OFFSET
_snort_emitter.emitting = false
_snort_emitter.amount = 6
_snort_emitter.lifetime = 0.35
_snort_emitter.explosiveness = 0.0
_snort_emitter.randomness = 0.5
_snort_emitter.local_coords = true
_snort_emitter.direction = Vector3(0.0, 0.2, -1.0).normalized()
_snort_emitter.spread = 25.0
_snort_emitter.initial_velocity_min = 2.0
_snort_emitter.initial_velocity_max = 4.0
_snort_emitter.scale_amount_min = 0.3
_snort_emitter.scale_amount_max = 0.7
_snort_emitter.mesh = sphere
_snort_emitter.color_ramp = snort_ramp
cube_guy.add_child(_snort_emitter)
# 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 _set_dust_state(new_state: DustState) -> void:
if new_state == _dust_state:
return
@@ -225,6 +298,17 @@ func _physics_process(delta: float) -> void:
_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
# Snort while charge-ahead is active
if _snort_emitter.emitting != both_held:
_snort_emitter.emitting = both_held
_both_held = both_held
# Reset kick timer on landing so the first kick after a bounce is immediate