Hit effect
This commit is contained in:
+42
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user