Compare commits

...
5 Commits
37 changed files with 578 additions and 15 deletions
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

+83
View File
@@ -8,6 +8,10 @@ var _speed_label: Label
var _state_label: Label var _state_label: Label
var _chain_label: Label var _chain_label: Label
var _weapon_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 var _player: CharacterBody3D
@@ -478,6 +482,7 @@ func _build_hud() -> void:
wp_style.content_margin_right = 16 wp_style.content_margin_right = 16
wp_style.content_margin_bottom = 12 wp_style.content_margin_bottom = 12
wp_panel.add_theme_stylebox_override("panel", wp_style) wp_panel.add_theme_stylebox_override("panel", wp_style)
wp_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
canvas.add_child(wp_panel) canvas.add_child(wp_panel)
_weapon_label = Label.new() _weapon_label = Label.new()
@@ -488,6 +493,68 @@ func _build_hud() -> void:
_weapon_label.add_theme_font_size_override("font_size", 24) _weapon_label.add_theme_font_size_override("font_size", 24)
wp_panel.add_child(_weapon_label) 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 ───────────────────────────────────────────────────────── # ── Utility: Key Name ─────────────────────────────────────────────────────────
@@ -527,6 +594,22 @@ func _process(_delta: float) -> void:
var sm = _player.get_node_or_null("MovementStateMachine") var sm = _player.get_node_or_null("MovementStateMachine")
if sm: if sm:
_chain_label.text = "Chain: %d (+%d%%)" % [sm.chain_count, int(sm.current_chain_bonus * 100)] _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: if _weapon_label:
var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager") var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
+32
View File
@@ -433,4 +433,36 @@ for i in range(int(sample_rate * mortar_duration)):
mortar_samples.append(thunk + ring) mortar_samples.append(thunk + ring)
write_wav('assets/sounds/mortar_fire.wav', mortar_samples) 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!") print("Generated all sounds!")
+1
View File
@@ -8,6 +8,7 @@ var default_bindings = {
"dash": {"type": "key", "code": KEY_SHIFT}, "dash": {"type": "key", "code": KEY_SHIFT},
"fire": {"type": "mouse", "code": MOUSE_BUTTON_LEFT}, "fire": {"type": "mouse", "code": MOUSE_BUTTON_LEFT},
"fire_alt": {"type": "mouse", "code": MOUSE_BUTTON_RIGHT}, "fire_alt": {"type": "mouse", "code": MOUSE_BUTTON_RIGHT},
"grapple": {"type": "mouse", "code": MOUSE_BUTTON_MIDDLE},
"reload": {"type": "key", "code": KEY_R}, "reload": {"type": "key", "code": KEY_R},
"interact": {"type": "key", "code": KEY_E}, "interact": {"type": "key", "code": KEY_E},
"move_forward": {"type": "key", "code": KEY_W}, "move_forward": {"type": "key", "code": KEY_W},
+9 -2
View File
@@ -57,10 +57,17 @@ class_name MovementParams
@export var wall_cling_stamina_drain: float = 2.0 @export var wall_cling_stamina_drain: float = 2.0
@export var wall_cling_max_stamina: 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 ────────────────────────────────────────────────────────────────────── # ── Dash ──────────────────────────────────────────────────────────────────────
@export var dash_speed: float = 10.0 @export var dash_speed: float = 10.0
@export var dash_duration: float = 0.25 @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 @export var dash_invulnerability_time: float = 0.1
# ── Rocket Jump ─────────────────────────────────────────────────────────────── # ── Rocket Jump ───────────────────────────────────────────────────────────────
@@ -70,7 +77,7 @@ class_name MovementParams
@export var rocket_jump_falloff: float = 1.5 @export var rocket_jump_falloff: float = 1.5
# ── Shotgun Jump ────────────────────────────────────────────────────────────── # ── Shotgun Jump ──────────────────────────────────────────────────────────────
@export var shotgun_jump_impulse: float = 10.0 @export var shotgun_jump_impulse: float = 12.0
# ── Double Jump ─────────────────────────────────────────────────────────────── # ── Double Jump ───────────────────────────────────────────────────────────────
@export var double_jump_velocity: float = 8.0 @export var double_jump_velocity: float = 8.0
+51
View File
@@ -20,6 +20,15 @@ var input_jump_pressed: bool = false
var input_jump_just_pressed: bool = false var input_jump_just_pressed: bool = false
var input_crouch: bool = false var input_crouch: bool = false
var input_dash: 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 ──────────────────────────────────────────────────────────── # ── State tracking ────────────────────────────────────────────────────────────
var wall_normal: Vector3 = Vector3.ZERO var wall_normal: Vector3 = Vector3.ZERO
@@ -40,6 +49,12 @@ var is_crouched: bool = false
func _ready() -> void: 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(): for child in get_children():
if child is Node and child.name.begins_with("state_"): if child is Node and child.name.begins_with("state_"):
states[child.name.replace("state_", "")] = child states[child.name.replace("state_", "")] = child
@@ -72,6 +87,17 @@ func _physics_process(delta: float) -> void:
chain_count = 0 chain_count = 0
current_chain_bonus = 0.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 # Manage global crouch state
var want_crouch = input_crouch var want_crouch = input_crouch
if current_state == "slide": 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) 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: func register_chain_mechanic(_mechanic_name: String) -> void:
if chain_timer > 0.0 and chain_count > 0: if chain_timer > 0.0 and chain_count > 0:
chain_count += 1 chain_count += 1
@@ -212,3 +259,7 @@ func _apply_crouch(crouched: bool) -> void:
shape_node.position.y = 0.0 shape_node.position.y = 0.0
if head: if head:
head.position.y = 0.7 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))
+84
View File
@@ -20,10 +20,17 @@ var wind_player: AudioStreamPlayer
var jump_player: AudioStreamPlayer var jump_player: AudioStreamPlayer
var double_jump_player: AudioStreamPlayer var double_jump_player: AudioStreamPlayer
var grapple_shoot_player: AudioStreamPlayer
var grapple_latch_player: AudioStreamPlayer
var grapple_swing_player: AudioStreamPlayer
# UI # UI
var hit_marker: Control var hit_marker: Control
var _hit_marker_tween: Tween var _hit_marker_tween: Tween
# Grapple
var grapple_rope: MeshInstance3D
func _ready() -> void: func _ready() -> void:
var sm := _ensure_machine() var sm := _ensure_machine()
@@ -42,6 +49,7 @@ func _ready() -> void:
_setup_audio() _setup_audio()
_setup_hit_marker() _setup_hit_marker()
_setup_grapple()
set_process(true) set_process(true)
set_physics_process(true) set_physics_process(true)
@@ -84,6 +92,24 @@ func _setup_audio() -> void:
wind_player.volume_db = -80.0 # Start silent wind_player.volume_db = -80.0 # Start silent
add_child(wind_player) add_child(wind_player)
wind_player.play() 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: func _setup_hit_marker() -> void:
hit_marker = Control.new() hit_marker = Control.new()
@@ -106,6 +132,22 @@ func _setup_hit_marker() -> void:
rect.rotation = angle rect.rotation = angle
hit_marker.add_child(rect) 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 var _last_hit_sound_time: int = 0
func spawn_damage_number(amount: float, hit_pos: Vector3) -> void: func spawn_damage_number(amount: float, hit_pos: Vector3) -> void:
@@ -179,6 +221,44 @@ func _physics_process(_delta: float) -> void:
sm.input_jump_pressed = Input.is_action_pressed("jump") sm.input_jump_pressed = Input.is_action_pressed("jump")
sm.input_crouch = Input.is_action_pressed("crouch") sm.input_crouch = Input.is_action_pressed("crouch")
sm.input_dash = Input.is_action_just_pressed("dash") 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 # Raw 2D input
var raw_input := Input.get_vector("move_left", "move_right", "move_forward", "move_back") 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: func _on_movement_event(ev: String, data: Dictionary) -> void:
if ev == "chain_updated": if ev == "chain_updated":
chain_updated.emit(data.count, data.bonus) chain_updated.emit(data.count, data.bonus)
elif ev == "grapple_shoot":
grapple_shoot_player.play()
elif ev == "grapple_latch":
grapple_latch_player.play()
+86
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://8uc46pvajxdf
+5
View File
@@ -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) "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] [physics]
+1 -1
View File
@@ -14,7 +14,7 @@ func _physics_process(delta: float) -> void:
fuse_time -= delta fuse_time -= delta
if fuse_time <= 0.0: if fuse_time <= 0.0:
_explode(global_position) _explode(global_position)
queue_free() destroy()
return return
# Apply gravity drop if any # Apply gravity drop if any
+132 -5
View File
@@ -1,16 +1,141 @@
extends Projectile extends Projectile
class_name ExplosiveProjectile class_name ExplosiveProjectile
var explosion_radius: float = 5.0 var explosion_radius: float = 2.5
var explosion_knockback: float = 20.0 var explosion_knockback: float = 20.0
var can_self_damage: bool = false var can_self_damage: bool = false
var falloff_curve: float = 1.0 # 1.0 is linear, 2.0 is quadratic (steep drop) 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: func _on_hit(result: Dictionary) -> void:
if not result.collider.has_method("take_damage"): if not result.collider.has_method("take_damage"):
ImpactSpawner.spawn(get_tree(), "crater", result.position, result.normal, explosion_radius * 1.5) ImpactSpawner.spawn(get_tree(), "crater", result.position, result.normal, explosion_radius * 1.5)
_explode(result.position) _explode(result.position)
queue_free() destroy()
func _explode(pos: Vector3) -> void: func _explode(pos: Vector3) -> void:
var space_state = get_world_3d().direct_space_state var space_state = get_world_3d().direct_space_state
@@ -49,10 +174,12 @@ func _explode(pos: Vector3) -> void:
# Normalised distance (0 = epicenter, 1 = edge) # Normalised distance (0 = epicenter, 1 = edge)
var ndist = dist / explosion_radius 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_damage = damage * damage_intensity
var final_knockback = explosion_knockback * intensity var final_knockback = explosion_knockback * knockback_intensity
# Knockback direction: from explosion pos to target # Knockback direction: from explosion pos to target
var dir = (target_pos - pos).normalized() var dir = (target_pos - pos).normalized()
+83
View File
@@ -8,6 +8,88 @@ var initial_aim_dir: Vector3 = Vector3.ZERO
var cruise_speed: float = 60.0 var cruise_speed: float = 60.0
var ignition_timer: float = 0.5 var ignition_timer: float = 0.5
var ignited: bool = false 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: func _physics_process(delta: float) -> void:
if not ignited: if not ignited:
@@ -15,6 +97,7 @@ func _physics_process(delta: float) -> void:
if ignition_timer <= 0.0: if ignition_timer <= 0.0:
ignited = true ignited = true
drop_gravity = 0.0 drop_gravity = 0.0
_start_homing_effects()
if ignited and is_instance_valid(weapon_source): if ignited and is_instance_valid(weapon_source):
var target_dir: Vector3 = initial_aim_dir var target_dir: Vector3 = initial_aim_dir
+1 -1
View File
@@ -76,7 +76,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
proj.bounciness = 0.4 # Heavy, loses momentum on bounce proj.bounciness = 0.4 # Heavy, loses momentum on bounce
# Explosive stats # 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.explosion_knockback = 60.0 # Massive knockback
proj.falloff_curve = 2.0 # Steep dropoff: "Should be an instant kill in closest 50%" 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) # (0-50% = dist<6. 6/12 = 0.5. 1.0 - 0.5^2 = 0.75 multiplier. 200 * 0.75 = 150 damage -> 1 shot kill)
+5 -2
View File
@@ -21,7 +21,7 @@ func _ready() -> void:
_previous_position = global_position _previous_position = global_position
# Automatically destroy after 5 seconds to prevent leaks # Automatically destroy after 5 seconds to prevent leaks
var t = get_tree().create_timer(5.0) var t = get_tree().create_timer(5.0)
t.timeout.connect(queue_free) t.timeout.connect(destroy)
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
# Apply gravity drop if any # Apply gravity drop if any
@@ -33,7 +33,7 @@ func _physics_process(delta: float) -> void:
distance_traveled += movement.length() distance_traveled += movement.length()
if distance_traveled > max_range: if distance_traveled > max_range:
queue_free() destroy()
return return
_check_collision() _check_collision()
@@ -63,4 +63,7 @@ func _on_hit(result: Dictionary) -> void:
var size = 0.4 if impact_type == "plasma" else 0.15 var size = 0.4 if impact_type == "plasma" else 0.15
ImpactSpawner.spawn(get_tree(), impact_type, result.position, result.normal, size) ImpactSpawner.spawn(get_tree(), impact_type, result.position, result.normal, size)
destroy()
func destroy() -> void:
queue_free() queue_free()
+1 -1
View File
@@ -98,7 +98,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
proj.owner_player = player proj.owner_player = player
# Explosive stats # Explosive stats
proj.explosion_radius = 8.0 # Moderate AoE proj.explosion_radius = 4.0 # Moderate AoE
proj.explosion_knockback = 45.0 # Strong knockback! proj.explosion_knockback = 45.0 # Strong knockback!
proj.can_self_damage = false # As requested proj.can_self_damage = false # As requested
+3 -3
View File
@@ -109,7 +109,7 @@ func _fire() -> void:
var tween = create_tween() var tween = create_tween()
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.1) 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 var base_dir = -camera.global_transform.basis.z
_current_aim_dir = base_dir _current_aim_dir = base_dir
@@ -162,7 +162,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
# Logic # Logic
proj.weapon_source = self 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.position = origin # Start at origin to spread outward around camera
proj.logical_source = origin proj.logical_source = origin
@@ -184,7 +184,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
proj.owner_player = player proj.owner_player = player
# Explosive stats # Explosive stats
proj.explosion_radius = 3.0 # Tiny AoE effect proj.explosion_radius = 1.5 # Tiny AoE effect
proj.explosion_knockback = 5.0 proj.explosion_knockback = 5.0
proj.can_self_damage = false proj.can_self_damage = false