Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0912669759 | ||
|
|
ca2a80e38b | ||
|
|
00f190d423 | ||
|
|
2c35f5074a | ||
|
|
9f3264170e | ||
|
|
eee1925b7a | ||
|
|
da16a3a757 | ||
|
|
ea96b41287 | ||
|
|
dcd0df2b2c | ||
|
|
bd04135dba | ||
|
|
09103ce366 | ||
|
|
e6a2199777 | ||
|
|
c32f45a8aa | ||
|
|
7b7107c5d6 | ||
|
|
5ae6e20039 | ||
|
|
1ccc66a601 | ||
|
|
67bd5b4302 | ||
|
|
13ca2f46e9 | ||
|
|
a8b3619f2e | ||
|
|
d8cb91e593 | ||
|
|
5c2d648801 | ||
|
|
02c6caa0d6 | ||
|
|
8c928c2d92 | ||
|
|
a86ef53622 | ||
|
|
cb04df1df3 | ||
|
|
e65e9fa76f | ||
|
|
e9112d9b3d | ||
|
|
5e9f4bec8a | ||
|
|
f2afd31415 | ||
|
|
8eea7d6e36 | ||
|
|
45fb7f0f38 |
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.
@@ -57,10 +57,8 @@ func _process(delta: float) -> void:
|
||||
var hspeed := Vector2(player.velocity.x, player.velocity.z).length()
|
||||
|
||||
# Dash FOV (highest priority)
|
||||
if hspeed > params.dash_speed * 0.8:
|
||||
if hspeed > params.walk_speed * 1.5:
|
||||
_target_fov = params.dash_fov
|
||||
elif hspeed > params.sprint_speed * 0.8:
|
||||
_target_fov = params.sprint_fov
|
||||
|
||||
camera.fov = lerpf(camera.fov, _target_fov, 1.0 - exp(-params.fov_lerp_speed * delta))
|
||||
|
||||
|
||||
+118
-41
@@ -7,15 +7,24 @@ class_name TestLevelBuilder
|
||||
var _speed_label: Label
|
||||
var _state_label: Label
|
||||
var _chain_label: Label
|
||||
var _weapon_label: Label
|
||||
var _player: CharacterBody3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
build()
|
||||
# Hide mouse
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
_build_materials()
|
||||
_build_geometry()
|
||||
_build_hud()
|
||||
|
||||
|
||||
func build() -> void:
|
||||
_build_input_map()
|
||||
func _build_materials() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _build_geometry() -> void:
|
||||
_build_environment()
|
||||
_build_floor()
|
||||
_build_walls_arena()
|
||||
@@ -23,9 +32,9 @@ func build() -> void:
|
||||
_build_platforms()
|
||||
_build_wall_run_corridor()
|
||||
_build_speed_corridor()
|
||||
_build_target_dummy()
|
||||
_build_lighting()
|
||||
_build_player()
|
||||
_build_hud()
|
||||
|
||||
|
||||
# ── Utility ───────────────────────────────────────────────────────────────────
|
||||
@@ -56,40 +65,6 @@ func _ramp_static(pos: Vector3, size: Vector3, rot_deg: Vector3, color: Color, n
|
||||
return body
|
||||
|
||||
|
||||
# ── Input Map ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_input_map() -> void:
|
||||
var bindings := {
|
||||
"jump": [KEY_SPACE],
|
||||
"sprint": [KEY_SHIFT],
|
||||
"crouch": [KEY_CTRL],
|
||||
"dash": [KEY_SHIFT],
|
||||
"fire": [MOUSE_BUTTON_RIGHT, KEY_F],
|
||||
"fire_alt": [MOUSE_BUTTON_LEFT],
|
||||
"reload": [KEY_R],
|
||||
"interact": [KEY_E],
|
||||
"move_forward": [KEY_W],
|
||||
"move_back": [KEY_S],
|
||||
"move_left": [KEY_A],
|
||||
"move_right": [KEY_D],
|
||||
"weapon_next": [MOUSE_BUTTON_WHEEL_UP],
|
||||
"weapon_prev": [MOUSE_BUTTON_WHEEL_DOWN],
|
||||
}
|
||||
for action in bindings:
|
||||
if not InputMap.has_action(action):
|
||||
InputMap.add_action(action)
|
||||
InputMap.action_erase_events(action)
|
||||
for code in bindings[action]:
|
||||
if code in [KEY_SPACE, KEY_SHIFT, KEY_CTRL, KEY_R, KEY_F, KEY_E, KEY_W, KEY_S, KEY_A, KEY_D]:
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = code
|
||||
InputMap.action_add_event(action, ev)
|
||||
else:
|
||||
var ev := InputEventMouseButton.new()
|
||||
ev.button_index = code
|
||||
InputMap.action_add_event(action, ev)
|
||||
|
||||
|
||||
# ── Environment ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_environment() -> void:
|
||||
@@ -107,7 +82,7 @@ func _build_environment() -> void:
|
||||
environment.sky = sky
|
||||
environment.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||||
environment.ambient_light_energy = 0.4
|
||||
environment.tonemap_mode = 2 # Filmic
|
||||
environment.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||
environment.glow_enabled = true
|
||||
environment.glow_intensity = 0.3
|
||||
environment.glow_bloom = 0.1
|
||||
@@ -234,6 +209,15 @@ func _build_speed_corridor() -> void:
|
||||
_box_static(Vector3(34, 0.15, float(i)), Vector3(8, 0.3, 0.5), Color(0.8, 0.8, 0.2), "SpeedBump_%d" % i)
|
||||
|
||||
|
||||
# ── Dummies ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_target_dummy() -> void:
|
||||
var dummy = load("res://entities/target_dummy.gd").new()
|
||||
dummy.name = "TargetDummy"
|
||||
# Place it somewhat centrally in the main arena, but out of the immediate spawn area
|
||||
dummy.position = Vector3(0, 0, -10)
|
||||
add_child(dummy)
|
||||
|
||||
# ── Lighting ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_lighting() -> void:
|
||||
@@ -262,8 +246,9 @@ func _build_lighting() -> void:
|
||||
func _build_player() -> void:
|
||||
var player := CharacterBody3D.new()
|
||||
player.name = "Player"
|
||||
player.position = Vector3(0, 1.0, 8)
|
||||
player.position = Vector3(0, 1.0, 0)
|
||||
add_child(player)
|
||||
_player = player
|
||||
|
||||
# Collision shape
|
||||
var col_shape := CollisionShape3D.new()
|
||||
@@ -317,6 +302,15 @@ func _build_player() -> void:
|
||||
camera.fov = 90.0
|
||||
head_pivot.add_child(camera)
|
||||
|
||||
# ── Weapon Setup ────────────────────────────────────────────────────────────
|
||||
var wmanager_script = preload("res://weapons/weapon_manager.gd")
|
||||
if wmanager_script:
|
||||
var wman = wmanager_script.new()
|
||||
wman.name = "WeaponManager"
|
||||
wman.player = player
|
||||
wman.camera = camera
|
||||
camera.add_child(wman)
|
||||
|
||||
var rig_script = preload("res://characters/player/fps_camera_rig.gd")
|
||||
if rig_script:
|
||||
head_pivot.set_script(rig_script)
|
||||
@@ -449,11 +443,67 @@ func _build_hud() -> void:
|
||||
|
||||
var help := Label.new()
|
||||
help.name = "HelpLabel"
|
||||
help.text = "WASD Move | Shift Sprint | Space Jump | Double-tap Space Double Jump\nCtrl+Sprint Slide | LShift Dash (air) | Wall Run: run alongside walls | ESC toggle mouse"
|
||||
|
||||
var k_f = _get_key_name("move_forward")
|
||||
var k_l = _get_key_name("move_left")
|
||||
var k_b = _get_key_name("move_back")
|
||||
var k_r = _get_key_name("move_right")
|
||||
var k_j = _get_key_name("jump")
|
||||
var k_c = _get_key_name("crouch")
|
||||
var k_d = _get_key_name("dash")
|
||||
var k_fire = _get_key_name("fire")
|
||||
|
||||
var help_text = "%s%s%s%s Move | %s Jump | %s (in air) Double Jump\n%s (moving fast) Slide | %s Dash | %s Fire | ESC Pause Menu" % [k_f, k_l, k_b, k_r, k_j, k_j, k_c, k_d, k_fire]
|
||||
help.text = help_text
|
||||
help.add_theme_font_size_override("font_size", 13)
|
||||
help.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8, 0.7))
|
||||
help_panel.add_child(help)
|
||||
|
||||
# ── Weapon & Ammo Panel ───────────────────────────────────────────────
|
||||
var wp_panel := PanelContainer.new()
|
||||
wp_panel.name = "WeaponPanel"
|
||||
wp_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
wp_panel.offset_left = -250
|
||||
wp_panel.offset_top = -100
|
||||
wp_panel.offset_right = -20
|
||||
wp_panel.offset_bottom = -20
|
||||
var wp_style := StyleBoxFlat.new()
|
||||
wp_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
wp_style.corner_radius_top_left = 8
|
||||
wp_style.corner_radius_top_right = 8
|
||||
wp_style.corner_radius_bottom_left = 8
|
||||
wp_style.corner_radius_bottom_right = 8
|
||||
wp_style.content_margin_left = 16
|
||||
wp_style.content_margin_top = 12
|
||||
wp_style.content_margin_right = 16
|
||||
wp_style.content_margin_bottom = 12
|
||||
wp_panel.add_theme_stylebox_override("panel", wp_style)
|
||||
canvas.add_child(wp_panel)
|
||||
|
||||
_weapon_label = Label.new()
|
||||
_weapon_label.name = "WeaponLabel"
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
_weapon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_weapon_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
_weapon_label.add_theme_font_size_override("font_size", 24)
|
||||
wp_panel.add_child(_weapon_label)
|
||||
|
||||
|
||||
# ── Utility: Key Name ─────────────────────────────────────────────────────────
|
||||
|
||||
func _get_key_name(action: String) -> String:
|
||||
if not InputMap.has_action(action):
|
||||
return "?"
|
||||
var events = InputMap.action_get_events(action)
|
||||
for e in events:
|
||||
if e is InputEventKey:
|
||||
var code = e.physical_keycode if e.physical_keycode != 0 else e.keycode
|
||||
return OS.get_keycode_string(code)
|
||||
elif e is InputEventMouseButton:
|
||||
if e.button_index == MOUSE_BUTTON_LEFT: return "LClick"
|
||||
elif e.button_index == MOUSE_BUTTON_RIGHT: return "RClick"
|
||||
elif e.button_index == MOUSE_BUTTON_MIDDLE: return "MClick"
|
||||
return "?"
|
||||
|
||||
# ── HUD Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -477,3 +527,30 @@ func _process(_delta: float) -> void:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_chain_label.text = "Chain: %d (+%d%%)" % [sm.chain_count, int(sm.current_chain_bonus * 100)]
|
||||
|
||||
if _weapon_label:
|
||||
var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||
if wman and wman.weapons.has(wman.active_slot):
|
||||
var active_weapon = wman.weapons[wman.active_slot]
|
||||
var w_name = "Weapon"
|
||||
var cur_ammo = 0
|
||||
var max_ammo = 0
|
||||
|
||||
if "weapon_name" in active_weapon:
|
||||
w_name = active_weapon.weapon_name
|
||||
elif active_weapon is DoubleBarrelShotgun:
|
||||
w_name = "Double Barrel Shotgun"
|
||||
|
||||
if "current_ammo" in active_weapon:
|
||||
cur_ammo = active_weapon.current_ammo
|
||||
max_ammo = active_weapon.max_ammo
|
||||
elif "shells" in active_weapon:
|
||||
cur_ammo = active_weapon.shells
|
||||
max_ammo = 2
|
||||
|
||||
if "reloading" in active_weapon and active_weapon.reloading:
|
||||
_weapon_label.text = "%s\nReloading..." % w_name
|
||||
else:
|
||||
_weapon_label.text = "%s\n%d / %d" % [w_name, cur_ammo, max_ammo]
|
||||
else:
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
extends StaticBody3D
|
||||
class_name TargetDummy
|
||||
|
||||
var mesh_instance: MeshInstance3D
|
||||
var dps_label: Label3D
|
||||
|
||||
# Array of dictionaries: { "time": float, "amount": float }
|
||||
var damage_history: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("enemies") # Just in case we need to filter
|
||||
|
||||
# Collision Shape
|
||||
var shape = CollisionShape3D.new()
|
||||
shape.shape = CapsuleShape3D.new()
|
||||
shape.shape.radius = 0.5
|
||||
shape.shape.height = 2.0
|
||||
shape.position = Vector3(0, 1.0, 0)
|
||||
add_child(shape)
|
||||
|
||||
# Mesh
|
||||
mesh_instance = MeshInstance3D.new()
|
||||
var mesh = CapsuleMesh.new()
|
||||
mesh.radius = 0.5
|
||||
mesh.height = 2.0
|
||||
mesh_instance.mesh = mesh
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.8, 0.2, 0.2) # Red dummy
|
||||
mat.roughness = 0.7
|
||||
mesh.material = mat
|
||||
mesh_instance.position = Vector3(0, 1.0, 0)
|
||||
add_child(mesh_instance)
|
||||
|
||||
# DPS Label
|
||||
dps_label = Label3D.new()
|
||||
dps_label.text = "DPS: 0"
|
||||
dps_label.pixel_size = 0.01
|
||||
dps_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
dps_label.position = Vector3(0, 2.3, 0)
|
||||
dps_label.modulate = Color(1, 0.8, 0)
|
||||
add_child(dps_label)
|
||||
|
||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
||||
var now = Time.get_ticks_msec() / 1000.0
|
||||
damage_history.append({"time": now, "amount": amount})
|
||||
|
||||
if source and source.has_method("spawn_damage_number"):
|
||||
source.spawn_damage_number(amount, hit_position)
|
||||
|
||||
_wiggle()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_update_dps()
|
||||
|
||||
func _update_dps() -> void:
|
||||
var now = Time.get_ticks_msec() / 1000.0
|
||||
var total_damage: float = 0.0
|
||||
|
||||
# Keep only last 5 seconds
|
||||
var valid_history = []
|
||||
for dmg in damage_history:
|
||||
if now - dmg["time"] <= 5.0:
|
||||
valid_history.append(dmg)
|
||||
total_damage += dmg["amount"]
|
||||
|
||||
damage_history = valid_history
|
||||
|
||||
if damage_history.size() > 0:
|
||||
var dps = total_damage / 5.0
|
||||
dps_label.text = "DPS: %.1f" % dps
|
||||
else:
|
||||
dps_label.text = "DPS: 0"
|
||||
|
||||
func _wiggle() -> void:
|
||||
# Small tween to shake the dummy
|
||||
var tween = create_tween()
|
||||
var orig_pos = Vector3(0, 1.0, 0)
|
||||
var wiggle_dir = Vector3(randf_range(-0.1, 0.1), 0, randf_range(-0.1, 0.1))
|
||||
|
||||
tween.tween_property(mesh_instance, "position", orig_pos + wiggle_dir, 0.05)
|
||||
tween.tween_property(mesh_instance, "position", orig_pos - wiggle_dir, 0.05)
|
||||
tween.tween_property(mesh_instance, "position", orig_pos, 0.05)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bts5u08jyv5la
|
||||
@@ -0,0 +1,436 @@
|
||||
import math
|
||||
import random
|
||||
import struct
|
||||
import wave
|
||||
import os
|
||||
|
||||
os.makedirs('assets/sounds', exist_ok=True)
|
||||
|
||||
def write_wav(filename, samples, sample_rate=44100):
|
||||
with wave.open(filename, 'w') as wav_file:
|
||||
wav_file.setnchannels(1)
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(sample_rate)
|
||||
for s in samples:
|
||||
# Clamp and convert to 16-bit
|
||||
val = int(max(-1.0, min(1.0, s)) * 32767)
|
||||
data = struct.pack('<h', val)
|
||||
wav_file.writeframesraw(data)
|
||||
|
||||
# 1. Shotgun Fire Sound (Bang + Boom + Decay)
|
||||
sample_rate = 44100
|
||||
duration = 0.8
|
||||
num_samples = int(sample_rate * duration)
|
||||
fire_samples = []
|
||||
|
||||
# Low-pass filter states
|
||||
prev_val1 = 0.0
|
||||
prev_val2 = 0.0
|
||||
|
||||
for i in range(num_samples):
|
||||
t = i / sample_rate
|
||||
|
||||
# Base broadband noise
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
|
||||
# 1. The Crack (Initial explosion, very sharp envelope)
|
||||
crack_env = math.exp(-t * 50.0)
|
||||
crack = noise * crack_env * 1.5
|
||||
|
||||
# 2. The Body (Deep explosive boom, slower decay)
|
||||
body_env = math.exp(-t * 6.0)
|
||||
|
||||
# Dynamic low-pass filter (closes rapidly to muffle the tail into a boom)
|
||||
alpha = max(0.01, 0.2 * math.exp(-t * 15.0))
|
||||
filtered1 = prev_val1 + alpha * (noise - prev_val1)
|
||||
prev_val1 = filtered1
|
||||
|
||||
# Second pass for deeper muffle
|
||||
filtered2 = prev_val2 + alpha * (filtered1 - prev_val2)
|
||||
prev_val2 = filtered2
|
||||
|
||||
body = filtered2 * body_env * 10.0 # Boost the bass significantly
|
||||
|
||||
# Mix crack and body
|
||||
raw_sample = crack + body
|
||||
|
||||
# Soft clipping (tanh) to simulate a microphone peaking from a loud blast
|
||||
sample = math.tanh(raw_sample * 2.5)
|
||||
|
||||
fire_samples.append(sample)
|
||||
|
||||
write_wav('assets/sounds/shotgun_fire.wav', fire_samples)
|
||||
|
||||
|
||||
# 2. Shotgun Reload Sound (Break, Insert, Snap)
|
||||
duration = 1.0
|
||||
num_samples = int(sample_rate * duration)
|
||||
reload_samples = []
|
||||
|
||||
for i in range(num_samples):
|
||||
t = i / sample_rate
|
||||
sample = 0.0
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
|
||||
# Phase 1: Break open (t=0.0)
|
||||
if t < 0.2:
|
||||
env_click = math.exp(-t * 60.0)
|
||||
click = noise * env_click * 0.5
|
||||
clunk_freq = 300.0 * math.exp(-t * 20.0) + 100.0
|
||||
clunk = math.sin(t * clunk_freq * 2 * math.pi) * math.exp(-t * 15.0) * 0.4
|
||||
sample += click + clunk
|
||||
|
||||
# Phase 2: Insert shells (t=0.3 to 0.6)
|
||||
if 0.3 <= t < 0.6:
|
||||
t2 = t - 0.3
|
||||
slide_env = math.sin(t2 * math.pi / 0.3) * 0.15
|
||||
slide = noise * slide_env
|
||||
if t2 > 0.15:
|
||||
seat_env = math.exp(-(t2 - 0.15) * 40.0)
|
||||
seat_click = noise * seat_env * 0.4
|
||||
sample += seat_click
|
||||
sample += slide
|
||||
|
||||
# Phase 3: Snap shut (t=0.7)
|
||||
if t >= 0.7:
|
||||
t3 = t - 0.7
|
||||
snap_env = math.exp(-t3 * 40.0)
|
||||
snap = noise * snap_env * 0.8
|
||||
ring_env = math.exp(-t3 * 10.0)
|
||||
ring1 = math.sin(t3 * 3500.0 * 2 * math.pi)
|
||||
ring2 = math.sin(t3 * 4200.0 * 2 * math.pi)
|
||||
ring = (ring1 + ring2) * ring_env * 0.1
|
||||
thud = math.sin(t3 * 80.0 * 2 * math.pi) * math.exp(-t3 * 15.0) * 0.5
|
||||
sample += snap + ring + thud
|
||||
|
||||
reload_samples.append(sample)
|
||||
|
||||
# Global low-pass to smooth out the harsh digital noise
|
||||
prev = 0.0
|
||||
for i in range(len(reload_samples)):
|
||||
prev = prev + 0.5 * (reload_samples[i] - prev)
|
||||
reload_samples[i] = max(-1.0, min(1.0, prev * 1.5))
|
||||
|
||||
write_wav('assets/sounds/shotgun_reload.wav', reload_samples)
|
||||
|
||||
# 3. Footstep Sound (Quiet Thud + Scrape)
|
||||
footstep_duration = 0.15
|
||||
footstep_samples = []
|
||||
for i in range(int(sample_rate * footstep_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
# Dull Thud (lower frequency, less metallic)
|
||||
thud_env = math.exp(-t * 25.0)
|
||||
thud = math.sin(t * 50.0 * 2 * math.pi) * thud_env * 0.15
|
||||
# Scrape (very quiet, low-passed)
|
||||
scrape_env = math.exp(-t * 50.0)
|
||||
scrape = noise * scrape_env * 0.03
|
||||
footstep_samples.append(thud + scrape)
|
||||
|
||||
# Filter footstep to ensure no tin-can high frequencies
|
||||
prev = 0.0
|
||||
for i in range(len(footstep_samples)):
|
||||
prev = prev + 0.3 * (footstep_samples[i] - prev)
|
||||
footstep_samples[i] = prev
|
||||
|
||||
write_wav('assets/sounds/footstep.wav', footstep_samples)
|
||||
|
||||
# 4. Dash Sound (Whoosh)
|
||||
dash_duration = 0.5
|
||||
dash_samples = []
|
||||
prev_dash = 0.0
|
||||
for i in range(int(sample_rate * dash_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
env = math.sin(t * math.pi / dash_duration) ** 2 # Smooth bell curve
|
||||
|
||||
alpha = 0.1
|
||||
filtered = prev_dash + alpha * (noise - prev_dash)
|
||||
prev_dash = filtered
|
||||
|
||||
dash_samples.append(filtered * env * 0.8)
|
||||
write_wav('assets/sounds/dash.wav', dash_samples)
|
||||
|
||||
# 5. Hit Confirm (Tink/Chirp)
|
||||
hit_duration = 0.2
|
||||
hit_samples = []
|
||||
for i in range(int(sample_rate * hit_duration)):
|
||||
t = i / sample_rate
|
||||
env = math.exp(-t * 30.0)
|
||||
# High pitch chirp sweeping slightly
|
||||
freq = 2000.0 + math.exp(-t * 20.0) * 1000.0
|
||||
tink = math.sin(t * freq * 2 * math.pi) * env * 0.3
|
||||
hit_samples.append(tink)
|
||||
write_wav('assets/sounds/hit_confirm.wav', hit_samples)
|
||||
|
||||
# 6. Wallrun (Continuous scrape)
|
||||
# We want this to seamlessly loop
|
||||
wallrun_duration = 1.0
|
||||
wallrun_samples = []
|
||||
for i in range(int(sample_rate * wallrun_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
# Give it a slight 4/4 rhythm
|
||||
rhythm = 0.8 + 0.2 * math.sin(t * 8.0 * math.pi)
|
||||
|
||||
# Low pass to keep it as a dull scrape
|
||||
alpha = 0.05
|
||||
sample = noise * alpha * rhythm * 0.4
|
||||
wallrun_samples.append(sample)
|
||||
write_wav('assets/sounds/wallrun.wav', wallrun_samples)
|
||||
|
||||
# 7. Slide (Continuous deep friction)
|
||||
slide_duration = 1.0
|
||||
slide_samples = []
|
||||
for i in range(int(sample_rate * slide_duration)):
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
# Deeper, steadier scrape than wallrun
|
||||
alpha = 0.02
|
||||
sample = noise * alpha * 0.5
|
||||
slide_samples.append(sample)
|
||||
write_wav('assets/sounds/slide.wav', slide_samples)
|
||||
|
||||
# 8. Wind (Subtle rushing air)
|
||||
wind_duration = 2.0 # Generate 2 seconds
|
||||
sample_count = int(sample_rate * wind_duration)
|
||||
raw_wind = []
|
||||
prev_wind = 0.0
|
||||
|
||||
# Generate extra long to stabilize filter
|
||||
for i in range(sample_count + int(sample_rate * 0.5)):
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
alpha = 0.015
|
||||
filtered = prev_wind + alpha * (noise - prev_wind)
|
||||
prev_wind = filtered
|
||||
if i >= int(sample_rate * 0.5):
|
||||
raw_wind.append(filtered * 0.5)
|
||||
|
||||
# Crossfade the last 0.1s into the first 0.1s to make it perfectly loop
|
||||
crossfade_samples = int(sample_rate * 0.1)
|
||||
loop_wind = raw_wind[:-crossfade_samples] # remove the tail
|
||||
|
||||
for i in range(crossfade_samples):
|
||||
# Blend the tail into the start
|
||||
tail_sample = raw_wind[-(crossfade_samples - i)]
|
||||
start_sample = loop_wind[i]
|
||||
t = i / float(crossfade_samples)
|
||||
loop_wind[i] = start_sample * t + tail_sample * (1.0 - t)
|
||||
|
||||
write_wav('assets/sounds/wind.wav', loop_wind)
|
||||
|
||||
# 9. Jump Sound (Quick exertion / push)
|
||||
jump_duration = 0.2
|
||||
jump_samples = []
|
||||
for i in range(int(sample_rate * jump_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
# Lower frequency push (less tin can)
|
||||
push_env = math.exp(-t * 25.0)
|
||||
push = math.sin(t * 60.0 * 2 * math.pi) * push_env * 0.3
|
||||
# High frequency clothing ruffle (quieter)
|
||||
ruffle_env = math.exp(-t * 30.0)
|
||||
ruffle = noise * ruffle_env * 0.05
|
||||
jump_samples.append(push + ruffle)
|
||||
|
||||
# Filter jump to ensure no tin-can high frequencies
|
||||
prev = 0.0
|
||||
for i in range(len(jump_samples)):
|
||||
prev = prev + 0.4 * (jump_samples[i] - prev)
|
||||
jump_samples[i] = prev
|
||||
|
||||
write_wav('assets/sounds/jump.wav', jump_samples)
|
||||
|
||||
# 10. Double Jump Sound (Airy, cloudy whoosh)
|
||||
dj_duration = 0.4
|
||||
dj_samples = []
|
||||
prev_dj = 0.0
|
||||
for i in range(int(sample_rate * dj_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
# Airy filter
|
||||
alpha = 0.05
|
||||
filtered = prev_dj + alpha * (noise - prev_dj)
|
||||
prev_dj = filtered
|
||||
|
||||
# Envelope: soft attack, long decay
|
||||
env = (math.exp(-t * 8.0) - math.exp(-t * 40.0)) * 1.5
|
||||
|
||||
# Slight pitch movement to give it a "lifting" feel
|
||||
lift = math.sin(t * 150.0 * 2 * math.pi * (1.0 + t)) * env * 0.1
|
||||
|
||||
dj_samples.append((filtered * 0.4 + lift) * env)
|
||||
write_wav('assets/sounds/double_jump.wav', dj_samples)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# NEW WEAPONS (AK47, M4, MP7, DMR, Plasma, Nail)
|
||||
# ==========================================
|
||||
|
||||
# 11. AK47 (Heavy punch, low thud, sharp crack)
|
||||
ak_duration = 0.2
|
||||
ak_samples = []
|
||||
for i in range(int(sample_rate * ak_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
|
||||
# Sharp metallic crack
|
||||
crack_env = math.exp(-t * 100.0)
|
||||
crack = noise * crack_env * 0.4
|
||||
|
||||
# Deep heavy punch (70Hz sweep down to 40Hz)
|
||||
punch_freq = max(40.0, 70.0 - t * 300.0)
|
||||
punch_env = math.exp(-t * 20.0)
|
||||
punch = math.sin(t * punch_freq * 2 * math.pi) * punch_env * 0.6
|
||||
|
||||
# Mechanism ring
|
||||
ring_env = math.exp(-t * 15.0)
|
||||
ring = math.sin(t * 800.0 * 2 * math.pi) * ring_env * 0.05
|
||||
|
||||
ak_samples.append(crack + punch + ring)
|
||||
write_wav('assets/sounds/ak47_fire.wav', ak_samples)
|
||||
|
||||
|
||||
# 12. M4 (Moderate punch, sharper crack, tighter burst)
|
||||
m4_duration = 0.15
|
||||
m4_samples = []
|
||||
for i in range(int(sample_rate * m4_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
|
||||
crack_env = math.exp(-t * 120.0)
|
||||
crack = noise * crack_env * 0.5
|
||||
|
||||
punch_freq = 90.0
|
||||
punch_env = math.exp(-t * 25.0)
|
||||
punch = math.sin(t * punch_freq * 2 * math.pi) * punch_env * 0.4
|
||||
|
||||
m4_samples.append(crack + punch)
|
||||
write_wav('assets/sounds/m4_fire.wav', m4_samples)
|
||||
|
||||
|
||||
# 13. MP7 (Very fast, light pop, high pitched)
|
||||
mp7_duration = 0.1
|
||||
mp7_samples = []
|
||||
for i in range(int(sample_rate * mp7_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
|
||||
pop_env = math.exp(-t * 80.0)
|
||||
pop = noise * pop_env * 0.3
|
||||
|
||||
punch_freq = 150.0
|
||||
punch_env = math.exp(-t * 35.0)
|
||||
punch = math.sin(t * punch_freq * 2 * math.pi) * punch_env * 0.2
|
||||
|
||||
mp7_samples.append(pop + punch)
|
||||
write_wav('assets/sounds/mp7_fire.wav', mp7_samples)
|
||||
|
||||
|
||||
# 14. DMR (Extremely loud, echoing boom, very sharp)
|
||||
dmr_duration = 0.4
|
||||
dmr_samples = []
|
||||
for i in range(int(sample_rate * dmr_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
|
||||
crack_env = math.exp(-t * 50.0)
|
||||
crack = noise * crack_env * 0.6
|
||||
|
||||
boom_freq = max(30.0, 60.0 - t * 100.0)
|
||||
boom_env = math.exp(-t * 8.0)
|
||||
boom = math.sin(t * boom_freq * 2 * math.pi) * boom_env * 0.7
|
||||
|
||||
tail_env = math.exp(-t * 5.0)
|
||||
tail = noise * tail_env * 0.1 # outdoor echo reflection
|
||||
|
||||
dmr_samples.append(crack + boom + tail)
|
||||
write_wav('assets/sounds/dmr_fire.wav', dmr_samples)
|
||||
|
||||
|
||||
# 15. Plasma Gun (Sci-Fi pew, tonal sweep)
|
||||
plasma_duration = 0.2
|
||||
plasma_samples = []
|
||||
for i in range(int(sample_rate * plasma_duration)):
|
||||
t = i / sample_rate
|
||||
|
||||
# Sweep from 1200 Hz down to 300 Hz rapidly
|
||||
freq = max(300.0, 1200.0 * math.exp(-t * 30.0))
|
||||
pew_env = math.exp(-t * 15.0)
|
||||
pew = math.sin(t * freq * 2 * math.pi) * pew_env * 0.6
|
||||
|
||||
plasma_samples.append(pew)
|
||||
write_wav('assets/sounds/plasma_fire.wav', plasma_samples)
|
||||
|
||||
|
||||
# 16. Nail Gun (Clack clack clack, metallic hit)
|
||||
nail_duration = 0.1
|
||||
nail_samples = []
|
||||
for i in range(int(sample_rate * nail_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
|
||||
# Sharp metallic impact
|
||||
hit_env = math.exp(-t * 150.0)
|
||||
hit = noise * hit_env * 0.4
|
||||
|
||||
# Ringing metal (two tones)
|
||||
ring_env = math.exp(-t * 40.0)
|
||||
ring = (math.sin(t * 3500.0 * 2 * math.pi) + math.sin(t * 4100.0 * 2 * math.pi)) * ring_env * 0.15
|
||||
|
||||
# Mechanism chunk
|
||||
chunk_env = math.exp(-t * 30.0)
|
||||
chunk = math.sin(t * 200.0 * 2 * math.pi) * chunk_env * 0.2
|
||||
|
||||
nail_samples.append(hit + ring + chunk)
|
||||
write_wav('assets/sounds/nailgun_fire.wav', nail_samples)
|
||||
|
||||
# 17. AWP (Thunderous crack, massive bass, slow echo)
|
||||
awp_duration = 0.6
|
||||
awp_samples = []
|
||||
for i in range(int(sample_rate * awp_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
crack = noise * math.exp(-t * 80.0) * 0.8
|
||||
boom = math.sin(t * max(30.0, 80.0 - t * 150.0) * 2 * math.pi) * math.exp(-t * 10.0) * 0.9
|
||||
awp_samples.append(crack + boom)
|
||||
write_wav('assets/sounds/awp_fire.wav', awp_samples)
|
||||
|
||||
# 18. Rocket Launcher (Deep thud, whoosh)
|
||||
rocket_duration = 0.4
|
||||
rocket_samples = []
|
||||
prev_r = 0.0
|
||||
for i in range(int(sample_rate * rocket_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
thud = math.sin(t * 50.0 * 2 * math.pi) * math.exp(-t * 20.0) * 0.7
|
||||
alpha = 0.05
|
||||
filtered = prev_r + alpha * (noise - prev_r)
|
||||
prev_r = filtered
|
||||
whoosh = filtered * math.exp(-t * 15.0) * 0.5
|
||||
rocket_samples.append(thud + whoosh)
|
||||
write_wav('assets/sounds/rocket_fire.wav', rocket_samples)
|
||||
|
||||
# 19. Swarm Launcher (High-pitched whistling zip)
|
||||
swarm_duration = 0.2
|
||||
swarm_samples = []
|
||||
for i in range(int(sample_rate * swarm_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
zip_freq = 600.0 + t * 2000.0
|
||||
pew = math.sin(t * zip_freq * 2 * math.pi) * math.exp(-t * 25.0) * 0.4
|
||||
swarm_samples.append(pew + noise * math.exp(-t * 50.0) * 0.2)
|
||||
write_wav('assets/sounds/swarm_fire.wav', swarm_samples)
|
||||
|
||||
# 20. Mortar (Heavy hollow thunk)
|
||||
mortar_duration = 0.3
|
||||
mortar_samples = []
|
||||
for i in range(int(sample_rate * mortar_duration)):
|
||||
t = i / sample_rate
|
||||
noise = random.uniform(-1.0, 1.0)
|
||||
thunk_freq = 60.0
|
||||
thunk = math.sin(t * thunk_freq * 2 * math.pi) * math.exp(-t * 15.0) * 0.7
|
||||
ring = math.sin(t * 200.0 * 2 * math.pi) * math.exp(-t * 30.0) * 0.2
|
||||
mortar_samples.append(thunk + ring)
|
||||
write_wav('assets/sounds/mortar_fire.wav', mortar_samples)
|
||||
|
||||
print("Generated all sounds!")
|
||||
@@ -0,0 +1,125 @@
|
||||
extends Node
|
||||
|
||||
const SAVE_PATH = "user://loadouts.json"
|
||||
|
||||
var weapon_db = {
|
||||
"none": {
|
||||
"name": "None",
|
||||
"category": "none",
|
||||
"script": ""
|
||||
},
|
||||
"double_barrel_shotgun": {
|
||||
"name": "Double Barrel Shotgun",
|
||||
"category": "primary",
|
||||
"script": "res://weapons/double_barrel_shotgun.gd"
|
||||
},
|
||||
"ak47": {
|
||||
"name": "AK-47",
|
||||
"category": "primary",
|
||||
"script": "res://weapons/ak47.gd"
|
||||
},
|
||||
"m4": {
|
||||
"name": "M4",
|
||||
"category": "primary",
|
||||
"script": "res://weapons/m4.gd"
|
||||
},
|
||||
"mp7": {
|
||||
"name": "MP7",
|
||||
"category": "primary",
|
||||
"script": "res://weapons/mp7.gd"
|
||||
},
|
||||
"dmr": {
|
||||
"name": "DMR",
|
||||
"category": "primary",
|
||||
"script": "res://weapons/dmr.gd"
|
||||
},
|
||||
"plasma_gun": {
|
||||
"name": "Plasma Gun",
|
||||
"category": "primary",
|
||||
"script": "res://weapons/plasma_gun.gd"
|
||||
},
|
||||
"nail_gun": {
|
||||
"name": "Nail Gun",
|
||||
"category": "primary",
|
||||
"script": "res://weapons/nail_gun.gd"
|
||||
},
|
||||
"awp": {
|
||||
"name": "AWP",
|
||||
"category": "special",
|
||||
"script": "res://weapons/awp.gd"
|
||||
},
|
||||
"rocket_launcher": {
|
||||
"name": "Rocket Launcher",
|
||||
"category": "special",
|
||||
"script": "res://weapons/rocket_launcher.gd"
|
||||
},
|
||||
"rocket_swarm": {
|
||||
"name": "Rocket Swarm",
|
||||
"category": "special",
|
||||
"script": "res://weapons/rocket_swarm.gd"
|
||||
},
|
||||
"mortar": {
|
||||
"name": "Mortar",
|
||||
"category": "special",
|
||||
"script": "res://weapons/mortar.gd"
|
||||
}
|
||||
}
|
||||
|
||||
var active_loadout_index: int = 0
|
||||
|
||||
var loadouts: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
_ensure_default_loadouts()
|
||||
load_loadouts()
|
||||
|
||||
func _ensure_default_loadouts() -> void:
|
||||
loadouts.clear()
|
||||
for i in range(5):
|
||||
loadouts.append({
|
||||
"name": "Loadout " + str(i + 1),
|
||||
"primary_1": "double_barrel_shotgun",
|
||||
"primary_2": "none",
|
||||
"special": "none"
|
||||
})
|
||||
|
||||
func get_active_loadout() -> Dictionary:
|
||||
if active_loadout_index >= 0 and active_loadout_index < loadouts.size():
|
||||
return loadouts[active_loadout_index]
|
||||
return loadouts[0]
|
||||
|
||||
func save_loadouts() -> void:
|
||||
var save_data = {
|
||||
"active_index": active_loadout_index,
|
||||
"loadouts": loadouts
|
||||
}
|
||||
var json_string = JSON.stringify(save_data)
|
||||
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
|
||||
func load_loadouts() -> void:
|
||||
if FileAccess.file_exists(SAVE_PATH):
|
||||
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var error = json.parse(json_string)
|
||||
if error == OK:
|
||||
var data = json.get_data()
|
||||
if data is Dictionary:
|
||||
if data.has("active_index"):
|
||||
active_loadout_index = data["active_index"]
|
||||
if data.has("loadouts") and data["loadouts"] is Array:
|
||||
var saved_loadouts = data["loadouts"]
|
||||
# Overwrite defaults, up to 5
|
||||
for i in range(min(5, saved_loadouts.size())):
|
||||
var sl = saved_loadouts[i]
|
||||
if sl is Dictionary:
|
||||
if sl.has("name"): loadouts[i]["name"] = sl["name"]
|
||||
if sl.has("primary_1"): loadouts[i]["primary_1"] = sl["primary_1"]
|
||||
if sl.has("primary_2"): loadouts[i]["primary_2"] = sl["primary_2"]
|
||||
if sl.has("special"): loadouts[i]["special"] = sl["special"]
|
||||
@@ -0,0 +1 @@
|
||||
uid://ck4n30620akl4
|
||||
@@ -0,0 +1,99 @@
|
||||
extends Node
|
||||
|
||||
const SAVE_PATH = "user://settings.json"
|
||||
|
||||
var default_bindings = {
|
||||
"jump": {"type": "key", "code": KEY_SPACE},
|
||||
"crouch": {"type": "key", "code": KEY_CTRL},
|
||||
"dash": {"type": "key", "code": KEY_SHIFT},
|
||||
"fire": {"type": "mouse", "code": MOUSE_BUTTON_LEFT},
|
||||
"fire_alt": {"type": "mouse", "code": MOUSE_BUTTON_RIGHT},
|
||||
"reload": {"type": "key", "code": KEY_R},
|
||||
"interact": {"type": "key", "code": KEY_E},
|
||||
"move_forward": {"type": "key", "code": KEY_W},
|
||||
"move_back": {"type": "key", "code": KEY_S},
|
||||
"move_left": {"type": "key", "code": KEY_A},
|
||||
"move_right": {"type": "key", "code": KEY_D},
|
||||
"weapon_next": {"type": "mouse", "code": MOUSE_BUTTON_WHEEL_UP},
|
||||
"weapon_prev": {"type": "mouse", "code": MOUSE_BUTTON_WHEEL_DOWN},
|
||||
"weapon_1": {"type": "key", "code": KEY_1},
|
||||
"weapon_2": {"type": "key", "code": KEY_2},
|
||||
"weapon_3": {"type": "key", "code": KEY_3},
|
||||
}
|
||||
|
||||
var current_bindings = {}
|
||||
|
||||
func _ready() -> void:
|
||||
# Initialize defaults
|
||||
current_bindings = default_bindings.duplicate(true)
|
||||
|
||||
# Load overrides
|
||||
_load_settings()
|
||||
|
||||
# Apply to InputMap
|
||||
_apply_all_bindings()
|
||||
|
||||
func _apply_all_bindings() -> void:
|
||||
for action in current_bindings:
|
||||
if not InputMap.has_action(action):
|
||||
InputMap.add_action(action)
|
||||
InputMap.action_erase_events(action)
|
||||
|
||||
var binding = current_bindings[action]
|
||||
var ev: InputEvent
|
||||
if binding["type"] == "key":
|
||||
ev = InputEventKey.new()
|
||||
ev.keycode = binding["code"]
|
||||
elif binding["type"] == "mouse":
|
||||
ev = InputEventMouseButton.new()
|
||||
ev.button_index = binding["code"]
|
||||
|
||||
if ev:
|
||||
InputMap.action_add_event(action, ev)
|
||||
|
||||
func rebind_action(action: String, event: InputEvent) -> void:
|
||||
if not current_bindings.has(action):
|
||||
return
|
||||
|
||||
if event is InputEventKey:
|
||||
var code = event.physical_keycode if event.physical_keycode != 0 else event.keycode
|
||||
current_bindings[action] = {"type": "key", "code": code}
|
||||
elif event is InputEventMouseButton:
|
||||
current_bindings[action] = {"type": "mouse", "code": event.button_index}
|
||||
else:
|
||||
return
|
||||
|
||||
_apply_all_bindings()
|
||||
_save_settings()
|
||||
|
||||
func reset_to_defaults() -> void:
|
||||
current_bindings = default_bindings.duplicate(true)
|
||||
_apply_all_bindings()
|
||||
_save_settings()
|
||||
|
||||
func _save_settings() -> void:
|
||||
var save_data = {
|
||||
"bindings": current_bindings
|
||||
}
|
||||
var json_string = JSON.stringify(save_data)
|
||||
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_string(json_string)
|
||||
file.close()
|
||||
|
||||
func _load_settings() -> void:
|
||||
if FileAccess.file_exists(SAVE_PATH):
|
||||
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||||
if file:
|
||||
var json_string = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var error = json.parse(json_string)
|
||||
if error == OK:
|
||||
var data = json.get_data()
|
||||
if data is Dictionary and data.has("bindings"):
|
||||
var loaded_bindings = data["bindings"]
|
||||
for action in loaded_bindings:
|
||||
if current_bindings.has(action):
|
||||
current_bindings[action] = loaded_bindings[action]
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6fe1eyas7wxv
|
||||
@@ -2,8 +2,7 @@ extends Resource
|
||||
class_name MovementParams
|
||||
|
||||
# ── Ground ────────────────────────────────────────────────────────────────────
|
||||
@export var walk_speed: float = 7.0
|
||||
@export var sprint_speed: float = 11.0
|
||||
@export var walk_speed: float = 11.0
|
||||
@export var crouch_speed: float = 4.0
|
||||
@export var ground_friction: float = 10.0
|
||||
@export var ground_acceleration: float = 50.0
|
||||
@@ -43,20 +42,23 @@ class_name MovementParams
|
||||
@export var wall_run_duration: float = 1.2
|
||||
@export var wall_run_gravity: float = 6.0
|
||||
@export var wall_run_auto_jump_speed: float = 10.0
|
||||
@export var wall_run_jump_horizontal: float = 14.0
|
||||
@export var wall_run_jump_off_normal: float = 9.0
|
||||
@export var wall_run_jump_horizontal: float = 8.0
|
||||
@export var wall_run_jump_off_normal: float = 5.0
|
||||
@export var wall_angle_threshold: float = 70.0
|
||||
@export var wall_detect_distance: float = 0.7
|
||||
@export var wall_ray_up_height: float = 0.6
|
||||
@export var wall_ray_down_height: float = -0.3
|
||||
|
||||
# ── Jumping ───────────────────────────────────────────────────────────────────
|
||||
@export var jump_cooldown: float = 0.27
|
||||
|
||||
# ── Wall Cling ────────────────────────────────────────────────────────────────
|
||||
@export var wall_cling_slide_speed: float = -1.5
|
||||
@export var wall_cling_stamina_drain: float = 2.0
|
||||
@export var wall_cling_max_stamina: float = 2.0
|
||||
|
||||
# ── Dash ──────────────────────────────────────────────────────────────────────
|
||||
@export var dash_speed: float = 18.0
|
||||
@export var dash_speed: float = 10.0
|
||||
@export var dash_duration: float = 0.25
|
||||
@export var dash_cooldown: float = 1.0
|
||||
@export var dash_invulnerability_time: float = 0.1
|
||||
@@ -68,7 +70,7 @@ class_name MovementParams
|
||||
@export var rocket_jump_falloff: float = 1.5
|
||||
|
||||
# ── Shotgun Jump ──────────────────────────────────────────────────────────────
|
||||
@export var shotgun_jump_impulse: float = 20.0
|
||||
@export var shotgun_jump_impulse: float = 10.0
|
||||
|
||||
# ── Double Jump ───────────────────────────────────────────────────────────────
|
||||
@export var double_jump_velocity: float = 8.0
|
||||
@@ -76,13 +78,13 @@ class_name MovementParams
|
||||
|
||||
# ── Chaining ──────────────────────────────────────────────────────────────────
|
||||
@export var chain_bonus_per_success: float = 0.05
|
||||
@export var chain_bonus_cap: float = 0.50
|
||||
@export var chain_bonus_cap: float = 0.25
|
||||
@export var chain_bonus_decay_rate: float = 0.5
|
||||
@export var chain_window: float = 1.5
|
||||
|
||||
# ── Camera ────────────────────────────────────────────────────────────────────
|
||||
@export var mouse_sensitivity: float = 0.002
|
||||
@export var base_fov: float = 90.0
|
||||
@export var sprint_fov: float = 100.0
|
||||
@export var dash_fov: float = 110.0
|
||||
@export var fov_lerp_speed: float = 10.0
|
||||
@export var head_bob_frequency: float = 12.0
|
||||
|
||||
@@ -18,7 +18,6 @@ var input_dir: Vector2 = Vector2.ZERO
|
||||
var wish_dir_world: Vector3 = Vector3.ZERO
|
||||
var input_jump_pressed: bool = false
|
||||
var input_jump_just_pressed: bool = false
|
||||
var input_sprint: bool = false
|
||||
var input_crouch: bool = false
|
||||
var input_dash: bool = false
|
||||
|
||||
@@ -29,6 +28,7 @@ var on_ground: bool = false
|
||||
var last_ground_time: float = 0.0
|
||||
var jump_buffer_time: float = 0.0
|
||||
var coyote_timer: float = 0.0
|
||||
var jump_cooldown_timer: float = 0.0
|
||||
var current_jump_count: int = 0
|
||||
var chain_timer: float = 0.0
|
||||
var chain_count: int = 0
|
||||
@@ -61,6 +61,10 @@ func _physics_process(delta: float) -> void:
|
||||
else:
|
||||
jump_buffer_time = maxf(jump_buffer_time - delta, 0.0)
|
||||
|
||||
# Update jump cooldown
|
||||
if jump_cooldown_timer > 0.0:
|
||||
jump_cooldown_timer = maxf(jump_cooldown_timer - delta, 0.0)
|
||||
|
||||
# Update chain timer
|
||||
if chain_timer > 0.0 and on_ground:
|
||||
chain_timer -= delta
|
||||
|
||||
@@ -8,6 +8,21 @@ signal chain_updated(count: int, bonus: float)
|
||||
var _machine: MovementStateMachine = null
|
||||
var head_pivot: Node3D = null # FPSCameraRig node
|
||||
var camera: Camera3D = null
|
||||
var _damage_layer: CanvasLayer = null
|
||||
|
||||
# Audio
|
||||
var footstep_player: AudioStreamPlayer
|
||||
var dash_player: AudioStreamPlayer
|
||||
var hit_player: AudioStreamPlayer
|
||||
var slide_player: AudioStreamPlayer
|
||||
var wallrun_player: AudioStreamPlayer
|
||||
var wind_player: AudioStreamPlayer
|
||||
var jump_player: AudioStreamPlayer
|
||||
var double_jump_player: AudioStreamPlayer
|
||||
|
||||
# UI
|
||||
var hit_marker: Control
|
||||
var _hit_marker_tween: Tween
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -22,9 +37,118 @@ func _ready() -> void:
|
||||
if head_pivot:
|
||||
camera = head_pivot.get_node_or_null("Camera3D")
|
||||
|
||||
_damage_layer = CanvasLayer.new()
|
||||
add_child(_damage_layer)
|
||||
|
||||
_setup_audio()
|
||||
_setup_hit_marker()
|
||||
|
||||
set_process(true)
|
||||
set_physics_process(true)
|
||||
|
||||
func _setup_audio() -> void:
|
||||
footstep_player = AudioStreamPlayer.new()
|
||||
footstep_player.stream = load("res://assets/sounds/footstep.wav")
|
||||
add_child(footstep_player)
|
||||
|
||||
jump_player = AudioStreamPlayer.new()
|
||||
jump_player.stream = load("res://assets/sounds/jump.wav")
|
||||
add_child(jump_player)
|
||||
|
||||
double_jump_player = AudioStreamPlayer.new()
|
||||
double_jump_player.stream = load("res://assets/sounds/double_jump.wav")
|
||||
add_child(double_jump_player)
|
||||
|
||||
dash_player = AudioStreamPlayer.new()
|
||||
dash_player.stream = load("res://assets/sounds/dash.wav")
|
||||
dash_player.volume_db = -5.0
|
||||
add_child(dash_player)
|
||||
|
||||
hit_player = AudioStreamPlayer.new()
|
||||
hit_player.stream = load("res://assets/sounds/hit_confirm.wav")
|
||||
hit_player.max_polyphony = 8
|
||||
add_child(hit_player)
|
||||
|
||||
slide_player = AudioStreamPlayer.new()
|
||||
slide_player.stream = load("res://assets/sounds/slide.wav")
|
||||
slide_player.volume_db = -10.0
|
||||
add_child(slide_player)
|
||||
|
||||
wallrun_player = AudioStreamPlayer.new()
|
||||
wallrun_player.stream = load("res://assets/sounds/wallrun.wav")
|
||||
wallrun_player.volume_db = -10.0
|
||||
add_child(wallrun_player)
|
||||
|
||||
wind_player = AudioStreamPlayer.new()
|
||||
wind_player.stream = load("res://assets/sounds/wind.wav")
|
||||
wind_player.volume_db = -80.0 # Start silent
|
||||
add_child(wind_player)
|
||||
wind_player.play()
|
||||
|
||||
func _setup_hit_marker() -> void:
|
||||
hit_marker = Control.new()
|
||||
hit_marker.set_anchors_preset(Control.PRESET_CENTER)
|
||||
hit_marker.modulate.a = 0.0 # Hidden by default
|
||||
_damage_layer.add_child(hit_marker)
|
||||
|
||||
# Draw an X perfectly centered
|
||||
var length = 12
|
||||
var thickness = 2
|
||||
for angle in [PI/4, 3*PI/4, 5*PI/4, 7*PI/4]:
|
||||
var rect = ColorRect.new()
|
||||
rect.color = Color.WHITE
|
||||
rect.size = Vector2(length, thickness)
|
||||
rect.pivot_offset = rect.size / 2.0
|
||||
# Center the rect itself at (0,0) before offset
|
||||
var center_pos = -rect.size / 2.0
|
||||
# Move it outward along the angle so it doesn't cover the exact center dot
|
||||
rect.position = center_pos + Vector2(cos(angle), sin(angle)) * (length / 2.0)
|
||||
rect.rotation = angle
|
||||
hit_marker.add_child(rect)
|
||||
|
||||
var _last_hit_sound_time: int = 0
|
||||
|
||||
func spawn_damage_number(amount: float, hit_pos: Vector3) -> void:
|
||||
if not camera or not _damage_layer: return
|
||||
|
||||
var label = load("res://ui/floating_damage_text.gd").new()
|
||||
label.text = str(round(amount))
|
||||
label.add_theme_font_size_override("font_size", 32)
|
||||
label.add_theme_color_override("font_color", Color(1.0, 0.6, 0.1))
|
||||
label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.8))
|
||||
label.add_theme_constant_override("outline_size", 6)
|
||||
|
||||
# Small random offset in 3D space so multiple pellets don't perfectly overlap
|
||||
var offset = Vector3(randf_range(-0.4, 0.4), randf_range(-0.4, 0.4), randf_range(-0.4, 0.4))
|
||||
label.target_pos = hit_pos + offset
|
||||
label.camera = camera
|
||||
|
||||
_damage_layer.add_child(label)
|
||||
|
||||
# Only play the hit sound and flash the marker once per frame (or roughly every 16ms)
|
||||
# This prevents the shotgun from stacking 8 hit sounds in a single frame.
|
||||
var now = Time.get_ticks_msec()
|
||||
if now - _last_hit_sound_time > 10:
|
||||
_last_hit_sound_time = now
|
||||
hit_player.play()
|
||||
|
||||
if _hit_marker_tween and _hit_marker_tween.is_valid():
|
||||
_hit_marker_tween.kill()
|
||||
|
||||
hit_marker.modulate.a = 1.0
|
||||
_hit_marker_tween = create_tween()
|
||||
_hit_marker_tween.tween_property(hit_marker, "modulate:a", 0.0, 0.4)
|
||||
|
||||
func apply_impulse(force: Vector3) -> void:
|
||||
velocity += force
|
||||
# Force an upward launch to break ground contact
|
||||
if force.y > 0.1:
|
||||
# Tiny upward bump helps move_and_slide detach from the floor
|
||||
global_position.y += 0.1
|
||||
|
||||
func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null) -> void:
|
||||
# Show damage number for self-damage
|
||||
spawn_damage_number(amount, _hit_pos)
|
||||
|
||||
func _ensure_machine() -> MovementStateMachine:
|
||||
if is_instance_valid(_machine):
|
||||
@@ -37,8 +161,15 @@ func _ensure_machine() -> MovementStateMachine:
|
||||
_machine = null
|
||||
return null
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
var speed = velocity.length()
|
||||
var wind_factor = clampf((speed - 10.0) / 25.0, 0.0, 1.0)
|
||||
# Decibel scale is non-linear, so lerping from -80 means it stays inaudible for a long time.
|
||||
# -40 db is practically silent for a background loop, -15 db is our max quiet wind.
|
||||
wind_player.volume_db = lerpf(-40.0, -15.0, wind_factor)
|
||||
if not wind_player.playing and wind_factor > 0.0:
|
||||
wind_player.play()
|
||||
|
||||
var sm := _ensure_machine()
|
||||
if not sm:
|
||||
return
|
||||
@@ -46,7 +177,6 @@ func _physics_process(_delta: float) -> void:
|
||||
# Poll input in _physics_process so it's synchronized with the state machine tick
|
||||
sm.input_jump_just_pressed = Input.is_action_just_pressed("jump")
|
||||
sm.input_jump_pressed = Input.is_action_pressed("jump")
|
||||
sm.input_sprint = Input.is_action_pressed("sprint")
|
||||
sm.input_crouch = Input.is_action_pressed("crouch")
|
||||
sm.input_dash = Input.is_action_just_pressed("dash")
|
||||
|
||||
|
||||
@@ -31,19 +31,22 @@ func update(delta: float) -> void:
|
||||
# ── Air control ───────────────────────────────────────────────────────
|
||||
var wish_dir: Vector3 = machine.wish_dir_world
|
||||
var hvel := Vector3(vel.x, 0.0, vel.z)
|
||||
var current_speed := hvel.length()
|
||||
|
||||
if wish_dir.length_squared() > 0.01:
|
||||
var target_speed := maxf(current_speed, params.walk_speed)
|
||||
target_speed = minf(target_speed, params.max_air_speed)
|
||||
wish_dir = wish_dir.normalized()
|
||||
var current_speed = hvel.length()
|
||||
|
||||
var target_vel := wish_dir * target_speed
|
||||
var new_hvel = hvel.lerp(target_vel, 1.0 - exp(-params.air_control * params.air_acceleration * delta))
|
||||
# Add a strong force in the wish direction
|
||||
var air_accel = params.air_strafe_accel * delta
|
||||
var new_hvel = hvel + wish_dir * air_accel
|
||||
|
||||
# Prevent speed loss during sharp turns, but allow braking if holding backward
|
||||
if hvel.dot(target_vel) >= 0.0 and current_speed > params.walk_speed:
|
||||
if new_hvel.length_squared() > 0.01:
|
||||
new_hvel = new_hvel.normalized() * current_speed
|
||||
# Limit the speed so we don't gain speed purely from air control.
|
||||
# We only allow air control to change our direction (strafing)
|
||||
# or to accelerate us up to our walk speed if we started slow.
|
||||
var max_allowed_speed = maxf(current_speed, params.walk_speed)
|
||||
|
||||
if new_hvel.length() > max_allowed_speed:
|
||||
new_hvel = new_hvel.normalized() * max_allowed_speed
|
||||
|
||||
hvel = new_hvel
|
||||
else:
|
||||
@@ -62,9 +65,9 @@ func update(delta: float) -> void:
|
||||
machine.current_jump_count = 0
|
||||
|
||||
# Bunny hop: if jump was buffered or pressed on landing frame
|
||||
if machine.input_jump_pressed or machine.jump_buffer_time > 0.0:
|
||||
if (machine.input_jump_pressed or machine.jump_buffer_time > 0.0) and machine.jump_cooldown_timer <= 0.0:
|
||||
var land_speed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||
var bhop_speed := minf(land_speed + params.bunny_hop_speed_gain, params.bunny_hop_speed_cap)
|
||||
var bhop_speed := maxf(land_speed, minf(land_speed + params.bunny_hop_speed_gain, params.bunny_hop_speed_cap))
|
||||
# Maintain horizontal direction, boost speed
|
||||
var hdir := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||
if hdir.length_squared() > 0.01:
|
||||
@@ -78,6 +81,9 @@ func update(delta: float) -> void:
|
||||
machine.on_ground = false
|
||||
machine.jump_buffer_time = 0.0
|
||||
machine.register_chain_mechanic("bunny_hop")
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
if player.jump_player:
|
||||
player.jump_player.play()
|
||||
# Stay in air state
|
||||
return
|
||||
|
||||
@@ -85,10 +91,13 @@ func update(delta: float) -> void:
|
||||
return
|
||||
|
||||
# ── Double Jump ───────────────────────────────────────────────────────
|
||||
if machine.input_jump_just_pressed and machine.current_jump_count < params.double_jump_max_count:
|
||||
if machine.input_jump_just_pressed and machine.current_jump_count < params.double_jump_max_count and machine.jump_cooldown_timer <= 0.0:
|
||||
player.velocity.y = params.double_jump_velocity
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
machine.current_jump_count += 1
|
||||
machine.register_chain_mechanic("double_jump")
|
||||
if player.double_jump_player:
|
||||
player.double_jump_player.play()
|
||||
return
|
||||
|
||||
# ── Wall run transition ───────────────────────────────────────────────
|
||||
|
||||
@@ -8,6 +8,7 @@ var params: MovementParams:
|
||||
var elapsed: float = 0.0
|
||||
var direction: Vector3 = Vector3.ZERO
|
||||
var _exit_speed: float = 0.0
|
||||
var _last_vel: Vector3 = Vector3.ZERO
|
||||
|
||||
# Cooldown tracked across dash instances
|
||||
static var _last_dash_time: float = -999.0
|
||||
@@ -23,6 +24,10 @@ func enter(_data: Dictionary = {}) -> void:
|
||||
elapsed = 0.0
|
||||
_last_dash_time = now
|
||||
|
||||
var player := machine.player
|
||||
if player.dash_player:
|
||||
player.dash_player.play()
|
||||
|
||||
# Direction: use wish direction if available, else player forward
|
||||
var wish := machine.wish_dir_world
|
||||
if wish.length_squared() > 0.01:
|
||||
@@ -33,9 +38,25 @@ func enter(_data: Dictionary = {}) -> void:
|
||||
direction = direction.normalized()
|
||||
|
||||
machine.register_chain_mechanic("dash")
|
||||
_exit_speed = machine.get_effective_speed(params.dash_speed)
|
||||
|
||||
var current_hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z)
|
||||
var current_speed := current_hvel.length()
|
||||
var proj := current_hvel.dot(direction)
|
||||
|
||||
var base_dash_speed = machine.get_effective_speed(params.dash_speed)
|
||||
|
||||
if proj >= 0.0:
|
||||
# Dashing forward or diagonally forward: add base dash speed to the projected speed
|
||||
_exit_speed = proj + base_dash_speed
|
||||
else:
|
||||
# Dashing backward or diagonally backward:
|
||||
# Reflect current momentum into the new direction, smoothly scaling from base_dash_speed (at sideways) to current_speed (at exactly backward).
|
||||
var backward_factor = -proj / current_speed if current_speed > 0.001 else 0.0
|
||||
_exit_speed = lerpf(base_dash_speed, maxf(current_speed, base_dash_speed), backward_factor)
|
||||
|
||||
machine.player.velocity = direction * _exit_speed
|
||||
machine.on_ground = false
|
||||
_last_vel = machine.player.velocity
|
||||
|
||||
|
||||
func exit() -> void:
|
||||
@@ -46,15 +67,26 @@ func exit() -> void:
|
||||
func update(delta: float) -> void:
|
||||
elapsed += delta
|
||||
if elapsed > params.dash_duration:
|
||||
# Transition to air with preserved velocity (slight decay)
|
||||
machine.player.velocity = direction * _exit_speed * 0.85
|
||||
if machine.player.is_on_floor():
|
||||
machine.on_ground = true
|
||||
machine.switch_to("ground")
|
||||
else:
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# Maintain dash velocity
|
||||
var player := machine.player
|
||||
var vel: Vector3 = player.velocity
|
||||
|
||||
if vel.distance_squared_to(_last_vel) > 100.0:
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# Maintain dash velocity — no gravity, no friction
|
||||
machine.player.velocity = direction * _exit_speed
|
||||
machine.player.move_and_slide()
|
||||
vel = direction * _exit_speed
|
||||
if player.is_on_floor():
|
||||
vel.y = -0.5 # Snap to floor to handle ramps
|
||||
|
||||
if machine.player.is_on_floor():
|
||||
machine.on_ground = true
|
||||
machine.switch_to("ground")
|
||||
player.velocity = vel
|
||||
player.move_and_slide()
|
||||
_last_vel = player.velocity
|
||||
|
||||
@@ -5,6 +5,7 @@ var machine: MovementStateMachine
|
||||
var params: MovementParams:
|
||||
get: return machine.params
|
||||
|
||||
var _footstep_timer: float = 0.0
|
||||
|
||||
func enter(_data: Dictionary = {}) -> void:
|
||||
machine.on_ground = true
|
||||
@@ -26,13 +27,44 @@ func update(delta: float) -> void:
|
||||
var player := machine.player
|
||||
var vel: Vector3 = player.velocity
|
||||
|
||||
# ── Jump (coyote time + jump buffer) ──────────────────────────────────
|
||||
if machine.input_jump_just_pressed and machine.coyote_timer > 0.0 and machine.jump_cooldown_timer <= 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.on_ground = false
|
||||
machine.coyote_timer = 0.0
|
||||
machine.register_chain_mechanic("jump")
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
if player.jump_player:
|
||||
player.jump_player.play()
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# Jump buffer: player pressed jump just before landing
|
||||
if machine.on_ground and machine.jump_buffer_time > 0.0 and machine.jump_cooldown_timer <= 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.on_ground = false
|
||||
machine.jump_buffer_time = 0.0
|
||||
machine.register_chain_mechanic("jump")
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
if player.jump_player:
|
||||
player.jump_player.play()
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# ── Slide (crouch while moving fast enough) ───────────────────────────
|
||||
if machine.input_crouch and machine.slide_cooldown_timer <= 0.0:
|
||||
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||
if hspeed > params.slide_min_speed:
|
||||
machine.switch_to("slide")
|
||||
return
|
||||
|
||||
# ── Determine target speed ────────────────────────────────────────────
|
||||
var wish_dir: Vector3 = machine.wish_dir_world
|
||||
var speed := params.walk_speed
|
||||
if machine.input_crouch:
|
||||
speed = params.crouch_speed
|
||||
elif machine.input_sprint and wish_dir.length_squared() > 0.01:
|
||||
speed = params.sprint_speed
|
||||
var effective_speed := machine.get_effective_speed(speed)
|
||||
|
||||
# ── Horizontal movement (exponential interpolation for smoothness) ───
|
||||
@@ -52,13 +84,25 @@ func update(delta: float) -> void:
|
||||
vel.y -= params.gravity * delta
|
||||
else:
|
||||
# Snap to floor: small downward velocity keeps is_on_floor() stable
|
||||
vel.y = -0.5
|
||||
# ONLY if we aren't being launched upward by an impulse!
|
||||
if vel.y <= 0.0:
|
||||
vel.y = -0.5
|
||||
|
||||
player.velocity = vel
|
||||
player.move_and_slide()
|
||||
|
||||
_update_capsule_height()
|
||||
|
||||
var current_hspeed = Vector2(player.velocity.x, player.velocity.z).length()
|
||||
if current_hspeed > 1.0:
|
||||
_footstep_timer -= delta
|
||||
if _footstep_timer <= 0.0:
|
||||
if player.footstep_player:
|
||||
player.footstep_player.play()
|
||||
_footstep_timer = max(0.2, 3.0 / current_hspeed)
|
||||
else:
|
||||
_footstep_timer = 0.0
|
||||
|
||||
# ── Update ground status ──────────────────────────────────────────────
|
||||
if player.is_on_floor():
|
||||
machine.on_ground = true
|
||||
@@ -66,36 +110,10 @@ func update(delta: float) -> void:
|
||||
else:
|
||||
machine.on_ground = false
|
||||
|
||||
# ── Jump (coyote time + jump buffer) ──────────────────────────────────
|
||||
if machine.input_jump_just_pressed and machine.coyote_timer > 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.on_ground = false
|
||||
machine.coyote_timer = 0.0
|
||||
machine.register_chain_mechanic("jump")
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# Jump buffer: player pressed jump just before landing
|
||||
if machine.on_ground and machine.jump_buffer_time > 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.on_ground = false
|
||||
machine.jump_buffer_time = 0.0
|
||||
machine.register_chain_mechanic("jump")
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# ── Falling off ledge → air state ─────────────────────────────────────
|
||||
if not machine.on_ground and machine.coyote_timer <= 0.0:
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# ── Slide (crouch while moving fast enough) ───────────────────────────
|
||||
if machine.input_crouch and machine.slide_cooldown_timer <= 0.0:
|
||||
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||
if hspeed > params.slide_min_speed:
|
||||
machine.switch_to("slide")
|
||||
# ── Launch or Falling off ledge → air state ───────────────────────────
|
||||
if not machine.on_ground:
|
||||
if player.velocity.y > 0.0 or machine.coyote_timer <= 0.0:
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# ── Wall run check ────────────────────────────────────────────────────
|
||||
|
||||
@@ -14,6 +14,9 @@ func enter(_data: Dictionary = {}) -> void:
|
||||
elapsed = 0.0
|
||||
machine.register_chain_mechanic("slide")
|
||||
|
||||
if machine.player.slide_player and not machine.player.slide_player.playing:
|
||||
machine.player.slide_player.play()
|
||||
|
||||
# Save original capsule height and halve it
|
||||
var shape: CapsuleShape3D = _get_capsule()
|
||||
if shape:
|
||||
@@ -38,6 +41,10 @@ func enter(_data: Dictionary = {}) -> void:
|
||||
|
||||
func exit() -> void:
|
||||
machine.slide_cooldown_timer = params.slide_cooldown
|
||||
|
||||
if machine.player.slide_player:
|
||||
machine.player.slide_player.stop()
|
||||
|
||||
# Restore original capsule height
|
||||
var shape: CapsuleShape3D = _get_capsule()
|
||||
if shape and _saved_capsule_height > 0.0:
|
||||
@@ -47,6 +54,10 @@ func exit() -> void:
|
||||
func update(delta: float) -> void:
|
||||
elapsed += delta
|
||||
var player := machine.player
|
||||
|
||||
if player.slide_player and not player.slide_player.playing:
|
||||
player.slide_player.play()
|
||||
|
||||
var vel: Vector3 = player.velocity
|
||||
|
||||
# ── Apply friction to horizontal velocity ─────────────────────────────
|
||||
@@ -88,15 +99,17 @@ func update(delta: float) -> void:
|
||||
return
|
||||
|
||||
# Jump out of slide
|
||||
if machine.input_jump_just_pressed:
|
||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||
if hvel.length_squared() > 0.01:
|
||||
var dir := hvel.normalized()
|
||||
var jump_speed := minf(hvel.length() + params.slide_jump_speed_boost, params.bunny_hop_speed_cap)
|
||||
var current_speed := hvel.length()
|
||||
var jump_speed := maxf(current_speed, minf(current_speed + params.slide_jump_speed_boost, params.bunny_hop_speed_cap))
|
||||
player.velocity.x = dir.x * jump_speed
|
||||
player.velocity.z = dir.z * jump_speed
|
||||
machine.current_jump_count = 1
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
machine.on_ground = false
|
||||
machine.register_chain_mechanic("slide_jump")
|
||||
machine.switch_to("air")
|
||||
|
||||
@@ -48,12 +48,14 @@ func update(delta: float) -> void:
|
||||
return
|
||||
|
||||
# ── Jump off wall ─────────────────────────────────────────────────────
|
||||
if machine.input_jump_just_pressed:
|
||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||
var push_dir := machine.wall_normal * params.wall_run_jump_off_normal
|
||||
player.velocity = Vector3(push_dir.x, params.jump_velocity, push_dir.z)
|
||||
machine.wall_normal = Vector3.ZERO
|
||||
machine.wall_side = 0.0
|
||||
machine.wall_cooldown_timer = 0.3
|
||||
machine.register_chain_mechanic("wall_cling_jump")
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ var params: MovementParams:
|
||||
get: return machine.params
|
||||
|
||||
var elapsed: float = 0.0
|
||||
var _initial_y_vel: float = 0.0
|
||||
var _run_speed: float = 0.0
|
||||
var _last_vel: Vector3 = Vector3.ZERO
|
||||
|
||||
|
||||
var _current_tangent: Vector3 = Vector3.ZERO
|
||||
@@ -18,12 +19,21 @@ func enter(_data: Dictionary = {}) -> void:
|
||||
# but preserve some if they just jumped onto it.
|
||||
machine.player.velocity.y = minf(machine.player.velocity.y, 1.5)
|
||||
machine.on_ground = false
|
||||
_last_vel = machine.player.velocity
|
||||
|
||||
if machine.player.wallrun_player and not machine.player.wallrun_player.playing:
|
||||
machine.player.wallrun_player.play()
|
||||
|
||||
var hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z)
|
||||
_current_tangent = machine.wall_normal.cross(Vector3.UP).normalized()
|
||||
if hvel.dot(_current_tangent) < 0.0:
|
||||
_current_tangent = -_current_tangent
|
||||
|
||||
# Preserve speed along the wall if it's faster than base wall_run_speed
|
||||
var base_speed = machine.get_effective_speed(params.wall_run_speed)
|
||||
var projected_speed = hvel.dot(_current_tangent)
|
||||
_run_speed = maxf(base_speed, projected_speed)
|
||||
|
||||
# Camera tilt
|
||||
var rig = _get_camera_rig()
|
||||
if rig:
|
||||
@@ -36,6 +46,9 @@ func exit() -> void:
|
||||
if rig:
|
||||
rig.clear_wall_tilt()
|
||||
|
||||
if machine.player.wallrun_player:
|
||||
machine.player.wallrun_player.stop()
|
||||
|
||||
|
||||
func update(delta: float) -> void:
|
||||
elapsed += delta
|
||||
@@ -49,6 +62,15 @@ func update(delta: float) -> void:
|
||||
var player := machine.player
|
||||
var vel: Vector3 = player.velocity
|
||||
|
||||
if player.wallrun_player and not player.wallrun_player.playing:
|
||||
player.wallrun_player.play()
|
||||
|
||||
if vel.distance_squared_to(_last_vel) > 100.0:
|
||||
machine.wall_normal = Vector3.ZERO
|
||||
machine.wall_side = 0.0
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# ── Gradual gravity pull (starts light, increases over time) ──────────
|
||||
var gravity_factor := 0.1 + 0.9 * (elapsed / params.wall_run_duration)
|
||||
vel.y -= params.wall_run_gravity * gravity_factor * delta
|
||||
@@ -60,9 +82,8 @@ func update(delta: float) -> void:
|
||||
wall_tangent = -wall_tangent
|
||||
_current_tangent = wall_tangent
|
||||
|
||||
var effective_speed := machine.get_effective_speed(params.wall_run_speed)
|
||||
vel.x = wall_tangent.x * effective_speed
|
||||
vel.z = wall_tangent.z * effective_speed
|
||||
vel.x = wall_tangent.x * _run_speed
|
||||
vel.z = wall_tangent.z * _run_speed
|
||||
|
||||
# ── Look away to break wall run ───────────────────────────────────────
|
||||
var look_dir := -player.global_transform.basis.z
|
||||
@@ -77,16 +98,23 @@ func update(delta: float) -> void:
|
||||
vel -= machine.wall_normal * 2.0
|
||||
|
||||
# ── Wall jump ─────────────────────────────────────────────────────────
|
||||
if machine.input_jump_just_pressed:
|
||||
var jump_vel: Vector3 = machine.wall_normal * params.wall_run_jump_off_normal
|
||||
var h_look := Vector3(look_dir.x, 0.0, look_dir.z).normalized()
|
||||
jump_vel += h_look * params.wall_run_jump_horizontal
|
||||
if machine.input_jump_just_pressed and machine.jump_cooldown_timer <= 0.0:
|
||||
# Base jump off velocity on current preserved momentum
|
||||
var jump_vel: Vector3 = _current_tangent * _run_speed
|
||||
|
||||
# Spring off the wall
|
||||
jump_vel += machine.wall_normal * params.wall_run_jump_off_normal
|
||||
var h_look := Vector3(look_dir.x, 0.0, look_dir.z)
|
||||
if h_look.length_squared() > 0.01:
|
||||
jump_vel += h_look.normalized() * params.wall_run_jump_horizontal
|
||||
|
||||
jump_vel.y = params.wall_run_auto_jump_speed
|
||||
player.velocity = jump_vel
|
||||
machine.wall_normal = Vector3.ZERO
|
||||
machine.wall_side = 0.0
|
||||
machine.wall_cooldown_timer = 0.3
|
||||
machine.register_chain_mechanic("wall_jump")
|
||||
machine.jump_cooldown_timer = params.jump_cooldown
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
@@ -97,6 +125,7 @@ func update(delta: float) -> void:
|
||||
|
||||
player.velocity = vel
|
||||
player.move_and_slide()
|
||||
_last_vel = player.velocity
|
||||
|
||||
# ── Check still on wall ───────────────────────────────────────────────
|
||||
var still_on_wall := machine.detect_wall_horizontal()
|
||||
|
||||
@@ -15,6 +15,12 @@ run/main_scene="res://scenes/test_level/test_level.tscn"
|
||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
SettingsManager="*res://globals/settings_manager.gd"
|
||||
LoadoutManager="*res://globals/loadout_manager.gd"
|
||||
PauseMenu="*res://ui/pause_menu.gd"
|
||||
|
||||
[input]
|
||||
|
||||
move_forward={
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
extends Label
|
||||
class_name FloatingDamageText
|
||||
|
||||
var target_pos: Vector3
|
||||
var camera: Camera3D
|
||||
var float_offset: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
# Setup Tween for floating up and fading out
|
||||
var tween = create_tween()
|
||||
tween.set_parallel(true)
|
||||
# Float up 100 pixels
|
||||
tween.tween_property(self, "float_offset", 100.0, 1.0)
|
||||
# Fade out
|
||||
tween.tween_property(self, "modulate:a", 0.0, 1.0)
|
||||
|
||||
tween.chain().tween_callback(queue_free)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not is_instance_valid(camera):
|
||||
queue_free()
|
||||
return
|
||||
|
||||
if camera.is_position_behind(target_pos):
|
||||
visible = false
|
||||
else:
|
||||
visible = true
|
||||
var screen_pos = camera.unproject_position(target_pos)
|
||||
# Center the label on the point, then subtract the float offset (moving up on screen)
|
||||
position = screen_pos - size / 2.0 + Vector2(0, -float_offset)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b7i68343deyx
|
||||
@@ -0,0 +1,407 @@
|
||||
extends CanvasLayer
|
||||
|
||||
var bg: ColorRect
|
||||
var main_vbox: VBoxContainer
|
||||
var resume_btn: Button
|
||||
var respawn_btn: Button
|
||||
var loadouts_btn: Button
|
||||
var settings_btn: Button
|
||||
var exit_btn: Button
|
||||
|
||||
# Loadout UI
|
||||
var loadout_editor: Control
|
||||
var loadout_list_vbox: VBoxContainer
|
||||
var edit_panel: PanelContainer
|
||||
var name_edit: LineEdit
|
||||
var primary1_opt: OptionButton
|
||||
var primary2_opt: OptionButton
|
||||
var special_opt: OptionButton
|
||||
var save_loadout_btn: Button
|
||||
var back_to_main_btn: Button
|
||||
var editing_index: int = -1
|
||||
|
||||
# Settings UI
|
||||
var settings_editor: Control
|
||||
var settings_back_btn: Button
|
||||
var keybind_vbox: VBoxContainer
|
||||
var rebind_overlay: ColorRect
|
||||
var rebind_label: Label
|
||||
var waiting_for_input_action: String = ""
|
||||
|
||||
var quit_dialog: ConfirmationDialog
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
layer = 100 # Ensure it's on top
|
||||
visible = false
|
||||
|
||||
_build_ui()
|
||||
_connect_signals()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if waiting_for_input_action != "":
|
||||
if event is InputEventKey or event is InputEventMouseButton:
|
||||
if event.is_pressed():
|
||||
SettingsManager.rebind_action(waiting_for_input_action, event)
|
||||
waiting_for_input_action = ""
|
||||
rebind_overlay.visible = false
|
||||
_populate_keybindings()
|
||||
# Consume event so it doesn't trigger anything else
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
if visible:
|
||||
_resume()
|
||||
else:
|
||||
_pause()
|
||||
|
||||
func _pause() -> void:
|
||||
visible = true
|
||||
get_tree().paused = true
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
_show_main_menu()
|
||||
|
||||
func _resume() -> void:
|
||||
visible = false
|
||||
get_tree().paused = false
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||
|
||||
func _respawn() -> void:
|
||||
_resume() # Unpause
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
func _build_ui() -> void:
|
||||
bg = ColorRect.new()
|
||||
bg.color = Color(0, 0, 0, 0.7)
|
||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(bg)
|
||||
|
||||
# ── Main Menu ──
|
||||
var main_center = CenterContainer.new()
|
||||
main_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
bg.add_child(main_center)
|
||||
|
||||
main_vbox = VBoxContainer.new()
|
||||
main_vbox.add_theme_constant_override("separation", 20)
|
||||
main_center.add_child(main_vbox)
|
||||
|
||||
var title = Label.new()
|
||||
title.text = "PAUSED"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 48)
|
||||
main_vbox.add_child(title)
|
||||
|
||||
resume_btn = Button.new()
|
||||
resume_btn.text = "Resume"
|
||||
resume_btn.custom_minimum_size = Vector2(200, 50)
|
||||
main_vbox.add_child(resume_btn)
|
||||
|
||||
respawn_btn = Button.new()
|
||||
respawn_btn.text = "Respawn"
|
||||
respawn_btn.custom_minimum_size = Vector2(200, 50)
|
||||
main_vbox.add_child(respawn_btn)
|
||||
|
||||
loadouts_btn = Button.new()
|
||||
loadouts_btn.text = "Loadouts"
|
||||
loadouts_btn.custom_minimum_size = Vector2(200, 50)
|
||||
main_vbox.add_child(loadouts_btn)
|
||||
|
||||
settings_btn = Button.new()
|
||||
settings_btn.text = "Settings"
|
||||
settings_btn.custom_minimum_size = Vector2(200, 50)
|
||||
main_vbox.add_child(settings_btn)
|
||||
|
||||
exit_btn = Button.new()
|
||||
exit_btn.text = "Exit Game"
|
||||
exit_btn.custom_minimum_size = Vector2(200, 50)
|
||||
main_vbox.add_child(exit_btn)
|
||||
|
||||
# ── Loadout Editor Root ──
|
||||
loadout_editor = Control.new()
|
||||
loadout_editor.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
bg.add_child(loadout_editor)
|
||||
|
||||
var editor_center = CenterContainer.new()
|
||||
editor_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
loadout_editor.add_child(editor_center)
|
||||
|
||||
var editor_hbox = HBoxContainer.new()
|
||||
editor_hbox.add_theme_constant_override("separation", 40)
|
||||
editor_center.add_child(editor_hbox)
|
||||
|
||||
var left_vbox = VBoxContainer.new()
|
||||
editor_hbox.add_child(left_vbox)
|
||||
|
||||
var list_title = Label.new()
|
||||
list_title.text = "Your Loadouts"
|
||||
list_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
left_vbox.add_child(list_title)
|
||||
|
||||
loadout_list_vbox = VBoxContainer.new()
|
||||
left_vbox.add_child(loadout_list_vbox)
|
||||
|
||||
back_to_main_btn = Button.new()
|
||||
back_to_main_btn.text = "Back to Menu"
|
||||
back_to_main_btn.custom_minimum_size = Vector2(0, 40)
|
||||
left_vbox.add_child(back_to_main_btn)
|
||||
|
||||
edit_panel = PanelContainer.new()
|
||||
editor_hbox.add_child(edit_panel)
|
||||
|
||||
var edit_vbox = VBoxContainer.new()
|
||||
edit_vbox.add_theme_constant_override("separation", 10)
|
||||
edit_panel.add_child(edit_vbox)
|
||||
|
||||
var edit_title = Label.new()
|
||||
edit_title.text = "Edit Loadout"
|
||||
edit_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
edit_vbox.add_child(edit_title)
|
||||
|
||||
name_edit = LineEdit.new()
|
||||
name_edit.placeholder_text = "Loadout Name"
|
||||
name_edit.custom_minimum_size = Vector2(250, 0)
|
||||
edit_vbox.add_child(name_edit)
|
||||
|
||||
edit_vbox.add_child(_create_label("Primary 1:"))
|
||||
primary1_opt = OptionButton.new()
|
||||
edit_vbox.add_child(primary1_opt)
|
||||
|
||||
edit_vbox.add_child(_create_label("Primary 2:"))
|
||||
primary2_opt = OptionButton.new()
|
||||
edit_vbox.add_child(primary2_opt)
|
||||
|
||||
edit_vbox.add_child(_create_label("Special:"))
|
||||
special_opt = OptionButton.new()
|
||||
edit_vbox.add_child(special_opt)
|
||||
|
||||
save_loadout_btn = Button.new()
|
||||
save_loadout_btn.text = "Save Loadout"
|
||||
save_loadout_btn.custom_minimum_size = Vector2(0, 40)
|
||||
edit_vbox.add_child(save_loadout_btn)
|
||||
|
||||
# ── Settings Editor Root ──
|
||||
settings_editor = Control.new()
|
||||
settings_editor.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
bg.add_child(settings_editor)
|
||||
|
||||
var settings_center = CenterContainer.new()
|
||||
settings_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
settings_editor.add_child(settings_center)
|
||||
|
||||
var settings_vbox = VBoxContainer.new()
|
||||
settings_vbox.custom_minimum_size = Vector2(600, 400)
|
||||
settings_center.add_child(settings_vbox)
|
||||
|
||||
settings_back_btn = Button.new()
|
||||
settings_back_btn.text = "Back to Menu"
|
||||
settings_vbox.add_child(settings_back_btn)
|
||||
|
||||
var tab_container = TabContainer.new()
|
||||
tab_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
settings_vbox.add_child(tab_container)
|
||||
|
||||
# General Tab
|
||||
var general_tab = VBoxContainer.new()
|
||||
general_tab.name = "General"
|
||||
tab_container.add_child(general_tab)
|
||||
var gen_label = Label.new()
|
||||
gen_label.text = "Settings coming soon..."
|
||||
general_tab.add_child(gen_label)
|
||||
|
||||
# Keybindings Tab
|
||||
var key_tab = VBoxContainer.new()
|
||||
key_tab.name = "Keybindings"
|
||||
tab_container.add_child(key_tab)
|
||||
|
||||
var reset_btn = Button.new()
|
||||
reset_btn.text = "Reset to Defaults"
|
||||
reset_btn.pressed.connect(func():
|
||||
SettingsManager.reset_to_defaults()
|
||||
_populate_keybindings()
|
||||
)
|
||||
key_tab.add_child(reset_btn)
|
||||
|
||||
var scroll = ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
key_tab.add_child(scroll)
|
||||
|
||||
keybind_vbox = VBoxContainer.new()
|
||||
keybind_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.add_child(keybind_vbox)
|
||||
|
||||
# Rebind Overlay
|
||||
rebind_overlay = ColorRect.new()
|
||||
rebind_overlay.color = Color(0, 0, 0, 0.85)
|
||||
rebind_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
rebind_overlay.visible = false
|
||||
add_child(rebind_overlay)
|
||||
|
||||
var rebind_center = CenterContainer.new()
|
||||
rebind_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
rebind_overlay.add_child(rebind_center)
|
||||
|
||||
rebind_label = Label.new()
|
||||
rebind_label.text = "Press any key..."
|
||||
rebind_label.add_theme_font_size_override("font_size", 32)
|
||||
rebind_center.add_child(rebind_label)
|
||||
|
||||
|
||||
# ── Quit Dialog ──
|
||||
quit_dialog = ConfirmationDialog.new()
|
||||
quit_dialog.title = "Quit Game"
|
||||
quit_dialog.dialog_text = "Are you sure you want to close the game?"
|
||||
add_child(quit_dialog)
|
||||
|
||||
func _create_label(text: String) -> Label:
|
||||
var l = Label.new()
|
||||
l.text = text
|
||||
return l
|
||||
|
||||
func _connect_signals() -> void:
|
||||
resume_btn.pressed.connect(_resume)
|
||||
respawn_btn.pressed.connect(_respawn)
|
||||
loadouts_btn.pressed.connect(_show_loadouts)
|
||||
settings_btn.pressed.connect(_show_settings)
|
||||
exit_btn.pressed.connect(_show_exit_dialog)
|
||||
back_to_main_btn.pressed.connect(_show_main_menu)
|
||||
settings_back_btn.pressed.connect(_show_main_menu)
|
||||
save_loadout_btn.pressed.connect(_save_current_loadout)
|
||||
quit_dialog.confirmed.connect(_quit_game)
|
||||
|
||||
func _show_main_menu() -> void:
|
||||
main_vbox.visible = true
|
||||
loadout_editor.visible = false
|
||||
settings_editor.visible = false
|
||||
|
||||
func _show_loadouts() -> void:
|
||||
main_vbox.visible = false
|
||||
settings_editor.visible = false
|
||||
loadout_editor.visible = true
|
||||
edit_panel.visible = false
|
||||
_populate_loadout_list()
|
||||
|
||||
func _show_settings() -> void:
|
||||
main_vbox.visible = false
|
||||
loadout_editor.visible = false
|
||||
settings_editor.visible = true
|
||||
_populate_keybindings()
|
||||
|
||||
func _populate_keybindings() -> void:
|
||||
for c in keybind_vbox.get_children():
|
||||
c.queue_free()
|
||||
|
||||
var actions = SettingsManager.default_bindings.keys()
|
||||
for action in actions:
|
||||
var hbox = HBoxContainer.new()
|
||||
var lbl = Label.new()
|
||||
lbl.text = action.capitalize()
|
||||
lbl.custom_minimum_size = Vector2(200, 0)
|
||||
hbox.add_child(lbl)
|
||||
|
||||
var btn = Button.new()
|
||||
btn.text = _get_key_name(action)
|
||||
btn.custom_minimum_size = Vector2(150, 0)
|
||||
btn.pressed.connect(func(): _start_rebind(action))
|
||||
hbox.add_child(btn)
|
||||
|
||||
keybind_vbox.add_child(hbox)
|
||||
|
||||
func _start_rebind(action: String) -> void:
|
||||
waiting_for_input_action = action
|
||||
rebind_label.text = "Press any key for: " + action.capitalize()
|
||||
rebind_overlay.visible = true
|
||||
|
||||
func _get_key_name(action: String) -> String:
|
||||
if not InputMap.has_action(action):
|
||||
return "?"
|
||||
var events = InputMap.action_get_events(action)
|
||||
for e in events:
|
||||
if e is InputEventKey:
|
||||
var code = e.physical_keycode if e.physical_keycode != 0 else e.keycode
|
||||
return OS.get_keycode_string(code)
|
||||
elif e is InputEventMouseButton:
|
||||
if e.button_index == MOUSE_BUTTON_LEFT: return "LClick"
|
||||
elif e.button_index == MOUSE_BUTTON_RIGHT: return "RClick"
|
||||
elif e.button_index == MOUSE_BUTTON_MIDDLE: return "MClick"
|
||||
elif e.button_index == MOUSE_BUTTON_WHEEL_UP: return "MWheel Up"
|
||||
elif e.button_index == MOUSE_BUTTON_WHEEL_DOWN: return "MWheel Down"
|
||||
return "?"
|
||||
|
||||
func _populate_loadout_list() -> void:
|
||||
for c in loadout_list_vbox.get_children():
|
||||
c.queue_free()
|
||||
|
||||
for i in range(LoadoutManager.loadouts.size()):
|
||||
var l = LoadoutManager.loadouts[i]
|
||||
var hbox = HBoxContainer.new()
|
||||
|
||||
var btn = Button.new()
|
||||
var is_active = (i == LoadoutManager.active_loadout_index)
|
||||
btn.text = l["name"] + (" (Active)" if is_active else "")
|
||||
btn.custom_minimum_size = Vector2(200, 40)
|
||||
btn.pressed.connect(func(): _edit_loadout(i))
|
||||
hbox.add_child(btn)
|
||||
|
||||
var equip_btn = Button.new()
|
||||
equip_btn.text = "Equip"
|
||||
equip_btn.disabled = is_active
|
||||
equip_btn.pressed.connect(func(): _equip_loadout(i))
|
||||
hbox.add_child(equip_btn)
|
||||
|
||||
loadout_list_vbox.add_child(hbox)
|
||||
|
||||
func _equip_loadout(idx: int) -> void:
|
||||
LoadoutManager.active_loadout_index = idx
|
||||
LoadoutManager.save_loadouts()
|
||||
_populate_loadout_list()
|
||||
|
||||
func _edit_loadout(idx: int) -> void:
|
||||
editing_index = idx
|
||||
var l = LoadoutManager.loadouts[idx]
|
||||
name_edit.text = l["name"]
|
||||
|
||||
_populate_options(primary1_opt, "primary", l["primary_1"])
|
||||
_populate_options(primary2_opt, "primary", l["primary_2"])
|
||||
_populate_options(special_opt, "special", l["special"])
|
||||
|
||||
edit_panel.visible = true
|
||||
|
||||
func _populate_options(opt: OptionButton, category: String, current_id: String) -> void:
|
||||
opt.clear()
|
||||
var idx = 0
|
||||
var select_idx = 0
|
||||
|
||||
opt.add_item("None", 0)
|
||||
opt.set_item_metadata(0, "none")
|
||||
if current_id == "none": select_idx = 0
|
||||
idx += 1
|
||||
|
||||
for key in LoadoutManager.weapon_db:
|
||||
if key == "none": continue
|
||||
var w = LoadoutManager.weapon_db[key]
|
||||
if w["category"] == category:
|
||||
opt.add_item(w["name"], idx)
|
||||
opt.set_item_metadata(idx, key)
|
||||
if key == current_id:
|
||||
select_idx = idx
|
||||
idx += 1
|
||||
|
||||
opt.select(select_idx)
|
||||
|
||||
func _save_current_loadout() -> void:
|
||||
if editing_index >= 0 and editing_index < LoadoutManager.loadouts.size():
|
||||
var l = LoadoutManager.loadouts[editing_index]
|
||||
l["name"] = name_edit.text
|
||||
l["primary_1"] = primary1_opt.get_item_metadata(primary1_opt.get_selected_id())
|
||||
l["primary_2"] = primary2_opt.get_item_metadata(primary2_opt.get_selected_id())
|
||||
l["special"] = special_opt.get_item_metadata(special_opt.get_selected_id())
|
||||
LoadoutManager.save_loadouts()
|
||||
_populate_loadout_list()
|
||||
edit_panel.visible = false
|
||||
|
||||
func _show_exit_dialog() -> void:
|
||||
quit_dialog.popup_centered()
|
||||
|
||||
func _quit_game() -> void:
|
||||
get_tree().quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcvjb1h4fxcct
|
||||
@@ -0,0 +1,80 @@
|
||||
extends BaseHitscanWeapon
|
||||
class_name AK47
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "AK-47"
|
||||
fire_rate = 0.15 # ~400 RPM
|
||||
max_ammo = 30
|
||||
reload_time = 2.0
|
||||
base_damage = 35.0
|
||||
min_damage = 25.0
|
||||
falloff_start = 25.0
|
||||
max_range = 150.0
|
||||
automatic = true
|
||||
spread_angle = 0.015
|
||||
|
||||
func _build_model() -> void:
|
||||
# Position
|
||||
position = Vector3(0.3, -0.3, -0.6)
|
||||
|
||||
# Receiver
|
||||
var receiver = MeshInstance3D.new()
|
||||
var rec_mesh = BoxMesh.new()
|
||||
rec_mesh.size = Vector3(0.06, 0.12, 0.3)
|
||||
var rec_mat = StandardMaterial3D.new()
|
||||
rec_mat.albedo_color = Color(0.2, 0.2, 0.2)
|
||||
rec_mesh.material = rec_mat
|
||||
receiver.mesh = rec_mesh
|
||||
receiver.position = Vector3(0, 0, 0.1)
|
||||
model_root.add_child(receiver)
|
||||
|
||||
# Barrel
|
||||
var barrel = MeshInstance3D.new()
|
||||
var b_mesh = CylinderMesh.new()
|
||||
b_mesh.top_radius = 0.015
|
||||
b_mesh.bottom_radius = 0.015
|
||||
b_mesh.height = 0.45
|
||||
b_mesh.material = rec_mat
|
||||
barrel.mesh = b_mesh
|
||||
barrel.rotation.x = deg_to_rad(90)
|
||||
barrel.position = Vector3(0, 0.03, -0.2)
|
||||
model_root.add_child(barrel)
|
||||
|
||||
# Wood Handguard
|
||||
var guard = MeshInstance3D.new()
|
||||
var g_mesh = BoxMesh.new()
|
||||
g_mesh.size = Vector3(0.08, 0.08, 0.2)
|
||||
var wood_mat = StandardMaterial3D.new()
|
||||
wood_mat.albedo_color = Color(0.5, 0.25, 0.1)
|
||||
g_mesh.material = wood_mat
|
||||
guard.mesh = g_mesh
|
||||
guard.position = Vector3(0, 0, -0.1)
|
||||
model_root.add_child(guard)
|
||||
|
||||
# Wood Stock
|
||||
var stock = MeshInstance3D.new()
|
||||
var s_mesh = BoxMesh.new()
|
||||
s_mesh.size = Vector3(0.06, 0.1, 0.25)
|
||||
s_mesh.material = wood_mat
|
||||
stock.mesh = s_mesh
|
||||
stock.position = Vector3(0, -0.02, 0.35)
|
||||
model_root.add_child(stock)
|
||||
|
||||
# Muzzle Flash
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(1.0, 0.7, 0.2)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 5.0
|
||||
muzzle_flash.position = Vector3(0, 0.03, -0.45)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/ak47_fire.wav")
|
||||
fire_sound.position = muzzle_flash.position
|
||||
fire_sound.volume_db = -5.0
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # generic reload
|
||||
model_root.add_child(reload_sound)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bddm0nooqfj2
|
||||
@@ -0,0 +1,97 @@
|
||||
extends BaseHitscanWeapon
|
||||
class_name AWP
|
||||
|
||||
var default_fov: float = 75.0
|
||||
var target_fov: float = 75.0
|
||||
var ads_fov: float = 20.0 # Extremely high zoom
|
||||
var is_ads: bool = false
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "AWP"
|
||||
fire_rate = 1.5 # Slow
|
||||
max_ammo = 3
|
||||
reload_time = 3.0
|
||||
base_damage = 150.0 # 1 hit kill
|
||||
min_damage = 130.0 # Long falloff still kills
|
||||
falloff_start = 100.0
|
||||
max_range = 300.0
|
||||
automatic = false
|
||||
spread_angle = 0.0
|
||||
penetration_count = 1
|
||||
penetration_damage_penalty = 0.5
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.3, -0.3, -0.8)
|
||||
|
||||
var mat_green = StandardMaterial3D.new()
|
||||
mat_green.albedo_color = Color(0.1, 0.4, 0.1)
|
||||
var mat_black = StandardMaterial3D.new()
|
||||
mat_black.albedo_color = Color(0.1, 0.1, 0.1)
|
||||
|
||||
# Body
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = BoxMesh.new()
|
||||
b_mesh.size = Vector3(0.06, 0.12, 0.6)
|
||||
b_mesh.material = mat_green
|
||||
body.mesh = b_mesh
|
||||
model_root.add_child(body)
|
||||
|
||||
# Scope
|
||||
var scope = MeshInstance3D.new()
|
||||
var s_mesh = CylinderMesh.new()
|
||||
s_mesh.top_radius = 0.03
|
||||
s_mesh.bottom_radius = 0.03
|
||||
s_mesh.height = 0.25
|
||||
s_mesh.material = mat_black
|
||||
scope.mesh = s_mesh
|
||||
scope.rotation.x = deg_to_rad(90)
|
||||
scope.position = Vector3(0, 0.09, -0.1)
|
||||
model_root.add_child(scope)
|
||||
|
||||
# Long Barrel
|
||||
var barrel = MeshInstance3D.new()
|
||||
var bar_mesh = CylinderMesh.new()
|
||||
bar_mesh.top_radius = 0.015
|
||||
bar_mesh.bottom_radius = 0.02
|
||||
bar_mesh.height = 0.6
|
||||
bar_mesh.material = mat_black
|
||||
barrel.mesh = bar_mesh
|
||||
barrel.rotation.x = deg_to_rad(90)
|
||||
barrel.position = Vector3(0, 0.04, -0.6)
|
||||
model_root.add_child(barrel)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/awp_fire.wav")
|
||||
fire_sound.volume_db = 0.0
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing for now
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
super._process(delta)
|
||||
|
||||
if camera:
|
||||
if Input.is_action_just_pressed("fire_alt"):
|
||||
is_ads = not is_ads
|
||||
target_fov = ads_fov if is_ads else default_fov
|
||||
|
||||
camera.fov = lerpf(camera.fov, target_fov, 15.0 * delta)
|
||||
|
||||
# Move model slightly based on ADS
|
||||
var target_pos = Vector3(0.0, -0.15, -0.6) if is_ads else Vector3(0.3, -0.3, -0.8)
|
||||
position = position.lerp(target_pos, 15.0 * delta)
|
||||
|
||||
func _start_reload() -> void:
|
||||
super._start_reload()
|
||||
is_ads = false # Drop out of ADS on reload
|
||||
target_fov = default_fov
|
||||
|
||||
func unequip() -> void:
|
||||
is_ads = false
|
||||
if camera:
|
||||
camera.fov = default_fov
|
||||
target_fov = default_fov
|
||||
position = Vector3(0.3, -0.3, -0.8)
|
||||
@@ -0,0 +1 @@
|
||||
uid://xxh6d1k6k2nf
|
||||
@@ -0,0 +1,194 @@
|
||||
extends Node3D
|
||||
class_name BaseHitscanWeapon
|
||||
|
||||
@export var weapon_name: String = "Base Hitscan"
|
||||
@export var fire_rate: float = 0.1 # Seconds between shots
|
||||
@export var max_ammo: int = 30
|
||||
@export var reload_time: float = 1.5
|
||||
@export var base_damage: float = 10.0
|
||||
@export var min_damage: float = 5.0
|
||||
@export var falloff_start: float = 10.0
|
||||
@export var max_range: float = 100.0
|
||||
@export var automatic: bool = true
|
||||
@export var spread_angle: float = 0.01
|
||||
|
||||
@export var penetration_count: int = 0
|
||||
@export var penetration_damage_penalty: float = 0.5
|
||||
|
||||
var current_ammo: int = 30
|
||||
var reloading: bool = false
|
||||
var reload_timer: float = 0.0
|
||||
var fire_cooldown: float = 0.0
|
||||
var is_firing: bool = false
|
||||
|
||||
var player: CharacterBody3D
|
||||
var camera: Camera3D
|
||||
var muzzle_flash: OmniLight3D
|
||||
var fire_sound: AudioStreamPlayer3D
|
||||
var reload_sound: AudioStreamPlayer3D
|
||||
var model_root: Node3D
|
||||
|
||||
func _ready() -> void:
|
||||
current_ammo = max_ammo
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
|
||||
model_root = Node3D.new()
|
||||
add_child(model_root)
|
||||
_build_model()
|
||||
|
||||
func _build_model() -> void:
|
||||
# Virtual method to be overridden
|
||||
pass
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if fire_cooldown > 0.0:
|
||||
fire_cooldown -= delta
|
||||
|
||||
if reloading:
|
||||
reload_timer -= delta
|
||||
var spin_speed = (PI * 2.0) / reload_time
|
||||
model_root.rotation.x += spin_speed * delta
|
||||
|
||||
if reload_timer <= 0.0:
|
||||
current_ammo = max_ammo
|
||||
reloading = false
|
||||
model_root.rotation.x = 0.0
|
||||
print(weapon_name, " Reloaded!")
|
||||
|
||||
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
|
||||
_fire()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
if event.is_action_pressed("fire"):
|
||||
is_firing = true
|
||||
if not automatic and fire_cooldown <= 0.0 and current_ammo > 0 and not reloading:
|
||||
_fire()
|
||||
elif current_ammo == 0 and not reloading:
|
||||
_start_reload()
|
||||
elif event.is_action_released("fire"):
|
||||
is_firing = false
|
||||
|
||||
if event.is_action_pressed("reload"):
|
||||
if current_ammo < max_ammo and not reloading:
|
||||
_start_reload()
|
||||
|
||||
func _start_reload() -> void:
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
is_firing = false
|
||||
if reload_sound:
|
||||
reload_sound.play()
|
||||
|
||||
func _fire() -> void:
|
||||
if current_ammo <= 0 or reloading:
|
||||
return
|
||||
|
||||
current_ammo -= 1
|
||||
fire_cooldown = fire_rate
|
||||
|
||||
_shoot_hitscan()
|
||||
_play_muzzle_flash()
|
||||
|
||||
if fire_sound:
|
||||
# Slight pitch variation
|
||||
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
|
||||
fire_sound.play()
|
||||
|
||||
if current_ammo == 0:
|
||||
_start_reload()
|
||||
|
||||
func _play_muzzle_flash() -> void:
|
||||
if muzzle_flash:
|
||||
muzzle_flash.light_energy = 8.0
|
||||
var tween = create_tween()
|
||||
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.05)
|
||||
|
||||
func _shoot_hitscan() -> void:
|
||||
if not camera: return
|
||||
|
||||
var space_state = camera.get_world_3d().direct_space_state
|
||||
var origin = camera.global_position
|
||||
var forward = -camera.global_transform.basis.z.normalized()
|
||||
var right = camera.global_transform.basis.x.normalized()
|
||||
var up = camera.global_transform.basis.y.normalized()
|
||||
|
||||
# Calculate spread
|
||||
var r = sqrt(randf()) * spread_angle
|
||||
var theta = randf() * PI * 2.0
|
||||
var offset_dir = (right * cos(theta) + up * sin(theta)) * r
|
||||
var ray_dir = (forward + offset_dir).normalized()
|
||||
|
||||
var target = origin + ray_dir * max_range
|
||||
var query = PhysicsRayQueryParameters3D.create(origin, target)
|
||||
|
||||
if player:
|
||||
query.exclude = [player.get_rid()]
|
||||
|
||||
var hits_remaining = penetration_count + 1
|
||||
var current_damage_mult = 1.0
|
||||
var final_target = target
|
||||
|
||||
while hits_remaining > 0:
|
||||
var result = space_state.intersect_ray(query)
|
||||
if not result:
|
||||
break # Nothing else in the way
|
||||
|
||||
var hit_dist = origin.distance_to(result.position)
|
||||
var damage = base_damage * current_damage_mult
|
||||
|
||||
if hit_dist > falloff_start:
|
||||
var falloff_factor = clampf((hit_dist - falloff_start) / (max_range - falloff_start), 0.0, 1.0)
|
||||
damage = lerpf(base_damage * current_damage_mult, min_damage * current_damage_mult, falloff_factor)
|
||||
|
||||
if result.collider.has_method("take_damage"):
|
||||
result.collider.take_damage(damage, result.position, player)
|
||||
else:
|
||||
# Not a target, spawn a bullet hole
|
||||
ImpactSpawner.spawn(get_tree(), "bullet", result.position, result.normal, 0.1)
|
||||
|
||||
# Add this hit to exclude so next loop goes through it
|
||||
var exc = query.exclude
|
||||
exc.append(result.rid)
|
||||
|
||||
# If the collider has multiple shapes, we should really exclude the whole collision object
|
||||
# but RID of the body should be enough.
|
||||
query.exclude = exc
|
||||
|
||||
hits_remaining -= 1
|
||||
current_damage_mult *= penetration_damage_penalty
|
||||
final_target = result.position
|
||||
|
||||
# Spawn cosmetic tracer
|
||||
var tracer = Node3D.new()
|
||||
tracer.set_script(load("res://weapons/hitscan_tracer.gd"))
|
||||
|
||||
var t_mesh_inst = MeshInstance3D.new()
|
||||
var t_mesh = CapsuleMesh.new()
|
||||
t_mesh.radius = 0.02
|
||||
t_mesh.height = 1.0
|
||||
var t_mat = StandardMaterial3D.new()
|
||||
t_mat.albedo_color = Color(1.0, 0.8, 0.2) # Yellow-orange tracer
|
||||
t_mat.emission_enabled = true
|
||||
t_mat.emission = Color(2.0, 1.5, 0.5)
|
||||
t_mesh.material = t_mat
|
||||
|
||||
t_mesh_inst.mesh = t_mesh
|
||||
t_mesh_inst.rotation.x = deg_to_rad(90) # Orient capsule along Z axis
|
||||
t_mesh_inst.position = Vector3(0, 0, -0.5) # Shift it half a meter forward so the tail is at the origin
|
||||
tracer.add_child(t_mesh_inst)
|
||||
|
||||
tracer.target_position = final_target
|
||||
var visual_origin = muzzle_flash.global_position if muzzle_flash else global_position # Spawn from the barrel tip
|
||||
|
||||
if visual_origin.distance_squared_to(final_target) > 0.1:
|
||||
get_tree().current_scene.add_child(tracer)
|
||||
tracer.global_position = visual_origin
|
||||
|
||||
var up_vec = Vector3.UP
|
||||
if abs((final_target - visual_origin).normalized().dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
tracer.look_at(final_target, up_vec)
|
||||
@@ -0,0 +1 @@
|
||||
uid://l06bdsfbd0up
|
||||
@@ -0,0 +1,126 @@
|
||||
extends Node3D
|
||||
class_name BaseProjectileWeapon
|
||||
|
||||
@export var weapon_name: String = "Base Projectile"
|
||||
@export var fire_rate: float = 0.1
|
||||
@export var max_ammo: int = 30
|
||||
@export var reload_time: float = 1.5
|
||||
@export var base_damage: float = 10.0
|
||||
@export var min_damage: float = 5.0
|
||||
@export var falloff_start: float = 10.0
|
||||
@export var max_range: float = 200.0
|
||||
@export var automatic: bool = true
|
||||
@export var spread_angle: float = 0.01
|
||||
|
||||
@export var projectile_speed: float = 50.0
|
||||
@export var projectile_gravity: float = 0.0
|
||||
|
||||
var current_ammo: int = 30
|
||||
var reloading: bool = false
|
||||
var reload_timer: float = 0.0
|
||||
var fire_cooldown: float = 0.0
|
||||
var is_firing: bool = false
|
||||
|
||||
var player: CharacterBody3D
|
||||
var camera: Camera3D
|
||||
var muzzle_flash: OmniLight3D
|
||||
var fire_sound: AudioStreamPlayer3D
|
||||
var reload_sound: AudioStreamPlayer3D
|
||||
var model_root: Node3D
|
||||
|
||||
func _ready() -> void:
|
||||
current_ammo = max_ammo
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
|
||||
model_root = Node3D.new()
|
||||
add_child(model_root)
|
||||
_build_model()
|
||||
|
||||
func _build_model() -> void:
|
||||
pass
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if fire_cooldown > 0.0:
|
||||
fire_cooldown -= delta
|
||||
|
||||
if reloading:
|
||||
reload_timer -= delta
|
||||
var spin_speed = (PI * 2.0) / reload_time
|
||||
model_root.rotation.x += spin_speed * delta
|
||||
|
||||
if reload_timer <= 0.0:
|
||||
current_ammo = max_ammo
|
||||
reloading = false
|
||||
model_root.rotation.x = 0.0
|
||||
print(weapon_name, " Reloaded!")
|
||||
|
||||
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
|
||||
_fire()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
if event.is_action_pressed("fire"):
|
||||
is_firing = true
|
||||
if not automatic and fire_cooldown <= 0.0 and current_ammo > 0 and not reloading:
|
||||
_fire()
|
||||
elif current_ammo == 0 and not reloading:
|
||||
_start_reload()
|
||||
elif event.is_action_released("fire"):
|
||||
is_firing = false
|
||||
|
||||
if event.is_action_pressed("reload"):
|
||||
if current_ammo < max_ammo and not reloading:
|
||||
_start_reload()
|
||||
|
||||
func _start_reload() -> void:
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
is_firing = false
|
||||
if reload_sound:
|
||||
reload_sound.play()
|
||||
|
||||
func _fire() -> void:
|
||||
if current_ammo <= 0 or reloading:
|
||||
return
|
||||
|
||||
current_ammo -= 1
|
||||
fire_cooldown = fire_rate
|
||||
|
||||
_shoot_projectile()
|
||||
_play_muzzle_flash()
|
||||
|
||||
if fire_sound:
|
||||
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
|
||||
fire_sound.play()
|
||||
|
||||
if current_ammo == 0:
|
||||
_start_reload()
|
||||
|
||||
func _play_muzzle_flash() -> void:
|
||||
if muzzle_flash:
|
||||
muzzle_flash.light_energy = 8.0
|
||||
var tween = create_tween()
|
||||
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.05)
|
||||
|
||||
func _shoot_projectile() -> void:
|
||||
if not camera: return
|
||||
|
||||
var origin = camera.global_position
|
||||
var forward = -camera.global_transform.basis.z.normalized()
|
||||
var right = camera.global_transform.basis.x.normalized()
|
||||
var up = camera.global_transform.basis.y.normalized()
|
||||
|
||||
# Calculate spread
|
||||
var r = sqrt(randf()) * spread_angle
|
||||
var theta = randf() * PI * 2.0
|
||||
var offset_dir = (right * cos(theta) + up * sin(theta)) * r
|
||||
var fire_dir = (forward + offset_dir).normalized()
|
||||
|
||||
_spawn_custom_projectile(origin, fire_dir)
|
||||
|
||||
func _spawn_custom_projectile(_origin: Vector3, _fire_dir: Vector3) -> void:
|
||||
# Virtual method for custom projectiles (e.g., bouncing, homing)
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://cbkr1yr1xtfum
|
||||
@@ -0,0 +1,51 @@
|
||||
extends ExplosiveProjectile
|
||||
class_name BouncingProjectile
|
||||
|
||||
var fuse_time: float = -1.0 # -1 means no fuse started yet
|
||||
var bounce_fuse_delay: float = 3.0 # How long to wait after bouncing
|
||||
var impact_detonate_time: float = 2.0
|
||||
var bounciness: float = 0.4
|
||||
var flight_time: float = 0.0
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
flight_time += delta
|
||||
|
||||
if fuse_time > 0.0:
|
||||
fuse_time -= delta
|
||||
if fuse_time <= 0.0:
|
||||
_explode(global_position)
|
||||
queue_free()
|
||||
return
|
||||
|
||||
# Apply gravity drop if any
|
||||
velocity.y -= drop_gravity * delta
|
||||
|
||||
var movement = velocity * delta
|
||||
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsRayQueryParameters3D.create(_previous_position, global_position + movement)
|
||||
if owner_player:
|
||||
query.exclude = [owner_player.get_rid()]
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
if flight_time > impact_detonate_time:
|
||||
# Explode on impact if flying long enough
|
||||
_on_hit(result)
|
||||
else:
|
||||
# Bounce
|
||||
global_position = result.position + result.normal * 0.05
|
||||
_previous_position = global_position
|
||||
|
||||
# Reflect velocity
|
||||
velocity = velocity.bounce(result.normal) * bounciness
|
||||
|
||||
# Start the fuse if it hasn't started
|
||||
if fuse_time < 0.0:
|
||||
fuse_time = bounce_fuse_delay
|
||||
|
||||
# If it hits an entity directly, still bounce, it's a heavy mortar shell!
|
||||
else:
|
||||
global_position += movement
|
||||
distance_traveled += movement.length()
|
||||
_previous_position = global_position
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcmr6nvfk267e
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
extends BaseHitscanWeapon
|
||||
class_name DMR
|
||||
|
||||
var is_ads: bool = false
|
||||
var default_fov: float = 90.0
|
||||
var ads_fov: float = 40.0
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "DMR"
|
||||
fire_rate = 0.3 # Cooldown to prevent autoclicker spam
|
||||
max_ammo = 15
|
||||
reload_time = 2.2
|
||||
base_damage = 75.0
|
||||
min_damage = 60.0
|
||||
falloff_start = 50.0
|
||||
max_range = 300.0
|
||||
automatic = false
|
||||
spread_angle = 0.002 # Extremely accurate
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
if camera:
|
||||
default_fov = camera.fov
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.3, -0.3, -0.7)
|
||||
|
||||
var body_mat = StandardMaterial3D.new()
|
||||
body_mat.albedo_color = Color(0.15, 0.25, 0.15) # Olive drab
|
||||
var black_mat = StandardMaterial3D.new()
|
||||
black_mat.albedo_color = Color(0.05, 0.05, 0.05)
|
||||
|
||||
# Receiver
|
||||
var receiver = MeshInstance3D.new()
|
||||
var rec_mesh = BoxMesh.new()
|
||||
rec_mesh.size = Vector3(0.05, 0.12, 0.4)
|
||||
rec_mesh.material = body_mat
|
||||
receiver.mesh = rec_mesh
|
||||
receiver.position = Vector3(0, 0, 0.1)
|
||||
model_root.add_child(receiver)
|
||||
|
||||
# Barrel
|
||||
var barrel = MeshInstance3D.new()
|
||||
var b_mesh = CylinderMesh.new()
|
||||
b_mesh.top_radius = 0.01
|
||||
b_mesh.bottom_radius = 0.01
|
||||
b_mesh.height = 0.6
|
||||
b_mesh.material = black_mat
|
||||
barrel.mesh = b_mesh
|
||||
barrel.rotation.x = deg_to_rad(90)
|
||||
barrel.position = Vector3(0, 0.03, -0.3)
|
||||
model_root.add_child(barrel)
|
||||
|
||||
# Scope
|
||||
var scope = MeshInstance3D.new()
|
||||
var scope_mesh = CylinderMesh.new()
|
||||
scope_mesh.top_radius = 0.02
|
||||
scope_mesh.bottom_radius = 0.02
|
||||
scope_mesh.height = 0.2
|
||||
scope_mesh.material = black_mat
|
||||
scope.mesh = scope_mesh
|
||||
scope.rotation.x = deg_to_rad(90)
|
||||
scope.position = Vector3(0, 0.08, 0.05)
|
||||
model_root.add_child(scope)
|
||||
|
||||
# Muzzle Flash
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(1.0, 0.8, 0.4)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 6.0
|
||||
muzzle_flash.position = Vector3(0, 0.03, -0.6)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/dmr_fire.wav")
|
||||
fire_sound.position = muzzle_flash.position
|
||||
fire_sound.volume_db = -3.0
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
super._process(delta)
|
||||
|
||||
if camera:
|
||||
var target_fov = ads_fov if is_ads else default_fov
|
||||
camera.fov = lerpf(camera.fov, target_fov, 15.0 * delta)
|
||||
|
||||
# Move model to center when ADS
|
||||
var target_pos = Vector3(0.0, -0.2, -0.5) if is_ads else Vector3(0.3, -0.3, -0.7)
|
||||
position = position.lerp(target_pos, 15.0 * delta)
|
||||
|
||||
# Adjust sensitivity if player has a way to modify it (we can modify the camera rig later)
|
||||
# For now, FOV alone gives the zoom effect.
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
super._input(event)
|
||||
|
||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
if event.is_action_pressed("fire_alt"):
|
||||
is_ads = true
|
||||
elif event.is_action_released("fire_alt"):
|
||||
is_ads = false
|
||||
|
||||
func _start_reload() -> void:
|
||||
super._start_reload()
|
||||
is_ads = false # Drop out of ADS on reload
|
||||
|
||||
func unequip() -> void:
|
||||
is_ads = false
|
||||
if camera:
|
||||
camera.fov = default_fov
|
||||
position = Vector3(0.3, -0.3, -0.7)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dy2xsq04e6ryt
|
||||
@@ -0,0 +1,230 @@
|
||||
extends Node3D
|
||||
class_name DoubleBarrelShotgun
|
||||
|
||||
@export var reload_time: float = 1.0
|
||||
|
||||
var shells: int = 2
|
||||
var reloading: bool = false
|
||||
var reload_timer: float = 0.0
|
||||
|
||||
var player: CharacterBody3D
|
||||
var camera: Camera3D
|
||||
var muzzle_flash: OmniLight3D
|
||||
var fire_sound: AudioStreamPlayer3D
|
||||
var reload_sound: AudioStreamPlayer3D
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
_build_model()
|
||||
|
||||
func _build_model() -> void:
|
||||
# Simple FPS weapon placement (bottom right)
|
||||
position = Vector3(0.3, -0.3, -0.6)
|
||||
|
||||
# Stock
|
||||
var stock = MeshInstance3D.new()
|
||||
var stock_mesh = BoxMesh.new()
|
||||
stock_mesh.size = Vector3(0.1, 0.1, 0.4)
|
||||
var stock_mat = StandardMaterial3D.new()
|
||||
stock_mat.albedo_color = Color(0.4, 0.2, 0.1) # Brown wood
|
||||
stock_mesh.material = stock_mat
|
||||
stock.mesh = stock_mesh
|
||||
stock.position = Vector3(0, 0, 0.2)
|
||||
add_child(stock)
|
||||
|
||||
# Left Barrel
|
||||
var barrel_l = MeshInstance3D.new()
|
||||
var barrel_mesh = CylinderMesh.new()
|
||||
barrel_mesh.top_radius = 0.03
|
||||
barrel_mesh.bottom_radius = 0.03
|
||||
barrel_mesh.height = 0.6
|
||||
var barrel_mat = StandardMaterial3D.new()
|
||||
barrel_mat.albedo_color = Color(0.2, 0.2, 0.2) # Dark grey metal
|
||||
barrel_mat.metallic = 0.8
|
||||
barrel_mat.roughness = 0.4
|
||||
barrel_mesh.material = barrel_mat
|
||||
barrel_l.mesh = barrel_mesh
|
||||
barrel_l.rotation.x = deg_to_rad(90)
|
||||
barrel_l.position = Vector3(-0.04, 0.03, -0.1)
|
||||
add_child(barrel_l)
|
||||
|
||||
# Right Barrel
|
||||
var barrel_r = MeshInstance3D.new()
|
||||
barrel_r.mesh = barrel_mesh
|
||||
barrel_r.rotation.x = deg_to_rad(90)
|
||||
barrel_r.position = Vector3(0.04, 0.03, -0.1)
|
||||
add_child(barrel_r)
|
||||
|
||||
# Muzzle Flash Light
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(1.0, 0.8, 0.3)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 4.0
|
||||
muzzle_flash.position = Vector3(0, 0.03, -0.45)
|
||||
add_child(muzzle_flash)
|
||||
|
||||
# Audio Players
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/shotgun_fire.wav")
|
||||
fire_sound.position = muzzle_flash.position
|
||||
fire_sound.volume_db = -20.0
|
||||
add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
|
||||
add_child(reload_sound)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if reloading:
|
||||
reload_timer -= delta
|
||||
|
||||
# Spin vertically (flip) over the duration of the reload
|
||||
var spin_speed = (PI * 2.0) / reload_time
|
||||
rotation.x += spin_speed * delta
|
||||
|
||||
if reload_timer <= 0.0:
|
||||
shells = 2
|
||||
reloading = false
|
||||
rotation.x = 0.0 # Snap back to perfectly level
|
||||
print("Shotgun Reloaded!")
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
if event.is_action_pressed("fire"):
|
||||
_try_fire()
|
||||
elif event.is_action_released("fire"):
|
||||
_try_fire()
|
||||
|
||||
if event.is_action_pressed("reload"):
|
||||
if shells < 2 and not reloading:
|
||||
_start_reload()
|
||||
|
||||
func _start_reload() -> void:
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
if reload_sound:
|
||||
reload_sound.play()
|
||||
|
||||
func _try_fire() -> void:
|
||||
if shells > 0 and not reloading:
|
||||
shells -= 1
|
||||
print("BANG! Shells left: ", shells)
|
||||
_apply_impulse()
|
||||
_shoot_hitscan()
|
||||
_play_muzzle_flash()
|
||||
if fire_sound:
|
||||
fire_sound.play()
|
||||
|
||||
# Start reload timer if we just shot the LAST shell
|
||||
if shells == 0:
|
||||
_start_reload()
|
||||
|
||||
elif shells == 0 and not reloading:
|
||||
# Empty click, force reload
|
||||
_start_reload()
|
||||
|
||||
func _apply_impulse() -> void:
|
||||
if not player or not camera:
|
||||
return
|
||||
|
||||
# Get the shotgun impulse parameter from the player's movement controller
|
||||
var impulse_strength = 20.0
|
||||
if player.has_method("get") and "params" in player:
|
||||
if player.params and "shotgun_jump_impulse" in player.params:
|
||||
impulse_strength = player.params.shotgun_jump_impulse
|
||||
|
||||
# Impulse direction is opposite to the camera's forward direction
|
||||
var push_dir = camera.global_transform.basis.z.normalized()
|
||||
|
||||
# Apply impulse to player velocity
|
||||
player.velocity += push_dir * impulse_strength
|
||||
|
||||
# Small hack to ensure we aren't considered on floor if we shoot down
|
||||
if push_dir.y > 0:
|
||||
# Just slightly lift the player to break floor contact immediately
|
||||
player.position.y += 0.05
|
||||
|
||||
func _play_muzzle_flash() -> void:
|
||||
if muzzle_flash:
|
||||
muzzle_flash.light_energy = 8.0
|
||||
var tween = create_tween()
|
||||
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.1)
|
||||
|
||||
func _shoot_hitscan() -> void:
|
||||
if not camera: return
|
||||
|
||||
var space_state = camera.get_world_3d().direct_space_state
|
||||
var origin = camera.global_position
|
||||
var forward = -camera.global_transform.basis.z.normalized()
|
||||
var right = camera.global_transform.basis.x.normalized()
|
||||
var up = camera.global_transform.basis.y.normalized()
|
||||
|
||||
var num_pellets = 12
|
||||
var max_range = 50.0
|
||||
var base_dmg = 15.0
|
||||
var min_dmg = 3.0
|
||||
var falloff_start = 8.0
|
||||
var spread_angle = 0.08 # Radians
|
||||
|
||||
for i in range(num_pellets):
|
||||
# Random spread inside cone
|
||||
var r = sqrt(randf()) * spread_angle
|
||||
var theta = randf() * PI * 2.0
|
||||
var offset_dir = (right * cos(theta) + up * sin(theta)) * r
|
||||
var pellet_dir = (forward + offset_dir).normalized()
|
||||
|
||||
var target = origin + pellet_dir * max_range
|
||||
|
||||
var query = PhysicsRayQueryParameters3D.create(origin, target)
|
||||
if player:
|
||||
query.exclude = [player.get_rid()]
|
||||
|
||||
var final_target = target
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
final_target = result.position
|
||||
var hit_dist = origin.distance_to(result.position)
|
||||
var damage = base_dmg
|
||||
|
||||
if hit_dist > falloff_start:
|
||||
var falloff_factor = clampf((hit_dist - falloff_start) / (max_range - falloff_start), 0.0, 1.0)
|
||||
damage = lerpf(base_dmg, min_dmg, falloff_factor)
|
||||
|
||||
if result.collider.has_method("take_damage"):
|
||||
result.collider.take_damage(damage, result.position, player)
|
||||
else:
|
||||
ImpactSpawner.spawn(get_tree(), "bullet", result.position, result.normal, 0.08)
|
||||
|
||||
# Spawn cosmetic tracer
|
||||
var tracer = Node3D.new()
|
||||
tracer.set_script(load("res://weapons/hitscan_tracer.gd"))
|
||||
|
||||
var t_mesh_inst = MeshInstance3D.new()
|
||||
var t_mesh = CapsuleMesh.new()
|
||||
t_mesh.radius = 0.02
|
||||
t_mesh.height = 1.0
|
||||
var t_mat = StandardMaterial3D.new()
|
||||
t_mat.albedo_color = Color(1.0, 0.8, 0.2)
|
||||
t_mat.emission_enabled = true
|
||||
t_mat.emission = Color(2.0, 1.5, 0.5)
|
||||
t_mesh.material = t_mat
|
||||
|
||||
t_mesh_inst.mesh = t_mesh
|
||||
t_mesh_inst.rotation.x = deg_to_rad(90)
|
||||
t_mesh_inst.position = Vector3(0, 0, -0.5)
|
||||
tracer.add_child(t_mesh_inst)
|
||||
|
||||
tracer.target_position = final_target
|
||||
var visual_origin = muzzle_flash.global_position if muzzle_flash else global_position
|
||||
|
||||
if visual_origin.distance_squared_to(final_target) > 0.1:
|
||||
get_tree().current_scene.add_child(tracer)
|
||||
tracer.global_position = visual_origin
|
||||
|
||||
var up_vec = Vector3.UP
|
||||
if abs((final_target - visual_origin).normalized().dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
tracer.look_at(final_target, up_vec)
|
||||
@@ -0,0 +1 @@
|
||||
uid://chgdeglyrlf07
|
||||
@@ -0,0 +1,94 @@
|
||||
extends Projectile
|
||||
class_name ExplosiveProjectile
|
||||
|
||||
var explosion_radius: float = 5.0
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
func _explode(pos: Vector3) -> void:
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
|
||||
# We use a sphere shape query to find all physics bodies in radius
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = explosion_radius
|
||||
|
||||
var params = PhysicsShapeQueryParameters3D.new()
|
||||
params.shape = sphere
|
||||
params.transform = Transform3D(Basis(), pos)
|
||||
# Find bodies and areas
|
||||
params.collide_with_bodies = true
|
||||
|
||||
var results = space_state.intersect_shape(params)
|
||||
|
||||
# We want to only damage each object once (intersect_shape can return multiple shapes of the same object)
|
||||
var processed_rids = {}
|
||||
|
||||
for res in results:
|
||||
var col = res.collider
|
||||
if not col or processed_rids.has(res.rid):
|
||||
continue
|
||||
processed_rids[res.rid] = true
|
||||
|
||||
# Calculate distance
|
||||
var target_pos = col.global_position
|
||||
# If it's a character, maybe aim at their center (offset y by 1.0)
|
||||
if col is CharacterBody3D or col.has_method("take_damage"):
|
||||
if col.has_node("CollisionShape3D"):
|
||||
target_pos = col.get_node("CollisionShape3D").global_position
|
||||
|
||||
var dist = pos.distance_to(target_pos)
|
||||
if dist > explosion_radius:
|
||||
continue
|
||||
|
||||
# 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 final_damage = damage * intensity
|
||||
var final_knockback = explosion_knockback * intensity
|
||||
|
||||
# Knockback direction: from explosion pos to target
|
||||
var dir = (target_pos - pos).normalized()
|
||||
# Add a tiny bit of upward bias to knockback so it lifts them
|
||||
dir = (dir + Vector3(0, 0.5, 0)).normalized()
|
||||
|
||||
# Apply knockback if it has apply_impulse
|
||||
if col.has_method("apply_impulse"):
|
||||
col.apply_impulse(dir * final_knockback)
|
||||
|
||||
# Apply damage
|
||||
if col.has_method("take_damage"):
|
||||
if col == owner_player and not can_self_damage:
|
||||
# Skip self damage, but we still applied the knockback!
|
||||
pass
|
||||
else:
|
||||
col.take_damage(final_damage, target_pos, owner_player)
|
||||
|
||||
# Visual Effect
|
||||
var visual = MeshInstance3D.new()
|
||||
var s_mesh = SphereMesh.new()
|
||||
s_mesh.radius = explosion_radius
|
||||
s_mesh.height = explosion_radius * 2.0
|
||||
visual.mesh = s_mesh
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(1.0, 0.5, 0.0, 0.6) # Translucent orange
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.3, 0.0)
|
||||
visual.material_override = mat
|
||||
|
||||
visual.position = pos
|
||||
get_tree().current_scene.add_child(visual)
|
||||
|
||||
var tween = visual.create_tween()
|
||||
tween.tween_property(mat, "albedo_color:a", 0.0, 0.3)
|
||||
tween.parallel().tween_property(visual, "scale", Vector3(1.1, 1.1, 1.1), 0.3)
|
||||
tween.tween_callback(visual.queue_free)
|
||||
@@ -0,0 +1 @@
|
||||
uid://3pfuatif484y
|
||||
@@ -0,0 +1,22 @@
|
||||
extends Node3D
|
||||
class_name HitscanTracer
|
||||
|
||||
var speed: float = 300.0 # Extremely fast
|
||||
var target_position: Vector3
|
||||
var distance_traveled: float = 0.0
|
||||
var max_distance: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
max_distance = global_position.distance_to(target_position)
|
||||
if max_distance < 0.1:
|
||||
queue_free()
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
var move_dist = speed * delta
|
||||
distance_traveled += move_dist
|
||||
|
||||
if distance_traveled >= max_distance:
|
||||
# Reached target
|
||||
queue_free()
|
||||
else:
|
||||
global_position += -global_transform.basis.z * move_dist
|
||||
@@ -0,0 +1 @@
|
||||
uid://df1jms63ejdnl
|
||||
@@ -0,0 +1,42 @@
|
||||
extends ExplosiveProjectile
|
||||
class_name HomingProjectile
|
||||
|
||||
var weapon_source: Node
|
||||
var turn_speed: float = 5.0
|
||||
|
||||
var initial_aim_dir: Vector3 = Vector3.ZERO
|
||||
var cruise_speed: float = 60.0
|
||||
var ignition_timer: float = 0.5
|
||||
var ignited: bool = false
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not ignited:
|
||||
ignition_timer -= delta
|
||||
if ignition_timer <= 0.0:
|
||||
ignited = true
|
||||
drop_gravity = 0.0
|
||||
|
||||
if ignited and is_instance_valid(weapon_source):
|
||||
var target_dir: Vector3 = initial_aim_dir
|
||||
var is_painting = weapon_source.get("is_targeting")
|
||||
|
||||
if is_painting != null and is_painting:
|
||||
var target: Vector3 = weapon_source.get("homing_target_pos")
|
||||
if target != null and target != Vector3.ZERO:
|
||||
target_dir = (target - global_position).normalized()
|
||||
|
||||
if target_dir != Vector3.ZERO:
|
||||
var current_dir = velocity.normalized()
|
||||
var new_dir = current_dir.lerp(target_dir, turn_speed * delta).normalized()
|
||||
|
||||
# Accelerate to cruise speed
|
||||
var current_speed = velocity.length()
|
||||
var new_speed = move_toward(current_speed, cruise_speed, 80.0 * delta)
|
||||
velocity = new_dir * new_speed
|
||||
|
||||
var up_vec = Vector3.UP
|
||||
if abs(new_dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
transform.basis = Basis.looking_at(new_dir, up_vec)
|
||||
|
||||
super._physics_process(delta)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d12r4xife61uv
|
||||
@@ -0,0 +1,86 @@
|
||||
extends Node
|
||||
class_name ImpactSpawner
|
||||
|
||||
static var bullet_hole_mat: StandardMaterial3D = null
|
||||
static var crater_mat: StandardMaterial3D = null
|
||||
static var plasma_mat: StandardMaterial3D = null
|
||||
|
||||
static func _ensure_mats():
|
||||
if bullet_hole_mat != null:
|
||||
return
|
||||
|
||||
var make_tex = func(colors: Array, offsets: Array, size: int) -> Texture2D:
|
||||
var g = Gradient.new()
|
||||
# For Godot 4 Gradient, we set offsets and colors directly
|
||||
var p_offsets = PackedFloat32Array()
|
||||
var p_colors = PackedColorArray()
|
||||
for o in offsets: p_offsets.append(o)
|
||||
for c in colors: p_colors.append(c)
|
||||
g.offsets = p_offsets
|
||||
g.colors = p_colors
|
||||
|
||||
var t = GradientTexture2D.new()
|
||||
t.gradient = g
|
||||
t.fill = GradientTexture2D.FILL_RADIAL
|
||||
t.fill_from = Vector2(0.5, 0.5)
|
||||
t.fill_to = Vector2(0.5, 0.0)
|
||||
t.width = size
|
||||
t.height = size
|
||||
return t
|
||||
|
||||
var tex_b = make_tex.call(
|
||||
[Color(0.01, 0.01, 0.01, 1.0), Color(0.1, 0.1, 0.1, 0.8), Color(0.1, 0.1, 0.1, 0.0)],
|
||||
[0.0, 0.3, 1.0], 32)
|
||||
bullet_hole_mat = StandardMaterial3D.new()
|
||||
bullet_hole_mat.albedo_texture = tex_b
|
||||
bullet_hole_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
|
||||
var tex_c = make_tex.call(
|
||||
[Color(0.05, 0.05, 0.05, 0.9), Color(0.1, 0.1, 0.1, 0.6), Color(0.1, 0.1, 0.1, 0.0)],
|
||||
[0.0, 0.5, 1.0], 128)
|
||||
crater_mat = StandardMaterial3D.new()
|
||||
crater_mat.albedo_texture = tex_c
|
||||
crater_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
|
||||
var tex_p = make_tex.call(
|
||||
[Color(0.8, 0.9, 1.0, 1.0), Color(0.2, 0.6, 1.0, 0.8), Color(0.1, 0.3, 0.8, 0.0)],
|
||||
[0.0, 0.2, 1.0], 64)
|
||||
plasma_mat = StandardMaterial3D.new()
|
||||
plasma_mat.albedo_texture = tex_p
|
||||
plasma_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
plasma_mat.emission_enabled = true
|
||||
plasma_mat.emission_texture = tex_p
|
||||
plasma_mat.emission_energy_multiplier = 2.0
|
||||
|
||||
static func spawn(scene_tree: SceneTree, type: String, pos: Vector3, normal: Vector3, size: float = 0.2):
|
||||
_ensure_mats()
|
||||
|
||||
var mi = MeshInstance3D.new()
|
||||
var qm = QuadMesh.new()
|
||||
qm.size = Vector2(size, size)
|
||||
|
||||
var mat: StandardMaterial3D
|
||||
if type == "bullet":
|
||||
mat = bullet_hole_mat.duplicate()
|
||||
elif type == "crater":
|
||||
mat = crater_mat.duplicate()
|
||||
elif type == "plasma":
|
||||
mat = plasma_mat.duplicate()
|
||||
|
||||
qm.material = mat
|
||||
mi.mesh = qm
|
||||
|
||||
scene_tree.current_scene.add_child(mi)
|
||||
|
||||
mi.global_position = pos + normal * 0.01
|
||||
|
||||
if abs(normal.dot(Vector3.UP)) < 0.999:
|
||||
mi.look_at(mi.global_position - normal, Vector3.UP)
|
||||
else:
|
||||
mi.look_at(mi.global_position - normal, Vector3.RIGHT)
|
||||
|
||||
var duration = 5.0 if type == "crater" else 3.0
|
||||
var tween = mi.create_tween()
|
||||
tween.tween_interval(duration)
|
||||
tween.tween_property(mat, "albedo_color:a", 0.0, 1.0)
|
||||
tween.tween_callback(mi.queue_free)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bf5h8cxi8kct2
|
||||
@@ -0,0 +1,71 @@
|
||||
extends BaseHitscanWeapon
|
||||
class_name M4
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "M4"
|
||||
fire_rate = 0.08 # ~750 RPM
|
||||
max_ammo = 40
|
||||
reload_time = 1.6
|
||||
base_damage = 22.0
|
||||
min_damage = 12.0
|
||||
falloff_start = 15.0
|
||||
max_range = 100.0
|
||||
automatic = true
|
||||
spread_angle = 0.01
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.3, -0.3, -0.6)
|
||||
|
||||
var grey_mat = StandardMaterial3D.new()
|
||||
grey_mat.albedo_color = Color(0.25, 0.25, 0.25)
|
||||
var dark_mat = StandardMaterial3D.new()
|
||||
dark_mat.albedo_color = Color(0.1, 0.1, 0.1)
|
||||
|
||||
# Receiver
|
||||
var receiver = MeshInstance3D.new()
|
||||
var rec_mesh = BoxMesh.new()
|
||||
rec_mesh.size = Vector3(0.05, 0.14, 0.25)
|
||||
rec_mesh.material = grey_mat
|
||||
receiver.mesh = rec_mesh
|
||||
receiver.position = Vector3(0, 0, 0.1)
|
||||
model_root.add_child(receiver)
|
||||
|
||||
# Barrel
|
||||
var barrel = MeshInstance3D.new()
|
||||
var b_mesh = CylinderMesh.new()
|
||||
b_mesh.top_radius = 0.012
|
||||
b_mesh.bottom_radius = 0.012
|
||||
b_mesh.height = 0.4
|
||||
b_mesh.material = dark_mat
|
||||
barrel.mesh = b_mesh
|
||||
barrel.rotation.x = deg_to_rad(90)
|
||||
barrel.position = Vector3(0, 0.04, -0.2)
|
||||
model_root.add_child(barrel)
|
||||
|
||||
# Handguard
|
||||
var guard = MeshInstance3D.new()
|
||||
var g_mesh = BoxMesh.new()
|
||||
g_mesh.size = Vector3(0.06, 0.07, 0.22)
|
||||
g_mesh.material = dark_mat
|
||||
guard.mesh = g_mesh
|
||||
guard.position = Vector3(0, 0.04, -0.1)
|
||||
model_root.add_child(guard)
|
||||
|
||||
# Muzzle Flash
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(1.0, 0.8, 0.4)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 4.0
|
||||
muzzle_flash.position = Vector3(0, 0.04, -0.4)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/m4_fire.wav")
|
||||
fire_sound.position = muzzle_flash.position
|
||||
fire_sound.volume_db = -8.0
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
|
||||
model_root.add_child(reload_sound)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bctti6s7em04f
|
||||
@@ -0,0 +1,85 @@
|
||||
extends BaseProjectileWeapon
|
||||
class_name Mortar
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "Mortar"
|
||||
fire_rate = 1.5 # Slow
|
||||
max_ammo = 1
|
||||
reload_time = 3.0 # Extremely long
|
||||
base_damage = 200.0 # Huge damage
|
||||
min_damage = 20.0
|
||||
falloff_start = 0.0
|
||||
max_range = 500.0
|
||||
automatic = false
|
||||
spread_angle = 0.0
|
||||
|
||||
projectile_speed = 25.0 # Very slow
|
||||
projectile_gravity = 15.0 # Heavy drop
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.3, -0.4, -0.5)
|
||||
|
||||
var mat_olive = StandardMaterial3D.new()
|
||||
mat_olive.albedo_color = Color(0.2, 0.3, 0.1)
|
||||
|
||||
# Thick short tube
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = CylinderMesh.new()
|
||||
b_mesh.top_radius = 0.12
|
||||
b_mesh.bottom_radius = 0.12
|
||||
b_mesh.height = 0.5
|
||||
b_mesh.material = mat_olive
|
||||
body.mesh = b_mesh
|
||||
body.rotation.x = deg_to_rad(90)
|
||||
model_root.add_child(body)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/mortar_fire.wav")
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
var proj = load("res://weapons/bouncing_projectile.gd").new()
|
||||
|
||||
# Mesh
|
||||
var mesh_instance = MeshInstance3D.new()
|
||||
var p_mesh = SphereMesh.new()
|
||||
p_mesh.radius = 0.1
|
||||
p_mesh.height = 0.2
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.1, 0.1, 0.1) # Black bomb
|
||||
p_mesh.material = mat
|
||||
mesh_instance.mesh = p_mesh
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
# Logic
|
||||
proj.position = origin + fire_dir * 0.2
|
||||
proj.logical_source = origin
|
||||
proj.velocity = fire_dir * projectile_speed
|
||||
if player:
|
||||
proj.velocity += fire_dir * player.velocity.dot(fire_dir)
|
||||
proj.damage = base_damage
|
||||
proj.min_damage = min_damage
|
||||
proj.falloff_start = falloff_start
|
||||
proj.max_range = max_range
|
||||
proj.drop_gravity = projectile_gravity
|
||||
proj.owner_player = player
|
||||
|
||||
# Mortar Bouncing logic
|
||||
proj.fuse_time = -1.0 # Don't start ticking until it bounces
|
||||
proj.bounce_fuse_delay = 3.0 # Explodes 3 seconds after first bounce
|
||||
proj.impact_detonate_time = 2.0 # If flying > 2s, explode on impact instead
|
||||
proj.bounciness = 0.4 # Heavy, loses momentum on bounce
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 12.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)
|
||||
proj.can_self_damage = true # Can self damage
|
||||
|
||||
get_tree().current_scene.add_child(proj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b652882c1vye7
|
||||
@@ -0,0 +1,80 @@
|
||||
extends BaseHitscanWeapon
|
||||
class_name MP7
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "MP7"
|
||||
fire_rate = 0.05 # ~1200 RPM
|
||||
max_ammo = 45
|
||||
reload_time = 1.2
|
||||
base_damage = 12.0
|
||||
min_damage = 3.0
|
||||
falloff_start = 8.0
|
||||
max_range = 50.0
|
||||
automatic = true
|
||||
spread_angle = 0.025
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.25, -0.25, -0.5)
|
||||
|
||||
var black_mat = StandardMaterial3D.new()
|
||||
black_mat.albedo_color = Color(0.05, 0.05, 0.05)
|
||||
|
||||
# Receiver
|
||||
var receiver = MeshInstance3D.new()
|
||||
var rec_mesh = BoxMesh.new()
|
||||
rec_mesh.size = Vector3(0.04, 0.1, 0.2)
|
||||
rec_mesh.material = black_mat
|
||||
receiver.mesh = rec_mesh
|
||||
receiver.position = Vector3(0, 0, 0)
|
||||
model_root.add_child(receiver)
|
||||
|
||||
# Grip
|
||||
var grip = MeshInstance3D.new()
|
||||
var g_mesh = BoxMesh.new()
|
||||
g_mesh.size = Vector3(0.03, 0.12, 0.04)
|
||||
g_mesh.material = black_mat
|
||||
grip.mesh = g_mesh
|
||||
grip.position = Vector3(0, -0.06, 0.08)
|
||||
grip.rotation.x = deg_to_rad(10)
|
||||
model_root.add_child(grip)
|
||||
|
||||
# Foregrip
|
||||
var fgrip = MeshInstance3D.new()
|
||||
var fg_mesh = BoxMesh.new()
|
||||
fg_mesh.size = Vector3(0.03, 0.08, 0.03)
|
||||
fg_mesh.material = black_mat
|
||||
fgrip.mesh = fg_mesh
|
||||
fgrip.position = Vector3(0, -0.05, -0.06)
|
||||
fgrip.rotation.x = deg_to_rad(-10)
|
||||
model_root.add_child(fgrip)
|
||||
|
||||
# Barrel
|
||||
var barrel = MeshInstance3D.new()
|
||||
var b_mesh = CylinderMesh.new()
|
||||
b_mesh.top_radius = 0.008
|
||||
b_mesh.bottom_radius = 0.008
|
||||
b_mesh.height = 0.1
|
||||
b_mesh.material = black_mat
|
||||
barrel.mesh = b_mesh
|
||||
barrel.rotation.x = deg_to_rad(90)
|
||||
barrel.position = Vector3(0, 0.02, -0.15)
|
||||
model_root.add_child(barrel)
|
||||
|
||||
# Muzzle Flash
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(1.0, 0.9, 0.5)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 3.0
|
||||
muzzle_flash.position = Vector3(0, 0.02, -0.2)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/mp7_fire.wav")
|
||||
fire_sound.position = muzzle_flash.position
|
||||
fire_sound.volume_db = -10.0
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
|
||||
model_root.add_child(reload_sound)
|
||||
@@ -0,0 +1 @@
|
||||
uid://unto0q3j6jix
|
||||
@@ -0,0 +1,127 @@
|
||||
extends BaseProjectileWeapon
|
||||
class_name NailGun
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "Nail Gun"
|
||||
fire_rate = 0.06 # Very fast (1000 RPM)
|
||||
max_ammo = 60
|
||||
reload_time = 2.0
|
||||
base_damage = 18.0
|
||||
min_damage = 10.0
|
||||
falloff_start = 20.0
|
||||
max_range = 100.0
|
||||
automatic = true
|
||||
spread_angle = 0.02
|
||||
|
||||
projectile_speed = 60.0 # Fast but requires lead
|
||||
projectile_gravity = 5.0 # Slight drop
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.3, -0.3, -0.6)
|
||||
|
||||
var yellow_mat = StandardMaterial3D.new()
|
||||
yellow_mat.albedo_color = Color(0.8, 0.7, 0.1) # Construction yellow
|
||||
var dark_mat = StandardMaterial3D.new()
|
||||
dark_mat.albedo_color = Color(0.2, 0.2, 0.2)
|
||||
|
||||
# Body
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = BoxMesh.new()
|
||||
b_mesh.size = Vector3(0.06, 0.16, 0.25)
|
||||
b_mesh.material = yellow_mat
|
||||
body.mesh = b_mesh
|
||||
body.position = Vector3(0, 0.02, 0)
|
||||
model_root.add_child(body)
|
||||
|
||||
# Handle
|
||||
var handle = MeshInstance3D.new()
|
||||
var h_mesh = BoxMesh.new()
|
||||
h_mesh.size = Vector3(0.04, 0.1, 0.04)
|
||||
h_mesh.material = dark_mat
|
||||
handle.mesh = h_mesh
|
||||
handle.position = Vector3(0, -0.1, 0.05)
|
||||
model_root.add_child(handle)
|
||||
|
||||
# Mag (Nail Drum)
|
||||
var drum = MeshInstance3D.new()
|
||||
var d_mesh = CylinderMesh.new()
|
||||
d_mesh.top_radius = 0.05
|
||||
d_mesh.bottom_radius = 0.05
|
||||
d_mesh.height = 0.1
|
||||
d_mesh.material = dark_mat
|
||||
drum.mesh = d_mesh
|
||||
drum.rotation.z = deg_to_rad(90)
|
||||
drum.position = Vector3(0, -0.06, -0.05)
|
||||
model_root.add_child(drum)
|
||||
|
||||
# Barrel
|
||||
var barrel = MeshInstance3D.new()
|
||||
var bar_mesh = CylinderMesh.new()
|
||||
bar_mesh.top_radius = 0.01
|
||||
bar_mesh.bottom_radius = 0.01
|
||||
bar_mesh.height = 0.15
|
||||
bar_mesh.material = dark_mat
|
||||
barrel.mesh = bar_mesh
|
||||
barrel.rotation.x = deg_to_rad(90)
|
||||
barrel.position = Vector3(0, 0.05, -0.2)
|
||||
model_root.add_child(barrel)
|
||||
|
||||
# Muzzle Flash (Sparks)
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(1.0, 0.9, 0.6)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 3.0
|
||||
muzzle_flash.position = Vector3(0, 0.05, -0.3)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/nailgun_fire.wav")
|
||||
fire_sound.position = muzzle_flash.position
|
||||
fire_sound.volume_db = -5.0
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
var proj = load("res://weapons/projectile.gd").new()
|
||||
|
||||
# Give it a nail/spike mesh
|
||||
var mesh_instance = MeshInstance3D.new()
|
||||
var p_mesh = CylinderMesh.new()
|
||||
p_mesh.top_radius = 0.0
|
||||
p_mesh.bottom_radius = 0.01
|
||||
p_mesh.height = 0.1
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.7, 0.7, 0.7) # Shiny metal
|
||||
mat.metallic = 1.0
|
||||
mat.roughness = 0.3
|
||||
p_mesh.material = mat
|
||||
mesh_instance.mesh = p_mesh
|
||||
# Rotate it so it points forward
|
||||
mesh_instance.rotation.x = deg_to_rad(-90)
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
# Projectile rotation so it faces velocity
|
||||
var up_vec = Vector3.UP
|
||||
if abs(fire_dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
proj.transform.basis = Basis.looking_at(fire_dir, up_vec)
|
||||
|
||||
proj.position = muzzle_flash.global_position if muzzle_flash else origin
|
||||
proj.logical_source = origin
|
||||
|
||||
proj.velocity = fire_dir * projectile_speed
|
||||
if player:
|
||||
proj.velocity += fire_dir * player.velocity.dot(fire_dir)
|
||||
proj.damage = base_damage
|
||||
proj.min_damage = min_damage
|
||||
proj.falloff_start = falloff_start
|
||||
proj.max_range = max_range
|
||||
proj.drop_gravity = projectile_gravity
|
||||
proj.owner_player = player
|
||||
|
||||
# Spawn in the world
|
||||
get_tree().current_scene.add_child(proj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkl7mdau5363
|
||||
@@ -0,0 +1,108 @@
|
||||
extends BaseProjectileWeapon
|
||||
class_name PlasmaGun
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "Plasma Gun"
|
||||
fire_rate = 0.15 # Moderate
|
||||
max_ammo = 25
|
||||
reload_time = 1.8
|
||||
base_damage = 50.0
|
||||
min_damage = 50.0 # No falloff
|
||||
falloff_start = 0.0 # Unused because min=base
|
||||
max_range = 300.0
|
||||
automatic = true
|
||||
spread_angle = 0.005 # Highly accurate
|
||||
|
||||
projectile_speed = 80.0
|
||||
projectile_gravity = 0.0
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.3, -0.3, -0.6)
|
||||
|
||||
var white_mat = StandardMaterial3D.new()
|
||||
white_mat.albedo_color = Color(0.8, 0.8, 0.9)
|
||||
var glow_mat = StandardMaterial3D.new()
|
||||
glow_mat.albedo_color = Color(0.2, 0.5, 1.0)
|
||||
glow_mat.emission_enabled = true
|
||||
glow_mat.emission = Color(0.2, 0.5, 1.0)
|
||||
glow_mat.emission_energy_multiplier = 4.0
|
||||
|
||||
# Body
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = BoxMesh.new()
|
||||
b_mesh.size = Vector3(0.08, 0.14, 0.35)
|
||||
b_mesh.material = white_mat
|
||||
body.mesh = b_mesh
|
||||
body.position = Vector3(0, 0, 0.05)
|
||||
model_root.add_child(body)
|
||||
|
||||
# Glow Core
|
||||
var core = MeshInstance3D.new()
|
||||
var c_mesh = CylinderMesh.new()
|
||||
c_mesh.top_radius = 0.02
|
||||
c_mesh.bottom_radius = 0.02
|
||||
c_mesh.height = 0.4
|
||||
c_mesh.material = glow_mat
|
||||
core.mesh = c_mesh
|
||||
core.rotation.x = deg_to_rad(90)
|
||||
core.position = Vector3(0, 0.02, -0.15)
|
||||
model_root.add_child(core)
|
||||
|
||||
# Muzzle Flash
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(0.2, 0.5, 1.0)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 6.0
|
||||
muzzle_flash.position = Vector3(0, 0.02, -0.4)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/plasma_fire.wav")
|
||||
fire_sound.position = muzzle_flash.position
|
||||
fire_sound.volume_db = -5.0
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
var proj = load("res://weapons/projectile.gd").new()
|
||||
|
||||
# Give it a glowing blue mesh
|
||||
var mesh_instance = MeshInstance3D.new()
|
||||
var p_mesh = SphereMesh.new()
|
||||
p_mesh.radius = 0.08
|
||||
p_mesh.height = 0.16
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.2, 0.6, 1.0)
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(0.2, 0.6, 1.0)
|
||||
mat.emission_energy_multiplier = 5.0
|
||||
p_mesh.material = mat
|
||||
mesh_instance.mesh = p_mesh
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
var light = OmniLight3D.new()
|
||||
light.light_color = Color(0.2, 0.6, 1.0)
|
||||
light.light_energy = 2.0
|
||||
light.omni_range = 4.0
|
||||
proj.add_child(light)
|
||||
|
||||
proj.position = muzzle_flash.global_position if muzzle_flash else origin
|
||||
proj.logical_source = origin
|
||||
|
||||
proj.velocity = fire_dir * projectile_speed
|
||||
if player:
|
||||
proj.velocity += fire_dir * player.velocity.dot(fire_dir)
|
||||
proj.damage = base_damage
|
||||
proj.min_damage = min_damage
|
||||
proj.falloff_start = falloff_start
|
||||
proj.max_range = max_range
|
||||
proj.drop_gravity = projectile_gravity
|
||||
proj.owner_player = player
|
||||
proj.impact_type = "plasma"
|
||||
|
||||
# Spawn in the world
|
||||
get_tree().current_scene.add_child(proj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://n156sg54wvpp
|
||||
@@ -0,0 +1,66 @@
|
||||
extends Node3D
|
||||
class_name Projectile
|
||||
|
||||
var velocity: Vector3 = Vector3.ZERO
|
||||
var damage: float = 10.0
|
||||
var max_range: float = 200.0
|
||||
var distance_traveled: float = 0.0
|
||||
var owner_player: CharacterBody3D
|
||||
var falloff_start: float = 10.0
|
||||
var min_damage: float = 5.0
|
||||
var drop_gravity: float = 0.0 # For arc projectiles (nailgun might have slight drop, plasma none)
|
||||
var impact_type: String = "bullet"
|
||||
|
||||
var logical_source = null
|
||||
var _previous_position: Vector3
|
||||
|
||||
func _ready() -> void:
|
||||
if logical_source != null:
|
||||
_previous_position = logical_source
|
||||
else:
|
||||
_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)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# Apply gravity drop if any
|
||||
velocity.y -= drop_gravity * delta
|
||||
|
||||
var movement = velocity * delta
|
||||
global_position += movement
|
||||
|
||||
distance_traveled += movement.length()
|
||||
|
||||
if distance_traveled > max_range:
|
||||
queue_free()
|
||||
return
|
||||
|
||||
_check_collision()
|
||||
_previous_position = global_position
|
||||
|
||||
func _check_collision() -> void:
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsRayQueryParameters3D.create(_previous_position, global_position)
|
||||
if owner_player:
|
||||
query.exclude = [owner_player.get_rid()]
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
_on_hit(result)
|
||||
|
||||
func _on_hit(result: Dictionary) -> void:
|
||||
var hit_dist = distance_traveled - (_previous_position.distance_to(result.position))
|
||||
|
||||
var final_damage = damage
|
||||
if hit_dist > falloff_start and falloff_start > 0.0:
|
||||
var falloff_factor = clampf((hit_dist - falloff_start) / (max_range - falloff_start), 0.0, 1.0)
|
||||
final_damage = lerpf(damage, min_damage, falloff_factor)
|
||||
|
||||
if result.collider.has_method("take_damage"):
|
||||
result.collider.take_damage(final_damage, result.position, owner_player)
|
||||
else:
|
||||
var size = 0.4 if impact_type == "plasma" else 0.15
|
||||
ImpactSpawner.spawn(get_tree(), impact_type, result.position, result.normal, size)
|
||||
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://davfge7rifxls
|
||||
@@ -0,0 +1,105 @@
|
||||
extends BaseProjectileWeapon
|
||||
class_name RocketLauncher
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "Rocket Launcher"
|
||||
fire_rate = 1.0 # Moderate, but you only have 1 in the chamber anyway
|
||||
max_ammo = 1
|
||||
reload_time = 2.5 # Extremely long reload time
|
||||
base_damage = 120.0 # High damage
|
||||
min_damage = 120.0
|
||||
falloff_start = 0.0
|
||||
max_range = 300.0
|
||||
automatic = false
|
||||
spread_angle = 0.0
|
||||
|
||||
projectile_speed = 35.0 # Moderate speed
|
||||
projectile_gravity = 0.0 # Flies straight
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.4, -0.4, -0.6)
|
||||
|
||||
var mat_grey = StandardMaterial3D.new()
|
||||
mat_grey.albedo_color = Color(0.3, 0.3, 0.3)
|
||||
|
||||
# Main Tube
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = CylinderMesh.new()
|
||||
b_mesh.top_radius = 0.08
|
||||
b_mesh.bottom_radius = 0.08
|
||||
b_mesh.height = 0.8
|
||||
b_mesh.material = mat_grey
|
||||
body.mesh = b_mesh
|
||||
body.rotation.x = deg_to_rad(90)
|
||||
model_root.add_child(body)
|
||||
|
||||
# Back cone
|
||||
var cone = MeshInstance3D.new()
|
||||
var c_mesh = CylinderMesh.new()
|
||||
c_mesh.top_radius = 0.08
|
||||
c_mesh.bottom_radius = 0.04
|
||||
c_mesh.height = 0.2
|
||||
c_mesh.material = mat_grey
|
||||
cone.mesh = c_mesh
|
||||
cone.rotation.x = deg_to_rad(90)
|
||||
cone.position = Vector3(0, 0, 0.5)
|
||||
model_root.add_child(cone)
|
||||
|
||||
# Muzzle Flash
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(1.0, 0.6, 0.2)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 5.0
|
||||
muzzle_flash.position = Vector3(0, 0, -0.4)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/rocket_fire.wav")
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
var proj = load("res://weapons/explosive_projectile.gd").new()
|
||||
|
||||
# Mesh
|
||||
var mesh_instance = MeshInstance3D.new()
|
||||
var p_mesh = CylinderMesh.new()
|
||||
p_mesh.top_radius = 0.0
|
||||
p_mesh.bottom_radius = 0.05
|
||||
p_mesh.height = 0.2
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.8, 0.2, 0.2) # Red rocket
|
||||
p_mesh.material = mat
|
||||
mesh_instance.mesh = p_mesh
|
||||
mesh_instance.rotation.x = deg_to_rad(-90)
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
# Rotation
|
||||
var up_vec = Vector3.UP
|
||||
if abs(fire_dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
proj.transform.basis = Basis.looking_at(fire_dir, up_vec)
|
||||
|
||||
# Logic
|
||||
proj.position = muzzle_flash.global_position if muzzle_flash else origin
|
||||
proj.logical_source = origin
|
||||
proj.velocity = fire_dir * projectile_speed
|
||||
if player:
|
||||
proj.velocity += fire_dir * player.velocity.dot(fire_dir)
|
||||
proj.damage = base_damage
|
||||
proj.min_damage = min_damage
|
||||
proj.falloff_start = falloff_start
|
||||
proj.max_range = max_range
|
||||
proj.drop_gravity = projectile_gravity
|
||||
proj.owner_player = player
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 8.0 # Moderate AoE
|
||||
proj.explosion_knockback = 45.0 # Strong knockback!
|
||||
proj.can_self_damage = false # As requested
|
||||
|
||||
get_tree().current_scene.add_child(proj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://kw1ikqtc34wr
|
||||
@@ -0,0 +1,191 @@
|
||||
extends BaseProjectileWeapon
|
||||
class_name RocketSwarm
|
||||
|
||||
var is_targeting: bool = false
|
||||
var homing_target_pos: Vector3 = Vector3.ZERO
|
||||
var targeting_laser: MeshInstance3D
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "Rocket Swarm"
|
||||
fire_rate = 1.0 # 1 click fires all 8
|
||||
max_ammo = 4 # 4 swarms
|
||||
reload_time = 3.0
|
||||
base_damage = 20.0 # Per mini rocket
|
||||
min_damage = 20.0
|
||||
falloff_start = 0.0
|
||||
max_range = 300.0
|
||||
automatic = false
|
||||
spread_angle = 0.15 # Wide spread out of the barrel
|
||||
|
||||
projectile_speed = 25.0 # Slower so they have time to home
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.4, -0.4, -0.6)
|
||||
|
||||
var mat_box = StandardMaterial3D.new()
|
||||
mat_box.albedo_color = Color(0.2, 0.4, 0.5)
|
||||
|
||||
# Box launcher
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = BoxMesh.new()
|
||||
b_mesh.size = Vector3(0.2, 0.2, 0.4)
|
||||
b_mesh.material = mat_box
|
||||
body.mesh = b_mesh
|
||||
model_root.add_child(body)
|
||||
|
||||
# Laser
|
||||
targeting_laser = MeshInstance3D.new()
|
||||
var l_mesh = CylinderMesh.new()
|
||||
l_mesh.top_radius = 0.02 # Make it thicker and more visible
|
||||
l_mesh.bottom_radius = 0.02
|
||||
l_mesh.height = 300.0
|
||||
var l_mat = StandardMaterial3D.new()
|
||||
l_mat.albedo_color = Color(1.0, 0.0, 0.0, 1.0)
|
||||
l_mat.emission_enabled = true
|
||||
l_mat.emission = Color(2.0, 0.0, 0.0) # Bright red
|
||||
l_mesh.material = l_mat
|
||||
targeting_laser.mesh = l_mesh
|
||||
targeting_laser.rotation.x = deg_to_rad(90)
|
||||
targeting_laser.position = Vector3(0, 0, -150.0)
|
||||
targeting_laser.visible = false
|
||||
model_root.add_child(targeting_laser)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/swarm_fire.wav")
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
super._process(delta)
|
||||
|
||||
if camera:
|
||||
is_targeting = Input.is_action_pressed("fire_alt")
|
||||
targeting_laser.visible = is_targeting
|
||||
|
||||
if is_targeting:
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var origin = camera.global_position
|
||||
var target = origin - camera.global_transform.basis.z * 300.0
|
||||
var query = PhysicsRayQueryParameters3D.create(origin, target)
|
||||
if player:
|
||||
query.exclude = [player.get_rid()]
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
homing_target_pos = result.position
|
||||
# Shrink laser to hit point
|
||||
var dist = origin.distance_to(result.position)
|
||||
(targeting_laser.mesh as CylinderMesh).height = dist
|
||||
targeting_laser.position.z = -dist / 2.0
|
||||
else:
|
||||
homing_target_pos = target
|
||||
(targeting_laser.mesh as CylinderMesh).height = 300.0
|
||||
targeting_laser.position.z = -150.0
|
||||
|
||||
func unequip() -> void:
|
||||
is_targeting = false
|
||||
targeting_laser.visible = false
|
||||
|
||||
# Override fire to shoot 8 rockets at once
|
||||
func _fire() -> void:
|
||||
if current_ammo <= 0 and not reloading:
|
||||
_start_reload()
|
||||
return
|
||||
|
||||
if not camera: return
|
||||
|
||||
current_ammo -= 1
|
||||
fire_cooldown = fire_rate
|
||||
|
||||
if fire_sound:
|
||||
fire_sound.play()
|
||||
|
||||
if muzzle_flash:
|
||||
muzzle_flash.light_energy = 2.0
|
||||
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 base_dir = -camera.global_transform.basis.z
|
||||
|
||||
_current_aim_dir = base_dir
|
||||
|
||||
for i in range(8):
|
||||
var right = camera.global_transform.basis.x
|
||||
var up = camera.global_transform.basis.y
|
||||
|
||||
var fraction = float(i) / 7.0 # 0.0 to 1.0 (left to right)
|
||||
|
||||
# Offset from left (-0.1) to right (0.1) on the top edge of the launcher
|
||||
var local_x = lerpf(-0.1, 0.1, fraction)
|
||||
var local_offset = Vector3(local_x, 0.1, 0.0)
|
||||
|
||||
var launcher_center_local = Vector3(0.4, -0.4, -0.6)
|
||||
var proj_origin = camera.global_position + camera.global_transform.basis * (launcher_center_local + local_offset)
|
||||
|
||||
# Fire direction: radial fan out
|
||||
# They shoot mostly forward, but fan out horizontally, and slightly arch upwards radially
|
||||
var angle = lerpf(-PI/3.0, PI/3.0, fraction) # -60 deg to +60 deg fan
|
||||
var x_dir = sin(angle) * 0.6
|
||||
var y_dir = cos(angle) * 0.8 # Reduced upward angle so it's not mostly straight up
|
||||
|
||||
var fire_dir = (base_dir + right * x_dir + up * y_dir).normalized()
|
||||
_spawn_custom_projectile(proj_origin, fire_dir)
|
||||
|
||||
var _current_aim_dir: Vector3 = Vector3.ZERO
|
||||
|
||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
var proj = load("res://weapons/homing_projectile.gd").new()
|
||||
|
||||
# Mesh
|
||||
var mesh_instance = MeshInstance3D.new()
|
||||
var p_mesh = CylinderMesh.new()
|
||||
p_mesh.top_radius = 0.0
|
||||
p_mesh.bottom_radius = 0.02
|
||||
p_mesh.height = 0.1
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.8, 0.8, 0.1) # Yellow mini rocket
|
||||
p_mesh.material = mat
|
||||
mesh_instance.mesh = p_mesh
|
||||
mesh_instance.rotation.x = deg_to_rad(-90)
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
# Rotation
|
||||
var up_vec = Vector3.UP
|
||||
if abs(fire_dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
proj.transform.basis = Basis.looking_at(fire_dir, up_vec)
|
||||
|
||||
# Logic
|
||||
proj.weapon_source = self
|
||||
proj.turn_speed = 4.0 # Slower turn radius so they sweep nicely
|
||||
|
||||
proj.position = origin # Start at origin to spread outward around camera
|
||||
proj.logical_source = origin
|
||||
|
||||
# Initial pop-up speed
|
||||
proj.velocity = fire_dir * 5.0
|
||||
if player:
|
||||
proj.velocity += fire_dir * player.velocity.dot(fire_dir)
|
||||
|
||||
proj.initial_aim_dir = _current_aim_dir
|
||||
proj.cruise_speed = 65.0
|
||||
proj.ignition_timer = randf_range(0.3, 0.6) # Staggered ignition
|
||||
|
||||
proj.damage = base_damage
|
||||
proj.min_damage = min_damage
|
||||
proj.falloff_start = falloff_start
|
||||
proj.max_range = max_range
|
||||
proj.drop_gravity = 5.0 # Slight arc before ignition
|
||||
proj.owner_player = player
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 3.0 # Tiny AoE effect
|
||||
proj.explosion_knockback = 5.0
|
||||
proj.can_self_damage = false
|
||||
|
||||
get_tree().current_scene.add_child(proj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2dla2at16k6d
|
||||
@@ -0,0 +1,82 @@
|
||||
extends Node3D
|
||||
class_name WeaponManager
|
||||
|
||||
var player: CharacterBody3D
|
||||
var camera: Camera3D
|
||||
|
||||
var active_slot: int = 1 # 1: Primary 1, 2: Primary 2, 3: Special
|
||||
var weapons: Dictionary = {} # slot_index: Node3D
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
|
||||
# Wait one frame for LoadoutManager to be fully ready if needed
|
||||
await get_tree().process_frame
|
||||
_build_loadout()
|
||||
|
||||
func _build_loadout() -> void:
|
||||
# Clear existing weapons
|
||||
for w in weapons.values():
|
||||
if is_instance_valid(w):
|
||||
w.queue_free()
|
||||
weapons.clear()
|
||||
|
||||
var l = LoadoutManager.get_active_loadout()
|
||||
|
||||
_spawn_weapon(1, l["primary_1"])
|
||||
_spawn_weapon(2, l["primary_2"])
|
||||
_spawn_weapon(3, l["special"])
|
||||
|
||||
_equip_slot(1)
|
||||
|
||||
func _spawn_weapon(slot: int, weapon_id: String) -> void:
|
||||
if weapon_id == "" or weapon_id == "none" or not LoadoutManager.weapon_db.has(weapon_id):
|
||||
return
|
||||
|
||||
var w_data = LoadoutManager.weapon_db[weapon_id]
|
||||
var script_path = w_data["script"]
|
||||
if script_path == "":
|
||||
return
|
||||
|
||||
var script = load(script_path)
|
||||
if script:
|
||||
var w = script.new()
|
||||
w.name = "Weapon_" + str(slot) + "_" + weapon_id
|
||||
# Assuming all weapons have player and camera vars
|
||||
if "player" in w: w.player = player
|
||||
if "camera" in w: w.camera = camera
|
||||
add_child(w)
|
||||
weapons[slot] = w
|
||||
w.visible = false
|
||||
w.set_process_input(false)
|
||||
|
||||
func _equip_slot(slot: int) -> void:
|
||||
if weapons.has(active_slot):
|
||||
var w = weapons[active_slot]
|
||||
w.visible = false
|
||||
w.set_process_input(false)
|
||||
if "is_firing" in w:
|
||||
w.is_firing = false
|
||||
if w.has_method("unequip"):
|
||||
w.unequip()
|
||||
|
||||
active_slot = slot
|
||||
|
||||
if weapons.has(active_slot):
|
||||
var w = weapons[active_slot]
|
||||
w.visible = true
|
||||
w.set_process_input(true)
|
||||
print("Equipped slot ", slot)
|
||||
else:
|
||||
print("Slot ", slot, " is empty!")
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
if event.is_action_pressed("weapon_1"):
|
||||
_equip_slot(1)
|
||||
elif event.is_action_pressed("weapon_2"):
|
||||
_equip_slot(2)
|
||||
elif event.is_action_pressed("weapon_3"):
|
||||
_equip_slot(3)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bp5ujeumiq5j
|
||||
Reference in New Issue
Block a user