Release 0.2.0: device-aware controls, spear, settings, shaders
Show touch controls only on touch platforms and keyboard hints only on desktop, via a shared Controls.use_touch_ui() gate (is_touchscreen_available is unreliable with emulate_touch_from_mouse). Bumps version to 0.2.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,16 +27,23 @@ var _pre_slide_vel_y: float = 0.0
|
||||
|
||||
var _slam_ring_mesh: ArrayMesh = null
|
||||
|
||||
# Ability system — kick=0, slam=1, dash=2, roll=3
|
||||
# Ability system — kick=0, dash=1, slam=2, roll=3
|
||||
var ability_cd: Array[float] = [0.0, 0.0, 0.0, 0.0]
|
||||
var _ability_active: bool = false
|
||||
var _ability_timer: float = 0.0
|
||||
var _active_ability: int = -1
|
||||
var _dash_dir: Vector3 = Vector3.ZERO
|
||||
var _dash_speed_cur: float = 0.0
|
||||
var _roll_dir: Vector3 = Vector3.ZERO
|
||||
var _roll_ramp: float = 0.0
|
||||
var _roll_spin: float = 0.0
|
||||
var _roll_target: Node3D = null
|
||||
var _roll_chains_left: int = 0
|
||||
var _roll_hit: Array[Node] = []
|
||||
var _bull_anim: AnimationPlayer = null
|
||||
# Locomotion animation state: true while the looping DASH clip is driving WASD
|
||||
# movement, false while idling. Flipping it cross-fades between the two clips.
|
||||
var _locomotion_moving: bool = false
|
||||
|
||||
var _slam_pending: bool = false
|
||||
var _charge_trail_emitter: CPUParticles3D = null
|
||||
@@ -49,6 +56,17 @@ var _cube_scale: Vector3 = Vector3.ONE
|
||||
signal died(cause: String)
|
||||
var _dead: bool = false
|
||||
|
||||
# HP system — the bull soaks several clean hits instead of dying to the first.
|
||||
# Each landed blade drops one pip and starts an i-frame window (so one pass can't
|
||||
# empty the bar); at 0 the run ends via `died`. The HUD listens on `health_changed`.
|
||||
signal health_changed(current: int, max_hp: int)
|
||||
# Fired on every landed hit (after i-frames pass) so the HUD can flash the damage
|
||||
# vignette; `cause` mirrors take_sword_hit ("gored" / "thrown").
|
||||
signal hit_taken(cause: String)
|
||||
var _max_hp: int = 10
|
||||
var _hp: int = 10
|
||||
var _hit_iframes: float = 0.0
|
||||
|
||||
var _huff_player: AudioStreamPlayer = null
|
||||
var _crowd_player: AudioStreamPlayer = null
|
||||
var _huff_timer: float = 0.0
|
||||
@@ -61,6 +79,8 @@ var _charge_dust_ramp: Gradient = null
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group(&"player")
|
||||
_max_hp = maxi(1, int(DP.f("bull_max_hp")))
|
||||
_hp = _max_hp
|
||||
_cube_scale = cube_guy.scale
|
||||
_setup_hoof_dust()
|
||||
_setup_legs()
|
||||
@@ -258,8 +278,9 @@ func _find_anim_player(node: Node) -> AnimationPlayer:
|
||||
func _setup_bull_idle() -> void:
|
||||
if _bull_anim == null:
|
||||
return
|
||||
# IDLE and ROLL both loop as continuous states; the ability clips are one-shot.
|
||||
for clip: StringName in [_ANIM_IDLE, _ANIM_ROLL]:
|
||||
# IDLE, DASH and ROLL loop as continuous states (DASH doubles as the WASD
|
||||
# locomotion cycle); the remaining ability clips are one-shot.
|
||||
for clip: StringName in [_ANIM_IDLE, _ANIM_DASH, _ANIM_ROLL]:
|
||||
if _bull_anim.has_animation(clip):
|
||||
_bull_anim.get_animation(clip).loop_mode = Animation.LOOP_LINEAR
|
||||
_play_idle()
|
||||
@@ -268,6 +289,7 @@ func _setup_bull_idle() -> void:
|
||||
# Return to the looping idle (and undo any ability speed-scale). Cross-fades so a
|
||||
# finishing ability clip eases back to idle instead of snapping.
|
||||
func _play_idle() -> void:
|
||||
_locomotion_moving = false
|
||||
if _bull_anim == null:
|
||||
return
|
||||
_bull_anim.speed_scale = 1.0
|
||||
@@ -275,25 +297,56 @@ func _play_idle() -> void:
|
||||
_bull_anim.play(_ANIM_IDLE, 0.2)
|
||||
|
||||
|
||||
# Drive the WASD locomotion clip: the looping DASH doubles as the run cycle while a
|
||||
# direction is held, easing back to idle when the bull coasts to a stop. Cross-fades
|
||||
# on each transition so idle↔run (and steering into it after an ability) blend rather
|
||||
# than snap. Abilities own the animation while active, so this yields to them.
|
||||
func _update_locomotion_anim(moving: bool) -> void:
|
||||
if _ability_active or _bull_anim == null or moving == _locomotion_moving:
|
||||
return
|
||||
_locomotion_moving = moving
|
||||
if moving:
|
||||
if _bull_anim.has_animation(_ANIM_DASH):
|
||||
_bull_anim.speed_scale = 1.0
|
||||
_bull_anim.play(_ANIM_DASH, 0.25)
|
||||
else:
|
||||
_play_idle()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if _dead:
|
||||
return
|
||||
# Abilities can be chained: a new one interrupts whatever is currently
|
||||
# active, gated only by each ability's own cooldown.
|
||||
if event.is_action_pressed(&"ability_kick") and ability_cd[0] <= 0.0:
|
||||
_activate_kick()
|
||||
elif event.is_action_pressed(&"ability_slam") and ability_cd[1] <= 0.0:
|
||||
_activate_slam()
|
||||
elif event.is_action_pressed(&"ability_dash") and ability_cd[2] <= 0.0:
|
||||
_activate_dash()
|
||||
elif event.is_action_pressed(&"ability_roll") and ability_cd[3] <= 0.0:
|
||||
_activate_roll()
|
||||
if event.is_action_pressed(&"ability_kick"):
|
||||
try_activate_ability(0)
|
||||
elif event.is_action_pressed(&"ability_dash"):
|
||||
try_activate_ability(1)
|
||||
elif event.is_action_pressed(&"ability_slam"):
|
||||
try_activate_ability(2)
|
||||
elif event.is_action_pressed(&"ability_roll"):
|
||||
try_activate_ability(3)
|
||||
|
||||
|
||||
# Public ability entry point shared by keyboard/gamepad input and the on-screen
|
||||
# touch buttons. Slot order matches ability_cd — kick=0, dash=1, slam=2, roll=3.
|
||||
# No-ops if the bull is dead or the slot is still cooling down.
|
||||
func try_activate_ability(slot: int) -> void:
|
||||
if _dead or slot < 0 or slot >= ability_cd.size() or ability_cd[slot] > 0.0:
|
||||
return
|
||||
match slot:
|
||||
0: _activate_kick()
|
||||
1: _activate_dash()
|
||||
2: _activate_slam()
|
||||
3: _activate_roll()
|
||||
|
||||
|
||||
func ability_cooldown_fraction(slot: int) -> float:
|
||||
var maxcd := 0.0
|
||||
match slot:
|
||||
0: maxcd = DP.f("kick_cooldown")
|
||||
1: maxcd = DP.f("slam_cooldown")
|
||||
2: maxcd = DP.f("dash_cooldown")
|
||||
1: maxcd = DP.f("dash_cooldown")
|
||||
2: maxcd = DP.f("slam_cooldown")
|
||||
3: maxcd = DP.f("roll_cooldown")
|
||||
return ability_cd[slot] / maxcd if maxcd > 0.0 else 0.0
|
||||
|
||||
@@ -306,9 +359,9 @@ func _activate_kick() -> void:
|
||||
ability_cd[0] = DP.f("kick_cooldown")
|
||||
_ability_active = true
|
||||
_active_ability = 0
|
||||
_ability_timer = 0.6
|
||||
_ability_timer = 0.3
|
||||
if _bull_anim:
|
||||
_bull_anim.speed_scale = 1.0
|
||||
_bull_anim.speed_scale = 2.0
|
||||
if _bull_anim and _bull_anim.has_animation(_ANIM_KICK):
|
||||
_bull_anim.play(_ANIM_KICK)
|
||||
_ability_timer = _bull_anim.get_animation(_ANIM_KICK).length
|
||||
@@ -321,11 +374,12 @@ func _activate_kick() -> void:
|
||||
|
||||
|
||||
func _activate_slam() -> void:
|
||||
ability_cd[1] = DP.f("slam_cooldown")
|
||||
ability_cd[2] = DP.f("slam_cooldown")
|
||||
_ability_active = true
|
||||
_active_ability = 1
|
||||
_active_ability = 2
|
||||
_legs.set(&"tail_ragdoll", true)
|
||||
_slam_pending = true
|
||||
SpearProjectile.shed_all.call_deferred(self) # the leap shakes off any stuck spears
|
||||
var jump_vel := DP.f("slam_jump_velocity")
|
||||
velocity.y = jump_vel
|
||||
# Airtime with an accelerated descent: rise (v/g) + fall (v / (g·√mult)).
|
||||
@@ -339,9 +393,9 @@ func _activate_slam() -> void:
|
||||
|
||||
|
||||
func _activate_dash() -> void:
|
||||
ability_cd[2] = DP.f("dash_cooldown")
|
||||
ability_cd[1] = DP.f("dash_cooldown")
|
||||
_ability_active = true
|
||||
_active_ability = 2
|
||||
_active_ability = 1
|
||||
_ability_timer = 0.45
|
||||
if _bull_anim:
|
||||
_bull_anim.speed_scale = 1.0
|
||||
@@ -350,8 +404,9 @@ func _activate_dash() -> void:
|
||||
_ability_timer = _bull_anim.get_animation(_ANIM_DASH).length
|
||||
var raw := cube_guy.global_transform.basis.z
|
||||
_dash_dir = Vector3(raw.x, 0.0, raw.z).normalized()
|
||||
velocity.x = _dash_dir.x * DP.f("dash_speed")
|
||||
velocity.z = _dash_dir.z * DP.f("dash_speed")
|
||||
_dash_speed_cur = DP.f("dash_speed")
|
||||
velocity.x = _dash_dir.x * _dash_speed_cur
|
||||
velocity.z = _dash_dir.z * _dash_speed_cur
|
||||
_spawn_dash_burst()
|
||||
_legs.set(&"charge_pitch_target", DP.f("charge_head_pitch"))
|
||||
if _huff_player:
|
||||
@@ -366,10 +421,16 @@ func _activate_roll() -> void:
|
||||
_ability_timer = DP.f("roll_duration")
|
||||
_roll_ramp = 0.0
|
||||
_roll_spin = 0.0
|
||||
# Curl into the ball along the current heading (or facing when standing still)
|
||||
# and launch at the base speed — it ramps up from here the longer you roll.
|
||||
_roll_chains_left = int(DP.f("roll_chain_count"))
|
||||
_roll_hit.clear()
|
||||
# Lock onto the closest matador and curl into the ball aimed at it; if the arena
|
||||
# is empty, launch along the current heading (or facing when standing still).
|
||||
_roll_target = _roll_pick_target()
|
||||
var flat := Vector3(velocity.x, 0.0, velocity.z)
|
||||
if flat.length() > 1.0:
|
||||
if _roll_target != null:
|
||||
var to_t := _roll_target.global_position - global_position
|
||||
_roll_dir = Vector3(to_t.x, 0.0, to_t.z).normalized()
|
||||
elif flat.length() > 1.0:
|
||||
_roll_dir = flat.normalized()
|
||||
else:
|
||||
var f := cube_guy.global_transform.basis.z
|
||||
@@ -392,14 +453,20 @@ func _activate_roll() -> void:
|
||||
_huff_player.play()
|
||||
|
||||
|
||||
# One rolling frame (Rammus Powerball): steer with momentum, accelerate the longer
|
||||
# the roll lasts, move (walls just slide), then pop the ball on the first matador we
|
||||
# ram. Fully replaces the normal locomotion path while active.
|
||||
func _tick_roll(delta: float, steer: Vector3) -> void:
|
||||
var steer_flat := Vector3(steer.x, 0.0, steer.z)
|
||||
if steer_flat.length() > 0.1:
|
||||
var t := clampf(DP.f("roll_turn") * delta, 0.0, 1.0)
|
||||
_roll_dir = _roll_dir.slerp(steer_flat.normalized(), t).normalized()
|
||||
# One rolling frame: home on the current target matador, accelerate the longer the
|
||||
# roll lasts, move (walls just slide), then pop the ball on contact and chain to the
|
||||
# next closest. Fully replaces the normal locomotion path while active. `steer` is
|
||||
# ignored — the roll auto-targets — but kept so the caller stays uniform.
|
||||
func _tick_roll(delta: float, _steer: Vector3) -> void:
|
||||
# Reacquire if the target ragdolled or was freed before we reached it.
|
||||
if not _roll_target_valid():
|
||||
_roll_target = _roll_pick_target()
|
||||
if _roll_target != null:
|
||||
var to_t := _roll_target.global_position - global_position
|
||||
var flat := Vector3(to_t.x, 0.0, to_t.z)
|
||||
if flat.length() > 0.1:
|
||||
var t := clampf(DP.f("roll_turn") * delta, 0.0, 1.0)
|
||||
_roll_dir = _roll_dir.slerp(flat.normalized(), t).normalized()
|
||||
|
||||
# Speed builds over time: base → max across roll_rampup_time, so a long roll
|
||||
# winds up into a runaway boulder while a quick tap barely gets going.
|
||||
@@ -431,25 +498,64 @@ func _tick_roll(delta: float, steer: Vector3) -> void:
|
||||
|
||||
|
||||
# Pop the ball on ramming a matador: launch every matador in the hit radius up and
|
||||
# outward, then end the roll (Rammus stops when Powerball connects). Returns true if
|
||||
# it popped so the caller can stop touching roll state this frame.
|
||||
# outward, then either CHAIN to the next closest (if chains remain and a target is
|
||||
# left) or end the roll. Returns true if it popped so the caller can stop touching
|
||||
# roll state this frame.
|
||||
func _roll_try_pop(speed: float) -> bool:
|
||||
var radius := DP.f("roll_hit_radius")
|
||||
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
if _roll_hit.has(mat) or not _mat_active(mat):
|
||||
continue
|
||||
var to_mat: Vector3 = (mat as Node3D).global_position - global_position
|
||||
to_mat.y = 0.0
|
||||
if to_mat.length() > radius:
|
||||
continue
|
||||
# Record everyone this pop reaches so the chain never re-picks a downed body.
|
||||
for m: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
var d: Vector3 = (m as Node3D).global_position - global_position
|
||||
d.y = 0.0
|
||||
if d.length() <= radius and not _roll_hit.has(m):
|
||||
_roll_hit.append(m)
|
||||
_hit_matadors_radius(radius, DP.f("roll_hit_strength"), DP.f("roll_knockup"))
|
||||
camera_pivot.call(&"trigger_hit", clampf(speed / 90.0, 0.35, 1.0))
|
||||
if _huff_player:
|
||||
_huff_player.pitch_scale = randf_range(1.05, 1.35)
|
||||
_huff_player.play()
|
||||
if _roll_chains_left > 0:
|
||||
var next := _roll_pick_target()
|
||||
if next != null:
|
||||
_roll_chains_left -= 1
|
||||
_roll_target = next
|
||||
return true # keep rolling — the ball carries on to the next target
|
||||
_end_roll()
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# Closest active matador we haven't already popped this roll, or null if none left.
|
||||
func _roll_pick_target() -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_d := INF
|
||||
for mat: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
if _roll_hit.has(mat) or not _mat_active(mat):
|
||||
continue
|
||||
var d: float = (mat as Node3D).global_position.distance_squared_to(global_position)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = mat as Node3D
|
||||
return best
|
||||
|
||||
|
||||
func _roll_target_valid() -> bool:
|
||||
return is_instance_valid(_roll_target) and _mat_active(_roll_target)
|
||||
|
||||
|
||||
# A matador still in play — valid, not ragdolled/consumed. Ragdolled bodies report
|
||||
# is_active() == false, so a popped target is skipped on the next chain search.
|
||||
func _mat_active(mat: Node) -> bool:
|
||||
return is_instance_valid(mat) and mat.has_method(&"is_active") and mat.call(&"is_active")
|
||||
|
||||
|
||||
# End the Powerball early: brake hard (the ball "pops" and unrolls) and drop back to
|
||||
# the normal locomotion path next frame.
|
||||
func _end_roll() -> void:
|
||||
@@ -457,6 +563,9 @@ func _end_roll() -> void:
|
||||
_active_ability = -1
|
||||
_ability_timer = 0.0
|
||||
_roll_ramp = 0.0
|
||||
_roll_target = null
|
||||
_roll_chains_left = 0
|
||||
_roll_hit.clear()
|
||||
_legs.set(&"charge_pitch_target", 0.0)
|
||||
_legs.set(&"tail_ragdoll", false)
|
||||
_legs.set(&"external_rotation", false)
|
||||
@@ -805,6 +914,19 @@ func _make_slam_ring_mesh() -> ArrayMesh:
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# ── Dead bull ─────────────────────────────────────────────────────────────
|
||||
# The run is over the instant HP hits zero, but the lose screen holds off a
|
||||
# beat before it appears (see HUD._show_game_over). Freeze the carcass through
|
||||
# that delay — no input, no abilities, just coast to a stop under gravity — so a
|
||||
# dead bull can't keep charging around while the screen is pending.
|
||||
if _dead:
|
||||
velocity.x = move_toward(velocity.x, 0.0, 40.0 * delta)
|
||||
velocity.z = move_toward(velocity.z, 0.0, 40.0 * delta)
|
||||
if not is_on_floor():
|
||||
velocity += get_gravity() * delta
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
# ── Gravity ───────────────────────────────────────────────────────────────
|
||||
if not is_on_floor():
|
||||
var gravity := get_gravity()
|
||||
@@ -824,6 +946,7 @@ func _physics_process(delta: float) -> void:
|
||||
var on_floor := is_on_floor()
|
||||
|
||||
# ── Ability cooldowns ────────────────────────────────────────────────────────
|
||||
_hit_iframes = maxf(0.0, _hit_iframes - delta)
|
||||
for i: int in 4:
|
||||
ability_cd[i] = maxf(0.0, ability_cd[i] - delta)
|
||||
if _ability_active:
|
||||
@@ -856,7 +979,13 @@ func _physics_process(delta: float) -> void:
|
||||
# input is released.
|
||||
var flat_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||
var max_speed := DP.f("max_speed")
|
||||
if direction.length_squared() > 0.01:
|
||||
if _active_ability == 1 and _ability_active:
|
||||
# Dash: leave the barrel fast like a bullet, then bleed exponentially back
|
||||
# down to cruise speed (fastest drop right at the start), so it settles into
|
||||
# normal running by the time the clip ends instead of holding top speed.
|
||||
_dash_speed_cur = lerpf(_dash_speed_cur, max_speed, 1.0 - exp(-DP.f("dash_decel") * delta))
|
||||
flat_vel = _dash_dir * _dash_speed_cur
|
||||
elif direction.length_squared() > 0.01:
|
||||
var cap := maxf(max_speed, flat_vel.length())
|
||||
flat_vel = flat_vel.move_toward(direction * cap, DP.f("move_accel") * delta)
|
||||
else:
|
||||
@@ -871,6 +1000,9 @@ func _physics_process(delta: float) -> void:
|
||||
cube_guy.rotation.y = lerp_angle(
|
||||
cube_guy.rotation.y, target_angle, delta * DP.f("visual_turn_speed"))
|
||||
|
||||
# Looping DASH clip while genuinely moving on the ground, idle otherwise.
|
||||
_update_locomotion_anim(on_floor and flat_speed > 1.0)
|
||||
|
||||
_pre_slide_vel_y = velocity.y
|
||||
var pre_wall_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||
move_and_slide()
|
||||
@@ -925,11 +1057,76 @@ func _physics_process(delta: float) -> void:
|
||||
# ── Hoof sounds ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
# A single clean hit — thrown or swung blade — ends the run. `cause` ("gored" /
|
||||
# "thrown") tags how it happened for the death notice. Latches so the lose screen is
|
||||
# raised exactly once even if several blades land on the same frame.
|
||||
func take_sword_hit(cause: String = "") -> void:
|
||||
if _dead:
|
||||
func get_hp() -> int:
|
||||
return _hp
|
||||
|
||||
|
||||
func get_max_hp() -> int:
|
||||
return _max_hp
|
||||
|
||||
|
||||
# A clean hit — thrown or swung blade — costs the bull one HP pip; the run ends only
|
||||
# when the last pip is gone. `cause` ("gored" / "thrown") tags how it happened for the
|
||||
# death notice. I-frames absorb a flurry so several blades in one pass drop one pip,
|
||||
# not the whole bar; `_dead` latches so the lose screen is raised exactly once.
|
||||
func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void:
|
||||
if _dead or _hit_iframes > 0.0:
|
||||
return
|
||||
_dead = true
|
||||
died.emit(cause)
|
||||
_hp -= 1
|
||||
_hit_iframes = DP.f("bull_hit_iframes")
|
||||
health_changed.emit(_hp, _max_hp)
|
||||
hit_taken.emit(cause)
|
||||
camera_pivot.call(&"trigger_hit", 0.7)
|
||||
# A spear gores a spot — blood spurts from the entry wound (where it struck the
|
||||
# bull). Melee gores pass no position and skip the spurt; gore can be turned off.
|
||||
if cause == "thrown" and Settings.gore:
|
||||
_spawn_blood(hit_pos if hit_pos != Vector3.ZERO else global_position)
|
||||
if _huff_player:
|
||||
_huff_player.pitch_scale = randf_range(0.7, 0.9)
|
||||
_huff_player.play()
|
||||
if _hp <= 0:
|
||||
_dead = true
|
||||
died.emit(cause)
|
||||
|
||||
|
||||
# Blood spurt from a spear wound: a short-lived spray of dark-red gobs flung out of the
|
||||
# entry point, biased outward from the bull's centre (so it reads as bursting from the
|
||||
# hide) with a touch of lift before gravity drags it down.
|
||||
func _spawn_blood(hit_pos: Vector3) -> void:
|
||||
var out := hit_pos - global_position
|
||||
out.y = 0.0
|
||||
out = out.normalized() if out.length() > 0.05 else Vector3(0.0, 0.0, 1.0)
|
||||
var dir := (out + Vector3.UP * 0.8).normalized()
|
||||
|
||||
var sphere := _particle_sphere(0.05, 4, 2, _unshaded_material())
|
||||
|
||||
var ramp := Gradient.new()
|
||||
ramp.set_color(0, Color(0.55, 0.02, 0.02, 1.0))
|
||||
ramp.set_color(1, Color(0.28, 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.7))
|
||||
scale_curve.add_point(Vector2(1.0, 0.0))
|
||||
|
||||
var p := CPUParticles3D.new()
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.9
|
||||
p.amount = 34
|
||||
p.lifetime = 0.7
|
||||
p.randomness = 0.5
|
||||
p.local_coords = false
|
||||
p.direction = dir
|
||||
p.spread = 42.0
|
||||
p.gravity = Vector3(0.0, -14.0, 0.0)
|
||||
p.initial_velocity_min = 3.5
|
||||
p.initial_velocity_max = 8.0
|
||||
p.scale_amount_min = 0.5
|
||||
p.scale_amount_max = 1.6
|
||||
p.mesh = sphere
|
||||
p.color_ramp = ramp
|
||||
p.scale_amount_curve = scale_curve
|
||||
get_parent().add_child(p)
|
||||
p.global_position = hit_pos
|
||||
p.restart()
|
||||
_free_after(p, 1.5)
|
||||
|
||||
Reference in New Issue
Block a user