Compare commits

...
31 Commits
Author SHA1 Message Date
Dotts 0912669759 Merge pull request 'Feat/2 weapons foundation' (#6) from feat/2-weapons-foundation into main
Reviewed-on: #6
2026-06-04 22:22:15 -07:00
DottsGit ca2a80e38b feat: reducing ms gain from shotguns. Adjusting swarm launcher 2026-06-05 01:19:51 -04:00
DottsGit 00f190d423 feat: reducing the wall jump speed 2026-06-05 00:56:59 -04:00
DottsGit 2c35f5074a feat: adds a cd to the jump to prevent spam jumping up a slope 2026-06-05 00:54:19 -04:00
DottsGit 9f3264170e feat: projectile speed is increased with respective player speed. Does not effect lateral projectile movement respective to the player 2026-06-05 00:42:22 -04:00
DottsGit eee1925b7a feat: create movement_params resource to centralize movement configuration variables 2026-06-05 00:29:45 -04:00
DottsGit da16a3a757 feat: implement weapon framework, hitscan logic, and player movement controller with state management 2026-06-05 00:25:41 -04:00
DottsGit ea96b41287 feat: adding tracers to hitscan weapons 2026-06-04 23:33:36 -04:00
DottsGit dcd0df2b2c feat: implement BaseHitscanWeapon class with raycasting, ammo management, and penetration support 2026-06-04 18:42:59 -04:00
DottsGit bd04135dba feat: implement explosive and homing projectile types for weapon system 2026-06-04 18:40:59 -04:00
DottsGit 09103ce366 feat: add Rocket Swarm, Mortar, and BouncingProjectile weapon types 2026-06-04 18:17:34 -04:00
DottsGit e6a2199777 feat: implement weapon system, loadout manager, and specialized projectile behaviors with associated assets 2026-06-04 18:14:50 -04:00
DottsGit c32f45a8aa feat: implement projectile system and add nail gun, plasma gun, and DMR weapons 2026-06-04 17:35:12 -04:00
DottsGit 7b7107c5d6 feat: implement PlayerMovementController to handle movement input, audio cues, and UI feedback 2026-06-04 17:22:30 -04:00
DottsGit 5ae6e20039 feat: add test_level_builder script for debugging level generation 2026-06-04 17:13:05 -04:00
DottsGit 1ccc66a601 feat: add plasma gun weapon implementation and new pause menu system with loadout management 2026-06-04 17:06:50 -04:00
DottsGit 67bd5b4302 feat: add double-barrel shotgun weapon, player movement state machine, and procedural sound assets 2026-06-04 16:40:15 -04:00
DottsGit 13ca2f46e9 feat: implement double barrel shotgun weapon with procedural sound generation 2026-06-04 13:23:31 -04:00
DottsGit a8b3619f2e feat: implement damage dummy system, floating UI numbers, and double-barrel shotgun mechanics 2026-06-04 13:15:26 -04:00
DottsGit d8cb91e593 feat: implement dash and wall run movement states for the player controller 2026-06-04 11:47:08 -04:00
DottsGit 5c2d648801 feat: implement dash movement state with velocity calculation and cooldown logic 2026-06-04 11:36:30 -04:00
DottsGit 02c6caa0d6 feat: implement pause menu with loadout management and rebindable input settings 2026-06-04 11:33:10 -04:00
DottsGit 8c928c2d92 feat: add test_level_builder script to debug tools for level creation testing 2026-06-04 10:28:26 -04:00
DottsGit a86ef53622 feat: implement modular state-based player movement system with chain mechanics 2026-06-04 10:26:18 -04:00
DottsGit cb04df1df3 feat: implement ground state movement logic for player character 2026-06-04 09:41:13 -04:00
DottsGit e65e9fa76f feat: implement wall run state logic for player movement 2026-06-04 09:36:46 -04:00
DottsGit e9112d9b3d feat: add StateDash movement state with cooldown and momentum-based velocity logic 2026-06-04 09:29:34 -04:00
DottsGit 5e9f4bec8a feat: add test_level_builder script for debugging level generation 2026-06-04 09:22:30 -04:00
DottsGit f2afd31415 feat: implement pause menu with loadout management functionality 2026-06-04 09:20:05 -04:00
DottsGit 8eea7d6e36 feat: implement loadout management system, interactive pause menu, and weapon slot switching functionality 2026-06-04 09:12:16 -04:00
DottsGit 45fb7f0f38 feat: add state machine movement classes, double barrel shotgun weapon, and test level builder utility 2026-06-04 01:21:50 -04:00
83 changed files with 3704 additions and 122 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -3
View File
@@ -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
View File
@@ -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()
@@ -316,6 +301,15 @@ func _build_player() -> void:
camera.current = true
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:
@@ -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"
+85
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://bts5u08jyv5la
+436
View File
@@ -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!")
+125
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
uid://ck4n30620akl4
+99
View File
@@ -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]
+1
View File
@@ -0,0 +1 @@
uid://b6fe1eyas7wxv
+10 -8
View File
@@ -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
+5 -1
View File
@@ -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
@@ -60,6 +60,10 @@ func _physics_process(delta: float) -> void:
jump_buffer_time = params.jump_buffer
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:
+132 -2
View File
@@ -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")
+22 -13
View File
@@ -31,20 +31,23 @@ 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:
# No input: slight air drag (very subtle)
@@ -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 ───────────────────────────────────────────────
+42 -10
View File
@@ -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
machine.switch_to("air")
if machine.player.is_on_floor():
machine.on_ground = true
machine.switch_to("ground")
else:
machine.switch_to("air")
return
# Maintain dash velocity — no gravity, no friction
machine.player.velocity = direction * _exit_speed
machine.player.move_and_slide()
if machine.player.is_on_floor():
machine.on_ground = true
machine.switch_to("ground")
# 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
vel = direction * _exit_speed
if player.is_on_floor():
vel.y = -0.5 # Snap to floor to handle ramps
player.velocity = vel
player.move_and_slide()
_last_vel = player.velocity
+51 -33
View File
@@ -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 ────────────────────────────────────────────────────
+15 -2
View File
@@ -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")
+3 -1
View File
@@ -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
+37 -8
View File
@@ -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:
@@ -35,6 +45,9 @@ func exit() -> void:
var rig = _get_camera_rig()
if rig:
rig.clear_wall_tilt()
if machine.player.wallrun_player:
machine.player.wallrun_player.stop()
func update(delta: float) -> void:
@@ -48,6 +61,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)
@@ -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()
+6
View File
@@ -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={
+30
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://b7i68343deyx
+407
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bcvjb1h4fxcct
+80
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://bddm0nooqfj2
+97
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://xxh6d1k6k2nf
+194
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://l06bdsfbd0up
+126
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://cbkr1yr1xtfum
+51
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://dcmr6nvfk267e
+118
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://dy2xsq04e6ryt
+230
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://chgdeglyrlf07
+94
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://3pfuatif484y
+22
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://df1jms63ejdnl
+42
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://d12r4xife61uv
+86
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://bf5h8cxi8kct2
+71
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://bctti6s7em04f
+85
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://b652882c1vye7
+80
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://unto0q3j6jix
+127
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://bkl7mdau5363
+108
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://n156sg54wvpp
+66
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://davfge7rifxls
+105
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://kw1ikqtc34wr
+191
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://d2dla2at16k6d
+82
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://bp5ujeumiq5j