Feat/3 grapple fundamentals #7
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 532 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 484 KiB |
@@ -8,6 +8,10 @@ var _speed_label: Label
|
||||
var _state_label: Label
|
||||
var _chain_label: Label
|
||||
var _weapon_label: Label
|
||||
var _grapple_icon: TextureRect
|
||||
var _dash_icon: TextureRect
|
||||
var _grapple_label: Label
|
||||
var _dash_label: Label
|
||||
var _player: CharacterBody3D
|
||||
|
||||
|
||||
@@ -478,6 +482,7 @@ func _build_hud() -> void:
|
||||
wp_style.content_margin_right = 16
|
||||
wp_style.content_margin_bottom = 12
|
||||
wp_panel.add_theme_stylebox_override("panel", wp_style)
|
||||
wp_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(wp_panel)
|
||||
|
||||
_weapon_label = Label.new()
|
||||
@@ -488,6 +493,68 @@ func _build_hud() -> void:
|
||||
_weapon_label.add_theme_font_size_override("font_size", 24)
|
||||
wp_panel.add_child(_weapon_label)
|
||||
|
||||
# ── Utilities Panel ───────────────────────────────────────────────────────
|
||||
var util_panel := PanelContainer.new()
|
||||
util_panel.name = "UtilPanel"
|
||||
util_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
util_panel.offset_left = -200
|
||||
util_panel.offset_top = -180
|
||||
util_panel.offset_right = -20
|
||||
util_panel.offset_bottom = -110
|
||||
var util_style := StyleBoxFlat.new()
|
||||
util_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
util_style.corner_radius_top_left = 8
|
||||
util_style.corner_radius_top_right = 8
|
||||
util_style.corner_radius_bottom_left = 8
|
||||
util_style.corner_radius_bottom_right = 8
|
||||
util_style.content_margin_left = 12
|
||||
util_style.content_margin_top = 8
|
||||
util_style.content_margin_right = 12
|
||||
util_style.content_margin_bottom = 8
|
||||
util_panel.add_theme_stylebox_override("panel", util_style)
|
||||
util_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(util_panel)
|
||||
|
||||
var util_hbox := HBoxContainer.new()
|
||||
util_hbox.name = "UtilHBox"
|
||||
util_hbox.add_theme_constant_override("separation", 20)
|
||||
util_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_panel.add_child(util_hbox)
|
||||
|
||||
var grapple_vbox := VBoxContainer.new()
|
||||
grapple_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(grapple_vbox)
|
||||
|
||||
_grapple_icon = TextureRect.new()
|
||||
_grapple_icon.texture = load("res://assets/ui/grapple_icon.jpg")
|
||||
_grapple_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_grapple_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_grapple_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
grapple_vbox.add_child(_grapple_icon)
|
||||
|
||||
_grapple_label = Label.new()
|
||||
_grapple_label.text = "Grapple"
|
||||
_grapple_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_grapple_label.add_theme_font_size_override("font_size", 12)
|
||||
grapple_vbox.add_child(_grapple_label)
|
||||
|
||||
var dash_vbox := VBoxContainer.new()
|
||||
dash_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(dash_vbox)
|
||||
|
||||
_dash_icon = TextureRect.new()
|
||||
_dash_icon.texture = load("res://assets/ui/dash_icon.jpg")
|
||||
_dash_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_dash_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_dash_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
dash_vbox.add_child(_dash_icon)
|
||||
|
||||
_dash_label = Label.new()
|
||||
_dash_label.text = "Ready"
|
||||
_dash_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_dash_label.add_theme_font_size_override("font_size", 12)
|
||||
dash_vbox.add_child(_dash_label)
|
||||
|
||||
|
||||
# ── Utility: Key Name ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -528,6 +595,22 @@ func _process(_delta: float) -> void:
|
||||
if sm:
|
||||
_chain_label.text = "Chain: %d (+%d%%)" % [sm.chain_count, int(sm.current_chain_bonus * 100)]
|
||||
|
||||
# Update utility indicators
|
||||
if sm.current_state == "grapple" or sm.is_grapple_shooting:
|
||||
_grapple_icon.modulate = Color(0.2, 1.0, 0.4)
|
||||
_grapple_label.text = "Grappling"
|
||||
else:
|
||||
_grapple_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_grapple_label.text = "Ready"
|
||||
|
||||
var dash_rem = sm.get_dash_cooldown_remaining()
|
||||
if dash_rem > 0.0:
|
||||
_dash_icon.modulate = Color(1.0, 0.3, 0.3)
|
||||
_dash_label.text = "%.1f" % dash_rem
|
||||
else:
|
||||
_dash_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_dash_label.text = "Ready"
|
||||
|
||||
if _weapon_label:
|
||||
var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||
if wman and wman.weapons.has(wman.active_slot):
|
||||
|
||||
@@ -433,4 +433,36 @@ for i in range(int(sample_rate * mortar_duration)):
|
||||
mortar_samples.append(thunk + ring)
|
||||
write_wav('assets/sounds/mortar_fire.wav', mortar_samples)
|
||||
|
||||
# 21. Rocket Flight (Deep continuous looping whoosh/rumble)
|
||||
flight_duration = 1.0 # 1 second loop
|
||||
flight_samples = []
|
||||
prev_f = 0.0
|
||||
for i in range(int(sample_rate * flight_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
alpha = 0.15 # Higher cutoff for more whoosh
|
||||
filtered = prev_f + alpha * (noise - prev_f)
|
||||
prev_f = filtered
|
||||
rumble = math.sin(t * 70.0 * 2 * math.pi) * 1.5
|
||||
raw = (filtered * 2.5 + rumble)
|
||||
# Heavy soft clipping to make it sound intensely loud
|
||||
flight_samples.append(math.tanh(raw * 2.0))
|
||||
write_wav('assets/sounds/rocket_flight.wav', flight_samples)
|
||||
|
||||
# 22. Swarm Flight (Higher pitched continuous hiss)
|
||||
swarm_flight_samples = []
|
||||
prev_sf = 0.0
|
||||
for i in range(int(sample_rate * flight_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
alpha = 0.4 # Higher cutoff for hiss
|
||||
filtered = prev_sf + alpha * (noise - prev_sf)
|
||||
prev_sf = filtered
|
||||
# Aggressive screaming tone
|
||||
whine = math.sin(t * 800.0 * 2 * math.pi) * 1.5
|
||||
raw = (filtered * 2.0 + whine)
|
||||
# Heavy soft clipping
|
||||
swarm_flight_samples.append(math.tanh(raw * 2.0))
|
||||
write_wav('assets/sounds/swarm_flight.wav', swarm_flight_samples)
|
||||
|
||||
print("Generated all sounds!")
|
||||
|
||||
@@ -8,6 +8,7 @@ var default_bindings = {
|
||||
"dash": {"type": "key", "code": KEY_SHIFT},
|
||||
"fire": {"type": "mouse", "code": MOUSE_BUTTON_LEFT},
|
||||
"fire_alt": {"type": "mouse", "code": MOUSE_BUTTON_RIGHT},
|
||||
"grapple": {"type": "mouse", "code": MOUSE_BUTTON_MIDDLE},
|
||||
"reload": {"type": "key", "code": KEY_R},
|
||||
"interact": {"type": "key", "code": KEY_E},
|
||||
"move_forward": {"type": "key", "code": KEY_W},
|
||||
|
||||
@@ -57,10 +57,17 @@ class_name MovementParams
|
||||
@export var wall_cling_stamina_drain: float = 2.0
|
||||
@export var wall_cling_max_stamina: float = 2.0
|
||||
|
||||
# ── Grapple ───────────────────────────────────────────────────────────────────
|
||||
@export var grapple_range: float = 30.0
|
||||
@export var grapple_pull_force: float = 25.0
|
||||
@export var grapple_jump_boost: float = 12.0
|
||||
@export var grapple_spring_strength: float = 10.0
|
||||
@export var grapple_air_control: float = 20.0
|
||||
|
||||
# ── Dash ──────────────────────────────────────────────────────────────────────
|
||||
@export var dash_speed: float = 10.0
|
||||
@export var dash_duration: float = 0.25
|
||||
@export var dash_cooldown: float = 1.0
|
||||
@export var dash_cooldown: float = 10.0
|
||||
@export var dash_invulnerability_time: float = 0.1
|
||||
|
||||
# ── Rocket Jump ───────────────────────────────────────────────────────────────
|
||||
@@ -70,7 +77,7 @@ class_name MovementParams
|
||||
@export var rocket_jump_falloff: float = 1.5
|
||||
|
||||
# ── Shotgun Jump ──────────────────────────────────────────────────────────────
|
||||
@export var shotgun_jump_impulse: float = 10.0
|
||||
@export var shotgun_jump_impulse: float = 12.0
|
||||
|
||||
# ── Double Jump ───────────────────────────────────────────────────────────────
|
||||
@export var double_jump_velocity: float = 8.0
|
||||
|
||||
@@ -20,6 +20,15 @@ var input_jump_pressed: bool = false
|
||||
var input_jump_just_pressed: bool = false
|
||||
var input_crouch: bool = false
|
||||
var input_dash: bool = false
|
||||
var input_grapple: bool = false
|
||||
var input_grapple_just_pressed: bool = false
|
||||
|
||||
# Grapple state
|
||||
var grapple_point: Vector3 = Vector3.ZERO
|
||||
var grapple_length: float = 0.0
|
||||
var grapple_travel_time: float = 0.0
|
||||
var grapple_shoot_time: float = 0.0
|
||||
var is_grapple_shooting: bool = false
|
||||
|
||||
# ── State tracking ────────────────────────────────────────────────────────────
|
||||
var wall_normal: Vector3 = Vector3.ZERO
|
||||
@@ -40,6 +49,12 @@ var is_crouched: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Ensure grapple state is injected
|
||||
var grapple_state = Node.new()
|
||||
grapple_state.name = "state_grapple"
|
||||
grapple_state.set_script(load("res://movement/states/state_grapple.gd"))
|
||||
add_child(grapple_state)
|
||||
|
||||
for child in get_children():
|
||||
if child is Node and child.name.begins_with("state_"):
|
||||
states[child.name.replace("state_", "")] = child
|
||||
@@ -72,6 +87,17 @@ func _physics_process(delta: float) -> void:
|
||||
chain_count = 0
|
||||
current_chain_bonus = 0.0
|
||||
|
||||
# Global grapple check: if just pressed, raycast from camera
|
||||
if input_grapple_just_pressed and current_state != "grapple" and not is_grapple_shooting:
|
||||
_try_start_grapple()
|
||||
|
||||
if is_grapple_shooting:
|
||||
grapple_shoot_time += delta
|
||||
if grapple_shoot_time >= grapple_travel_time:
|
||||
is_grapple_shooting = false
|
||||
movement_event.emit("grapple_latch", {})
|
||||
switch_to("grapple")
|
||||
|
||||
# Manage global crouch state
|
||||
var want_crouch = input_crouch
|
||||
if current_state == "slide":
|
||||
@@ -117,6 +143,27 @@ func switch_to(new_state_name: String, data: Dictionary = {}) -> void:
|
||||
state_changed.emit(prev, new_state_name)
|
||||
|
||||
|
||||
func _try_start_grapple() -> void:
|
||||
if not player or not player.camera:
|
||||
return
|
||||
var camera: Camera3D = player.camera
|
||||
var space_state := player.get_world_3d().direct_space_state
|
||||
var origin := camera.global_position
|
||||
var end := origin - camera.global_transform.basis.z * params.grapple_range
|
||||
var ray := PhysicsRayQueryParameters3D.create(origin, end)
|
||||
ray.exclude = [player.get_rid()]
|
||||
var hit := space_state.intersect_ray(ray)
|
||||
if not hit.is_empty():
|
||||
grapple_point = hit.position
|
||||
grapple_length = origin.distance_to(grapple_point)
|
||||
|
||||
# Travel at 45 m/s
|
||||
grapple_travel_time = grapple_length / 45
|
||||
grapple_shoot_time = 0.0
|
||||
is_grapple_shooting = true
|
||||
movement_event.emit("grapple_shoot", {})
|
||||
|
||||
|
||||
func register_chain_mechanic(_mechanic_name: String) -> void:
|
||||
if chain_timer > 0.0 and chain_count > 0:
|
||||
chain_count += 1
|
||||
@@ -212,3 +259,7 @@ func _apply_crouch(crouched: bool) -> void:
|
||||
shape_node.position.y = 0.0
|
||||
if head:
|
||||
head.position.y = 0.7
|
||||
|
||||
func get_dash_cooldown_remaining() -> float:
|
||||
var now := Time.get_ticks_msec() / 1000.0
|
||||
return maxf(0.0, params.dash_cooldown - (now - StateDash._last_dash_time))
|
||||
|
||||
@@ -20,10 +20,17 @@ var wind_player: AudioStreamPlayer
|
||||
var jump_player: AudioStreamPlayer
|
||||
var double_jump_player: AudioStreamPlayer
|
||||
|
||||
var grapple_shoot_player: AudioStreamPlayer
|
||||
var grapple_latch_player: AudioStreamPlayer
|
||||
var grapple_swing_player: AudioStreamPlayer
|
||||
|
||||
# UI
|
||||
var hit_marker: Control
|
||||
var _hit_marker_tween: Tween
|
||||
|
||||
# Grapple
|
||||
var grapple_rope: MeshInstance3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var sm := _ensure_machine()
|
||||
@@ -42,6 +49,7 @@ func _ready() -> void:
|
||||
|
||||
_setup_audio()
|
||||
_setup_hit_marker()
|
||||
_setup_grapple()
|
||||
|
||||
set_process(true)
|
||||
set_physics_process(true)
|
||||
@@ -85,6 +93,24 @@ func _setup_audio() -> void:
|
||||
add_child(wind_player)
|
||||
wind_player.play()
|
||||
|
||||
grapple_shoot_player = AudioStreamPlayer.new()
|
||||
grapple_shoot_player.stream = load("res://assets/sounds/dash.wav")
|
||||
grapple_shoot_player.volume_db = -5.0
|
||||
grapple_shoot_player.pitch_scale = 1.5
|
||||
add_child(grapple_shoot_player)
|
||||
|
||||
grapple_latch_player = AudioStreamPlayer.new()
|
||||
grapple_latch_player.stream = load("res://assets/sounds/hit_confirm.wav")
|
||||
grapple_latch_player.pitch_scale = 0.8
|
||||
add_child(grapple_latch_player)
|
||||
|
||||
grapple_swing_player = AudioStreamPlayer.new()
|
||||
grapple_swing_player.stream = load("res://assets/sounds/wind.wav")
|
||||
grapple_swing_player.pitch_scale = 1.5
|
||||
grapple_swing_player.volume_db = -80.0
|
||||
add_child(grapple_swing_player)
|
||||
grapple_swing_player.play()
|
||||
|
||||
func _setup_hit_marker() -> void:
|
||||
hit_marker = Control.new()
|
||||
hit_marker.set_anchors_preset(Control.PRESET_CENTER)
|
||||
@@ -106,6 +132,22 @@ func _setup_hit_marker() -> void:
|
||||
rect.rotation = angle
|
||||
hit_marker.add_child(rect)
|
||||
|
||||
func _setup_grapple() -> void:
|
||||
grapple_rope = MeshInstance3D.new()
|
||||
var rope_mesh = CylinderMesh.new()
|
||||
rope_mesh.top_radius = 0.04
|
||||
rope_mesh.bottom_radius = 0.04
|
||||
rope_mesh.height = 1.0 # Will be scaled dynamically
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.1, 0.1, 0.1) # Dark grey cable
|
||||
mat.roughness = 0.8
|
||||
rope_mesh.material = mat
|
||||
|
||||
grapple_rope.mesh = rope_mesh
|
||||
grapple_rope.visible = false
|
||||
get_tree().current_scene.call_deferred("add_child", grapple_rope)
|
||||
|
||||
var _last_hit_sound_time: int = 0
|
||||
|
||||
func spawn_damage_number(amount: float, hit_pos: Vector3) -> void:
|
||||
@@ -180,6 +222,44 @@ func _physics_process(_delta: float) -> void:
|
||||
sm.input_crouch = Input.is_action_pressed("crouch")
|
||||
sm.input_dash = Input.is_action_just_pressed("dash")
|
||||
|
||||
sm.input_grapple = Input.is_action_pressed("grapple")
|
||||
sm.input_grapple_just_pressed = Input.is_action_just_pressed("grapple")
|
||||
|
||||
# Update grapple rope visuals
|
||||
if (sm.current_state == "grapple" or sm.is_grapple_shooting) and camera:
|
||||
grapple_rope.visible = true
|
||||
|
||||
# For shoot, start at right side of camera slightly below and closer
|
||||
var start_pos = camera.global_position + camera.global_transform.basis * Vector3(0.3, -0.3, -0.15)
|
||||
var end_pos = sm.grapple_point
|
||||
|
||||
if sm.is_grapple_shooting and sm.grapple_travel_time > 0.0:
|
||||
# Interpolate end_pos
|
||||
var t = clampf(sm.grapple_shoot_time / sm.grapple_travel_time, 0.0, 1.0)
|
||||
end_pos = start_pos.lerp(sm.grapple_point, t)
|
||||
|
||||
var dist = start_pos.distance_to(end_pos)
|
||||
if dist > 0.01:
|
||||
grapple_rope.global_position = (start_pos + end_pos) / 2.0
|
||||
# Look at end point
|
||||
var up_vec = Vector3.UP
|
||||
var dir = (end_pos - start_pos).normalized()
|
||||
if abs(dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
# Set rotation first, which resets scale
|
||||
grapple_rope.transform.basis = Basis.looking_at(dir, up_vec) * Basis.from_euler(Vector3(PI/2.0, 0, 0))
|
||||
# Then apply the scale
|
||||
grapple_rope.scale = Vector3(1.0, dist, 1.0)
|
||||
else:
|
||||
if grapple_rope:
|
||||
grapple_rope.visible = false
|
||||
|
||||
# Swing sound
|
||||
if sm.current_state == "grapple" and sm.player.velocity.length() > 5.0:
|
||||
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -15.0, _delta * 10.0)
|
||||
else:
|
||||
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -80.0, _delta * 15.0)
|
||||
|
||||
# Raw 2D input
|
||||
var raw_input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||
|
||||
@@ -202,3 +282,7 @@ func _physics_process(_delta: float) -> void:
|
||||
func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
if ev == "chain_updated":
|
||||
chain_updated.emit(data.count, data.bonus)
|
||||
elif ev == "grapple_shoot":
|
||||
grapple_shoot_player.play()
|
||||
elif ev == "grapple_latch":
|
||||
grapple_latch_player.play()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
extends Node
|
||||
class_name StateGrapple
|
||||
|
||||
var machine: MovementStateMachine
|
||||
var params: MovementParams:
|
||||
get: return machine.params
|
||||
|
||||
func enter(_data: Dictionary = {}) -> void:
|
||||
machine.on_ground = false
|
||||
machine.wall_normal = Vector3.ZERO
|
||||
machine.wall_side = 0.0
|
||||
machine.register_chain_mechanic("grapple")
|
||||
|
||||
func exit() -> void:
|
||||
pass
|
||||
|
||||
func update(delta: float) -> void:
|
||||
var player := machine.player
|
||||
var vel: Vector3 = player.velocity
|
||||
|
||||
# If player releases the button, disconnect
|
||||
if not machine.input_grapple:
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
var grapple_pos: Vector3 = machine.grapple_point
|
||||
var current_dist: float = player.global_position.distance_to(grapple_pos)
|
||||
var dir_to_point: Vector3 = (grapple_pos - player.global_position).normalized()
|
||||
|
||||
# Jump to detach and get a boost
|
||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||
vel.y = params.jump_velocity
|
||||
# Add a directional boost if jumping while swinging
|
||||
var h_look = machine.wish_dir_world
|
||||
if h_look.length_squared() > 0.1:
|
||||
var h_boost = h_look.normalized() * params.grapple_jump_boost
|
||||
vel.x += h_boost.x
|
||||
vel.z += h_boost.z
|
||||
|
||||
player.velocity = vel
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
machine.register_chain_mechanic("grapple_jump")
|
||||
machine.switch_to("air")
|
||||
if player.jump_player:
|
||||
player.jump_player.play()
|
||||
return
|
||||
|
||||
# Apply gravity
|
||||
vel.y -= params.gravity * delta
|
||||
|
||||
# Apply slight inward pull (reeling in)
|
||||
vel += dir_to_point * params.grapple_pull_force * delta
|
||||
|
||||
# Pendulum / Rope physics
|
||||
# If the player tries to go further than the original rope length, pull them back aggressively
|
||||
var max_len: float = machine.grapple_length
|
||||
if current_dist > max_len:
|
||||
# Project velocity onto the tangent of the sphere (prevent moving further away)
|
||||
var radial_vel = vel.project(dir_to_point)
|
||||
# If radial_vel is pointing AWAY from the grapple point (dot < 0), kill it
|
||||
if radial_vel.dot(dir_to_point) < 0:
|
||||
vel -= radial_vel
|
||||
|
||||
# Add a spring force to pull them back to the sphere
|
||||
var diff = current_dist - max_len
|
||||
var spring_force = dir_to_point * diff * params.grapple_spring_strength
|
||||
vel += spring_force * delta
|
||||
|
||||
# Air control while swinging
|
||||
var wish_dir: Vector3 = machine.wish_dir_world
|
||||
if wish_dir.length_squared() > 0.01:
|
||||
wish_dir = wish_dir.normalized()
|
||||
# Add force tangentially to the rope
|
||||
# We want to steer, but not pull away from or directly into the rope.
|
||||
var tangent_wish = wish_dir - wish_dir.project(dir_to_point)
|
||||
if tangent_wish.length_squared() > 0.01:
|
||||
vel += tangent_wish.normalized() * params.grapple_air_control * delta
|
||||
|
||||
player.velocity = vel
|
||||
player.move_and_slide()
|
||||
|
||||
# Remove ground detach so players can grapple along the floor
|
||||
# if player.is_on_floor():
|
||||
# machine.on_ground = true
|
||||
# machine.switch_to("ground")
|
||||
# return
|
||||
@@ -0,0 +1 @@
|
||||
uid://8uc46pvajxdf
|
||||
@@ -83,6 +83,11 @@ reload={
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":82,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
grapple={
|
||||
"deadzone": 0.0,
|
||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[physics]
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ func _physics_process(delta: float) -> void:
|
||||
fuse_time -= delta
|
||||
if fuse_time <= 0.0:
|
||||
_explode(global_position)
|
||||
queue_free()
|
||||
destroy()
|
||||
return
|
||||
|
||||
# Apply gravity drop if any
|
||||
|
||||
@@ -1,16 +1,141 @@
|
||||
extends Projectile
|
||||
class_name ExplosiveProjectile
|
||||
|
||||
var explosion_radius: float = 5.0
|
||||
var explosion_radius: float = 2.5
|
||||
var explosion_knockback: float = 20.0
|
||||
var can_self_damage: bool = false
|
||||
var falloff_curve: float = 1.0 # 1.0 is linear, 2.0 is quadratic (steep drop)
|
||||
var flight_sound: AudioStreamPlayer3D
|
||||
var smoke_sys: GPUParticles3D
|
||||
var jet_sys: GPUParticles3D
|
||||
|
||||
static var _smoke_tex: GradientTexture2D
|
||||
|
||||
static func get_smoke_texture() -> GradientTexture2D:
|
||||
if _smoke_tex: return _smoke_tex
|
||||
_smoke_tex = GradientTexture2D.new()
|
||||
_smoke_tex.fill = GradientTexture2D.FILL_RADIAL
|
||||
_smoke_tex.fill_from = Vector2(0.5, 0.5)
|
||||
_smoke_tex.fill_to = Vector2(1.0, 1.0)
|
||||
|
||||
var g = Gradient.new()
|
||||
g.add_point(0.0, Color(1, 1, 1, 1))
|
||||
g.add_point(0.5, Color(1, 1, 1, 0.5))
|
||||
g.add_point(1.0, Color(1, 1, 1, 0))
|
||||
_smoke_tex.gradient = g
|
||||
_smoke_tex.width = 64
|
||||
_smoke_tex.height = 64
|
||||
return _smoke_tex
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
_setup_effects()
|
||||
|
||||
func destroy() -> void:
|
||||
if is_instance_valid(smoke_sys):
|
||||
smoke_sys.emitting = false
|
||||
var p = smoke_sys.get_parent()
|
||||
if p:
|
||||
var trans = smoke_sys.global_transform
|
||||
p.remove_child(smoke_sys)
|
||||
get_tree().current_scene.add_child(smoke_sys)
|
||||
smoke_sys.global_transform = trans
|
||||
var timer = smoke_sys.get_tree().create_timer(smoke_sys.lifetime)
|
||||
timer.timeout.connect(smoke_sys.queue_free)
|
||||
|
||||
if is_instance_valid(jet_sys):
|
||||
jet_sys.emitting = false
|
||||
var p = jet_sys.get_parent()
|
||||
if p:
|
||||
var trans = jet_sys.global_transform
|
||||
p.remove_child(jet_sys)
|
||||
get_tree().current_scene.add_child(jet_sys)
|
||||
jet_sys.global_transform = trans
|
||||
var timer = jet_sys.get_tree().create_timer(jet_sys.lifetime)
|
||||
timer.timeout.connect(jet_sys.queue_free)
|
||||
|
||||
super.destroy()
|
||||
|
||||
func _setup_effects() -> void:
|
||||
# Audio
|
||||
flight_sound = AudioStreamPlayer3D.new()
|
||||
flight_sound.stream = load("res://assets/sounds/rocket_flight.wav")
|
||||
flight_sound.autoplay = true
|
||||
if flight_sound.stream is AudioStreamWAV:
|
||||
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
flight_sound.unit_size = 50.0
|
||||
flight_sound.max_db = 6.0
|
||||
flight_sound.volume_db = 6.0
|
||||
add_child(flight_sound)
|
||||
flight_sound.play()
|
||||
|
||||
# Jet Fire Particles
|
||||
jet_sys = GPUParticles3D.new()
|
||||
var jet_mat = ParticleProcessMaterial.new()
|
||||
jet_mat.direction = Vector3(0, 0, 1) # Backwards (since model looks at -Z)
|
||||
jet_mat.spread = 10.0
|
||||
jet_mat.initial_velocity_min = 1.0
|
||||
jet_mat.initial_velocity_max = 2.0
|
||||
jet_mat.gravity = Vector3.ZERO
|
||||
|
||||
var jet_mesh = BoxMesh.new()
|
||||
jet_mesh.size = Vector3(0.05, 0.05, 0.05)
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(1.0, 0.6, 0.0)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.5, 0.0)
|
||||
jet_mesh.material = mat
|
||||
jet_sys.draw_pass_1 = jet_mesh
|
||||
jet_sys.process_material = jet_mat
|
||||
jet_sys.amount = 16
|
||||
jet_sys.lifetime = 0.2
|
||||
jet_sys.position = Vector3(0, 0, 0.1)
|
||||
add_child(jet_sys)
|
||||
|
||||
# Smoke Trail
|
||||
smoke_sys = GPUParticles3D.new()
|
||||
var smoke_mat = ParticleProcessMaterial.new()
|
||||
smoke_mat.direction = Vector3(0, 0, 1)
|
||||
smoke_mat.spread = 5.0
|
||||
smoke_mat.initial_velocity_min = 0.0
|
||||
smoke_mat.initial_velocity_max = 0.5
|
||||
smoke_mat.gravity = Vector3(0, 0.5, 0)
|
||||
|
||||
var grad = Gradient.new()
|
||||
grad.add_point(0.0, Color(0.5, 0.5, 0.5, 0.8))
|
||||
grad.add_point(1.0, Color(0.8, 0.8, 0.8, 0.0))
|
||||
var grad_tex = GradientTexture1D.new()
|
||||
grad_tex.gradient = grad
|
||||
smoke_mat.color_ramp = grad_tex
|
||||
|
||||
var curve = Curve.new()
|
||||
curve.add_point(Vector2(0.0, 1.0))
|
||||
curve.add_point(Vector2(1.0, 3.0))
|
||||
var curve_tex = CurveTexture.new()
|
||||
curve_tex.curve = curve
|
||||
smoke_mat.scale_curve = curve_tex
|
||||
|
||||
var smoke_mesh = QuadMesh.new()
|
||||
smoke_mesh.size = Vector2(0.2, 0.2)
|
||||
var s_mat = StandardMaterial3D.new()
|
||||
s_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
s_mat.vertex_color_use_as_albedo = true
|
||||
s_mat.albedo_texture = get_smoke_texture()
|
||||
s_mat.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
|
||||
s_mat.billboard_keep_scale = true
|
||||
smoke_mesh.material = s_mat
|
||||
smoke_sys.draw_pass_1 = smoke_mesh
|
||||
smoke_sys.process_material = smoke_mat
|
||||
smoke_sys.amount = 32
|
||||
smoke_sys.lifetime = 1.5
|
||||
smoke_sys.position = Vector3(0, 0, 0.15)
|
||||
add_child(smoke_sys)
|
||||
|
||||
func _on_hit(result: Dictionary) -> void:
|
||||
if not result.collider.has_method("take_damage"):
|
||||
ImpactSpawner.spawn(get_tree(), "crater", result.position, result.normal, explosion_radius * 1.5)
|
||||
_explode(result.position)
|
||||
queue_free()
|
||||
destroy()
|
||||
|
||||
func _explode(pos: Vector3) -> void:
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
@@ -49,10 +174,12 @@ func _explode(pos: Vector3) -> void:
|
||||
|
||||
# Normalised distance (0 = epicenter, 1 = edge)
|
||||
var ndist = dist / explosion_radius
|
||||
var intensity = clampf(1.0 - pow(ndist, falloff_curve), 0.0, 1.0)
|
||||
var falloff = clampf(pow(ndist, falloff_curve), 0.0, 1.0)
|
||||
var damage_intensity = lerpf(1.0, 0.33, falloff)
|
||||
var knockback_intensity = 1.0 - falloff
|
||||
|
||||
var final_damage = damage * intensity
|
||||
var final_knockback = explosion_knockback * intensity
|
||||
var final_damage = damage * damage_intensity
|
||||
var final_knockback = explosion_knockback * knockback_intensity
|
||||
|
||||
# Knockback direction: from explosion pos to target
|
||||
var dir = (target_pos - pos).normalized()
|
||||
|
||||
@@ -8,6 +8,88 @@ var initial_aim_dir: Vector3 = Vector3.ZERO
|
||||
var cruise_speed: float = 60.0
|
||||
var ignition_timer: float = 0.5
|
||||
var ignited: bool = false
|
||||
var effects_started: bool = false
|
||||
|
||||
func _setup_effects() -> void:
|
||||
pass # Wait until ignited
|
||||
|
||||
func _start_homing_effects() -> void:
|
||||
if effects_started: return
|
||||
effects_started = true
|
||||
|
||||
# Audio
|
||||
flight_sound = AudioStreamPlayer3D.new()
|
||||
flight_sound.stream = load("res://assets/sounds/swarm_flight.wav")
|
||||
flight_sound.autoplay = true
|
||||
if flight_sound.stream is AudioStreamWAV:
|
||||
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
flight_sound.unit_size = 50.0
|
||||
flight_sound.max_db = 6.0
|
||||
flight_sound.volume_db = 6.0
|
||||
add_child(flight_sound)
|
||||
flight_sound.play()
|
||||
|
||||
# Jet Fire Particles
|
||||
jet_sys = GPUParticles3D.new()
|
||||
var jet_mat = ParticleProcessMaterial.new()
|
||||
jet_mat.direction = Vector3(0, 0, 1)
|
||||
jet_mat.spread = 10.0
|
||||
jet_mat.initial_velocity_min = 0.5
|
||||
jet_mat.initial_velocity_max = 1.0
|
||||
jet_mat.gravity = Vector3.ZERO
|
||||
|
||||
var jet_mesh = BoxMesh.new()
|
||||
jet_mesh.size = Vector3(0.02, 0.02, 0.02)
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(1.0, 0.6, 0.0)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.5, 0.0)
|
||||
jet_mesh.material = mat
|
||||
jet_sys.draw_pass_1 = jet_mesh
|
||||
jet_sys.process_material = jet_mat
|
||||
jet_sys.amount = 16
|
||||
jet_sys.lifetime = 0.15
|
||||
jet_sys.position = Vector3(0, 0, 0.05)
|
||||
add_child(jet_sys)
|
||||
|
||||
# Smoke Trail
|
||||
smoke_sys = GPUParticles3D.new()
|
||||
var smoke_mat = ParticleProcessMaterial.new()
|
||||
smoke_mat.direction = Vector3(0, 0, 1)
|
||||
smoke_mat.spread = 5.0
|
||||
smoke_mat.initial_velocity_min = 0.0
|
||||
smoke_mat.initial_velocity_max = 0.2
|
||||
smoke_mat.gravity = Vector3(0, 0.5, 0)
|
||||
|
||||
var grad = Gradient.new()
|
||||
grad.add_point(0.0, Color(0.5, 0.5, 0.5, 0.8))
|
||||
grad.add_point(1.0, Color(0.8, 0.8, 0.8, 0.0))
|
||||
var grad_tex = GradientTexture1D.new()
|
||||
grad_tex.gradient = grad
|
||||
smoke_mat.color_ramp = grad_tex
|
||||
|
||||
var curve = Curve.new()
|
||||
curve.add_point(Vector2(0.0, 1.0))
|
||||
curve.add_point(Vector2(1.0, 3.0))
|
||||
var curve_tex = CurveTexture.new()
|
||||
curve_tex.curve = curve
|
||||
smoke_mat.scale_curve = curve_tex
|
||||
|
||||
var smoke_mesh = QuadMesh.new()
|
||||
smoke_mesh.size = Vector2(0.08, 0.08)
|
||||
var s_mat = StandardMaterial3D.new()
|
||||
s_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
s_mat.vertex_color_use_as_albedo = true
|
||||
s_mat.albedo_texture = ExplosiveProjectile.get_smoke_texture()
|
||||
s_mat.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
|
||||
s_mat.billboard_keep_scale = true
|
||||
smoke_mesh.material = s_mat
|
||||
smoke_sys.draw_pass_1 = smoke_mesh
|
||||
smoke_sys.process_material = smoke_mat
|
||||
smoke_sys.amount = 32
|
||||
smoke_sys.lifetime = 1.0
|
||||
smoke_sys.position = Vector3(0, 0, 0.05)
|
||||
add_child(smoke_sys)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not ignited:
|
||||
@@ -15,6 +97,7 @@ func _physics_process(delta: float) -> void:
|
||||
if ignition_timer <= 0.0:
|
||||
ignited = true
|
||||
drop_gravity = 0.0
|
||||
_start_homing_effects()
|
||||
|
||||
if ignited and is_instance_valid(weapon_source):
|
||||
var target_dir: Vector3 = initial_aim_dir
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
proj.bounciness = 0.4 # Heavy, loses momentum on bounce
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 12.0 # Very large AoE
|
||||
proj.explosion_radius = 10.0 # Very large AoE
|
||||
proj.explosion_knockback = 60.0 # Massive knockback
|
||||
proj.falloff_curve = 2.0 # Steep dropoff: "Should be an instant kill in closest 50%"
|
||||
# (0-50% = dist<6. 6/12 = 0.5. 1.0 - 0.5^2 = 0.75 multiplier. 200 * 0.75 = 150 damage -> 1 shot kill)
|
||||
|
||||
@@ -21,7 +21,7 @@ func _ready() -> void:
|
||||
_previous_position = global_position
|
||||
# Automatically destroy after 5 seconds to prevent leaks
|
||||
var t = get_tree().create_timer(5.0)
|
||||
t.timeout.connect(queue_free)
|
||||
t.timeout.connect(destroy)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# Apply gravity drop if any
|
||||
@@ -33,7 +33,7 @@ func _physics_process(delta: float) -> void:
|
||||
distance_traveled += movement.length()
|
||||
|
||||
if distance_traveled > max_range:
|
||||
queue_free()
|
||||
destroy()
|
||||
return
|
||||
|
||||
_check_collision()
|
||||
@@ -63,4 +63,7 @@ func _on_hit(result: Dictionary) -> void:
|
||||
var size = 0.4 if impact_type == "plasma" else 0.15
|
||||
ImpactSpawner.spawn(get_tree(), impact_type, result.position, result.normal, size)
|
||||
|
||||
destroy()
|
||||
|
||||
func destroy() -> void:
|
||||
queue_free()
|
||||
|
||||
@@ -98,7 +98,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
proj.owner_player = player
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 8.0 # Moderate AoE
|
||||
proj.explosion_radius = 4.0 # Moderate AoE
|
||||
proj.explosion_knockback = 45.0 # Strong knockback!
|
||||
proj.can_self_damage = false # As requested
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ func _fire() -> void:
|
||||
var tween = create_tween()
|
||||
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.1)
|
||||
|
||||
var origin = camera.global_position + camera.global_transform.basis * Vector3(0.4, -0.2, -0.6)
|
||||
var _origin = camera.global_position + camera.global_transform.basis * Vector3(0.4, -0.2, -0.6)
|
||||
var base_dir = -camera.global_transform.basis.z
|
||||
|
||||
_current_aim_dir = base_dir
|
||||
@@ -162,7 +162,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
|
||||
# Logic
|
||||
proj.weapon_source = self
|
||||
proj.turn_speed = 4.0 # Slower turn radius so they sweep nicely
|
||||
proj.turn_speed = 6.5 # Slower turn radius so they sweep nicely
|
||||
|
||||
proj.position = origin # Start at origin to spread outward around camera
|
||||
proj.logical_source = origin
|
||||
@@ -184,7 +184,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
proj.owner_player = player
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 3.0 # Tiny AoE effect
|
||||
proj.explosion_radius = 1.5 # Tiny AoE effect
|
||||
proj.explosion_knockback = 5.0
|
||||
proj.can_self_damage = false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user