feat: cel-shaded look & feel overhaul — real audio, comic UI, themed VFX

Audio (all CC0 — Kenney packs + OpenGameArt, see assets/sounds/SOURCES.md):
- Replace every procedural synth WAV with forged sounds (ffmpeg pitch/layer
  mixes): distinct fire sounds per weapon with 2-3 variations, explosions,
  footsteps x5, land/jump/dash/vault, knife swing, hit/kill confirms,
  bullet impacts, reload, flight loops, UI hover/click/confirm/error,
  match jingles, menu music
- AudioManager.stream_for(): AudioStreamRandomizer per sound id — every
  weapon now gets variation + pitch randomization on each shot
- Explosions and bullet impacts play positional audio; landing has its own
  sound instead of a pitched footstep; ambient wind bed on every map

Visuals:
- ExplosionVFX: shared cel-shaded burst (white-hot stepped core, ink
  shockwave ring, star spikes, flat smoke puffs) replaces the orange
  sphere in both local and remote-replay paths
- Comic star muzzle flashes on all weapons; unified cel tracer bolts with
  ink outlines across hitscan/shotgun/remote paths
- Impact decals: hard-stepped ink-splat gradients instead of soft airbrush
- Toon shading on first-person view models + arms, third-person weapons,
  and the procedural humanoid fallback (no hull outlines on FBX weapons —
  their hard normals tear the inverted hull)
- Fix: giant soft "blob" highlight on floors — toon rim/specular disabled
  on level-geometry materials (pre-existing artifact since the cel commit)
- Fix: own third-person weapon rendered into the first-person camera
  (shadows-only now covers first_person_mode)

UI:
- UITheme: comic theme on the root window — Bangers display font (OFL),
  paper panels with thick ink borders + hard drop shadows, papaya accent,
  themed buttons/inputs/popups; hover/click sounds on all menu buttons
- Main menu: tilted comic wordmark, sunset toon diorama, looping menu music
- Match HUD: themed scoreboard/killfeed/kill counter

Viewports:
- 4x MSAA project-wide + viewmodel/diorama viewports (crisp ink lines)
- Viewmodel camera near plane 0.01; third-person camera FOV syncs settings
- debug/visual_capture.gd: dev tool to screenshot menu + level for review

Verified: 11/11 movement tests, spawn smoke test 0 failures, before/after
screenshot comparison of menu and level.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-17 18:15:42 -04:00
co-authored by Claude Fable 5
parent 1358183f98
commit 6c8b0ddd28
94 changed files with 651 additions and 213 deletions
+2 -2
View File
@@ -35,12 +35,12 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/ak47_fire.wav")
fire_sound.stream = AudioManager.stream_for("ak47_fire", "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.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # generic reload
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
+2 -2
View File
@@ -34,13 +34,13 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/awp_fire.wav")
fire_sound.stream = AudioManager.stream_for("awp_fire", "res://assets/sounds/awp_fire.wav")
fire_sound.volume_db = 0.0
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing for now
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _process(delta: float) -> void:
+4 -30
View File
@@ -111,6 +111,7 @@ func _fire() -> void:
func _play_muzzle_flash() -> void:
if muzzle_flash:
ExplosionVFX.muzzle_flash(muzzle_flash.get_parent(), muzzle_flash.position)
muzzle_flash.light_energy = 8.0
var tween = create_tween()
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.05)
@@ -175,33 +176,6 @@ func _shoot_hitscan() -> void:
player.server_play_fire_effects.rpc_id(1, origin, final_target, name, true)
func _spawn_tracer(_origin: Vector3, final_target: Vector3) -> void:
# 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)
# Cel-styled cosmetic tracer from the barrel tip
var visual_origin = muzzle_flash.global_position if muzzle_flash else global_position
HitscanTracer.spawn_bolt(get_tree().current_scene, visual_origin, final_target)
+1
View File
@@ -109,6 +109,7 @@ func _fire() -> void:
func _play_muzzle_flash() -> void:
if muzzle_flash:
ExplosionVFX.muzzle_flash(muzzle_flash.get_parent(), muzzle_flash.position)
muzzle_flash.light_energy = 8.0
var tween = create_tween()
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.05)
+2 -2
View File
@@ -44,14 +44,14 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/dmr_fire.wav")
fire_sound.stream = AudioManager.stream_for("dmr_fire", "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.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _process(delta: float) -> void:
+5 -31
View File
@@ -44,14 +44,14 @@ func _build_model() -> void:
# Audio Players
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/shotgun_fire.wav")
fire_sound.stream = AudioManager.stream_for("shotgun_fire", "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.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
add_child(reload_sound)
func _process(delta: float) -> void:
@@ -127,6 +127,7 @@ func _apply_impulse() -> void:
func _play_muzzle_flash() -> void:
if muzzle_flash:
ExplosionVFX.muzzle_flash(muzzle_flash.get_parent(), muzzle_flash.position)
muzzle_flash.light_energy = 8.0
var tween = create_tween()
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.1)
@@ -180,33 +181,6 @@ func _shoot_hitscan() -> void:
# ImpactSpawner not available in this build, skip
pass
# 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.719)
tracer.add_child(t_mesh_inst)
tracer.target_position = final_target
# Cel-styled cosmetic tracer per pellet
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)
HitscanTracer.spawn_bolt(get_tree().current_scene, visual_origin, final_target)
+169
View File
@@ -0,0 +1,169 @@
extends Object
class_name ExplosionVFX
## Cel-shaded anime explosion, shared by local projectiles and the remote
## replay RPC. Flat unshaded colors stepped through hot stages (white core ->
## yellow -> orange -> smoke), an expanding ink shockwave ring, and radiating
## star spikes — no gradients, no realistic fire, all comic-book punch.
const CORE_STAGES := [
Color(1.0, 1.0, 0.95), # white-hot flash
Color(1.0, 0.85, 0.2), # yellow
Color(1.0, 0.45, 0.1), # orange
]
const INK := Color(0.09, 0.08, 0.1)
const SMOKE := Color(0.82, 0.8, 0.85)
static func _flat_mat(color: Color, emissive: float = 0.0) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
m.albedo_color = color
if emissive > 0.0:
m.emission_enabled = true
m.emission = color
m.emission_energy_multiplier = emissive
return m
static func spawn(scene_tree: SceneTree, pos: Vector3, radius: float) -> void:
var root := scene_tree.current_scene
if root == null:
return
# ── Core fireball: stepped color pops (each stage is a hard swap) ─────
var core := MeshInstance3D.new()
var core_mesh := SphereMesh.new()
core_mesh.radius = 1.0
core_mesh.height = 2.0
core_mesh.radial_segments = 16
core_mesh.rings = 8
core.mesh = core_mesh
var core_mat := _flat_mat(CORE_STAGES[0], 2.0)
core.material_override = core_mat
root.add_child(core)
core.global_position = pos
core.scale = Vector3.ONE * radius * 0.35
var t := core.create_tween()
t.tween_property(core, "scale", Vector3.ONE * radius * 0.9, 0.07) \
.set_trans(Tween.TRANS_QUINT).set_ease(Tween.EASE_OUT)
t.tween_callback(func(): core_mat.albedo_color = CORE_STAGES[1]; core_mat.emission = CORE_STAGES[1])
t.tween_property(core, "scale", Vector3.ONE * radius * 1.05, 0.08)
t.tween_callback(func(): core_mat.albedo_color = CORE_STAGES[2]; core_mat.emission = CORE_STAGES[2])
t.tween_property(core, "scale", Vector3.ONE * radius * 1.15, 0.08)
# collapse to nothing (anime explosions vanish, they don't fade)
t.tween_property(core, "scale", Vector3.ONE * 0.01, 0.12) \
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN)
t.tween_callback(core.queue_free)
# ── Ink shockwave ring: flat torus expanding outward ──────────────────
var ring := MeshInstance3D.new()
var ring_mesh := TorusMesh.new()
ring_mesh.inner_radius = 0.92
ring_mesh.outer_radius = 1.0
ring_mesh.rings = 32
ring_mesh.ring_segments = 6
ring.mesh = ring_mesh
ring.material_override = _flat_mat(INK)
root.add_child(ring)
ring.global_position = pos
ring.scale = Vector3.ONE * radius * 0.3
var rt := ring.create_tween()
rt.tween_property(ring, "scale", Vector3(radius * 2.2, radius * 0.5, radius * 2.2), 0.28) \
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
rt.parallel().tween_property(ring, "transparency", 1.0, 0.28) \
.set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_IN)
rt.tween_callback(ring.queue_free)
# ── Star spikes: stretched octahedra radiating out (impact flash) ─────
var spike_count := 6
for i in spike_count:
var spike := MeshInstance3D.new()
var spike_mesh := PrismMesh.new()
spike_mesh.size = Vector3(0.14, 1.0, 0.14)
spike.mesh = spike_mesh
spike.material_override = _flat_mat(Color(1.0, 0.95, 0.6), 1.5)
root.add_child(spike)
var dir := Vector3(randf_range(-1, 1), randf_range(-0.3, 1), randf_range(-1, 1)).normalized()
spike.global_position = pos + dir * radius * 0.3
var up := Vector3.UP if absf(dir.dot(Vector3.UP)) < 0.98 else Vector3.RIGHT
spike.look_at(pos + dir * 2.0, up)
spike.rotate_object_local(Vector3.RIGHT, PI / 2.0)
spike.scale = Vector3(radius * 0.25, radius * 0.9, radius * 0.25)
var st := spike.create_tween()
st.tween_property(spike, "global_position", pos + dir * radius * 1.5, 0.16) \
.set_trans(Tween.TRANS_QUART).set_ease(Tween.EASE_OUT)
st.parallel().tween_property(spike, "scale", Vector3(0.02, radius * 0.3, 0.02), 0.16)
st.tween_callback(spike.queue_free)
# ── Smoke puffs: flat gray spheres that pop out then shrink away ──────
var puff_count := 5
for i in puff_count:
var puff := MeshInstance3D.new()
var puff_mesh := SphereMesh.new()
puff_mesh.radius = 1.0
puff_mesh.height = 2.0
puff_mesh.radial_segments = 10
puff_mesh.rings = 5
puff.mesh = puff_mesh
var shade := randf_range(0.75, 0.95)
puff.material_override = _flat_mat(Color(shade, shade, shade * 1.05))
root.add_child(puff)
var dir := Vector3(randf_range(-1, 1), randf_range(0.2, 1), randf_range(-1, 1)).normalized()
puff.global_position = pos + dir * radius * 0.4
puff.scale = Vector3.ONE * radius * randf_range(0.2, 0.35)
var pt := puff.create_tween()
pt.tween_property(puff, "global_position", pos + dir * radius * randf_range(0.9, 1.3), 0.3) \
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
pt.parallel().tween_property(puff, "scale", Vector3.ONE * radius * randf_range(0.4, 0.55), 0.3)
pt.tween_property(puff, "scale", Vector3.ONE * 0.01, 0.25) \
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN)
pt.tween_callback(puff.queue_free)
# ── Audio: explosion samples with variation ───────────────────────────
var am = scene_tree.root.get_node_or_null("AudioManager")
if am and am.has_sound("explosion"):
am.play_3d("explosion", pos)
## Comic star-flash at a weapon muzzle: 4 flat spikes + hot core, parented to
## the weapon so it tracks view sway, gone in ~70 ms. Pairs with the existing
## OmniLight pop the weapons already do.
static func muzzle_flash(parent: Node3D, local_pos: Vector3) -> void:
if parent == null or not parent.is_inside_tree():
return
var flash := Node3D.new()
parent.add_child(flash)
flash.position = local_pos
flash.rotation.z = randf() * TAU
var core := MeshInstance3D.new()
var core_mesh := SphereMesh.new()
core_mesh.radius = 0.035
core_mesh.height = 0.07
core_mesh.radial_segments = 8
core_mesh.rings = 4
core.mesh = core_mesh
core.material_override = _flat_mat(Color(1.0, 0.98, 0.85), 3.0)
flash.add_child(core)
for i in 4:
var spike := MeshInstance3D.new()
var spike_mesh := PrismMesh.new()
spike_mesh.size = Vector3(0.025, 0.11, 0.012)
spike.mesh = spike_mesh
spike.material_override = _flat_mat(Color(1.0, 0.82, 0.25), 2.0)
spike.position = Vector3(0, 0, 0)
spike.rotation.z = TAU * float(i) / 4.0 + randf_range(-0.2, 0.2)
spike.translate_object_local(Vector3(0, 0.07, 0))
flash.add_child(spike)
var t := flash.create_tween()
t.tween_property(flash, "scale", Vector3.ONE * 1.4, 0.04) \
.set_trans(Tween.TRANS_QUINT).set_ease(Tween.EASE_OUT)
t.tween_property(flash, "scale", Vector3.ONE * 0.01, 0.03)
t.tween_callback(flash.queue_free)
+1
View File
@@ -0,0 +1 @@
uid://bpijupwnu2dyv
+2 -32
View File
@@ -210,35 +210,5 @@ func _explode(pos: Vector3) -> void:
if owner_player and owner_player.is_multiplayer_authority() and owner_player.has_method("server_play_explosion") and owner_player.multiplayer.has_multiplayer_peer() and not(owner_player.multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
owner_player.server_play_explosion.rpc_id(1, pos, explosion_radius)
# Local 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)
# Audio Effect
var audio = AudioStreamPlayer3D.new()
audio.stream = load("res://assets/sounds/shotgun_fire.wav") # using shotgun fire for a crash explosion
audio.bus = "SFX"
audio.pitch_scale = explosion_pitch
audio.max_distance = 150.0
get_tree().current_scene.add_child(audio)
audio.global_position = pos
audio.play()
audio.finished.connect(audio.queue_free)
# Local visual + audio: shared cel-shaded burst (also used by remote replay)
ExplosionVFX.spawn(get_tree(), pos, explosion_radius)
+35
View File
@@ -6,6 +6,41 @@ var target_position: Vector3
var distance_traveled: float = 0.0
var max_distance: float = 0.0
## Spawn a cel-styled tracer bolt: unshaded hot core with an ink outline,
## flying from origin to target. Shared by every hitscan weapon and the
## remote-fire replay so all bullets look on-theme.
static func spawn_bolt(scene: Node, origin: Vector3, target: Vector3,
color: Color = Color(1.0, 0.72, 0.15)) -> void:
if scene == null or origin.distance_squared_to(target) <= 0.1:
return
var tracer := HitscanTracer.new()
var core := MeshInstance3D.new()
var core_mesh := CapsuleMesh.new()
core_mesh.radius = 0.025
core_mesh.height = 1.1
var m := StandardMaterial3D.new()
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
m.albedo_color = color.lightened(0.35)
m.emission_enabled = true
m.emission = color
m.emission_energy_multiplier = 2.2
core_mesh.material = m
core.mesh = core_mesh
core.rotation.x = deg_to_rad(90) # orient capsule along Z
core.position = Vector3(0, 0, -0.55) # tail sits at the origin
core.material_overlay = LevelMaterials.outline(0.012)
tracer.add_child(core)
tracer.position = origin
tracer.target_position = target
scene.add_child(tracer)
var up := Vector3.UP
if absf((target - origin).normalized().dot(up)) > 0.99:
up = Vector3.RIGHT
tracer.look_at(target, up)
func _ready() -> void:
max_distance = global_position.distance_to(target_position)
if max_distance < 0.1:
+9 -1
View File
@@ -19,7 +19,9 @@ static func _ensure_mats():
for c in colors: p_colors.append(c)
g.offsets = p_offsets
g.colors = p_colors
# Hard color steps: reads as inked cel splats instead of soft airbrush
g.interpolation_mode = Gradient.GRADIENT_INTERPOLATE_CONSTANT
var t = GradientTexture2D.new()
t.gradient = g
t.fill = GradientTexture2D.FILL_RADIAL
@@ -62,6 +64,12 @@ static func _ensure_mats():
static func spawn(scene_tree: SceneTree, type: String, pos: Vector3, normal: Vector3, size: float = 0.2):
_ensure_mats()
# Positional impact tick (pooled; safe at fire-rate frequency)
if type == "bullet":
var am = scene_tree.root.get_node_or_null("AudioManager")
if am and am.has_sound("bullet_impact"):
am.play_3d("bullet_impact", pos, -6.0)
var mi = MeshInstance3D.new()
var qm = QuadMesh.new()
+1 -1
View File
@@ -44,7 +44,7 @@ func _build_model() -> void:
# Audio (reusing shotgun reload or something generic for now, ideally a swish)
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/dash.wav")
fire_sound.stream = AudioManager.stream_for("knife_swing", "res://assets/sounds/knife_swing.wav")
fire_sound.pitch_scale = 1.2
fire_sound.volume_db = 0.0
model_root.add_child(fire_sound)
+2 -2
View File
@@ -35,12 +35,12 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/m4_fire.wav")
fire_sound.stream = AudioManager.stream_for("m4_fire", "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.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
+2 -2
View File
@@ -36,12 +36,12 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/mortar_fire.wav")
fire_sound.stream = AudioManager.stream_for("mortar_fire", "res://assets/sounds/mortar_fire.wav")
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
+2 -2
View File
@@ -35,12 +35,12 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/mp7_fire.wav")
fire_sound.stream = AudioManager.stream_for("mp7_fire", "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.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
+2 -2
View File
@@ -51,14 +51,14 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/nailgun_fire.wav")
fire_sound.stream = AudioManager.stream_for("nailgun_fire", "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.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
+2 -2
View File
@@ -45,14 +45,14 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/plasma_fire.wav")
fire_sound.stream = AudioManager.stream_for("plasma_fire", "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.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
+2 -2
View File
@@ -38,12 +38,12 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/rocket_fire.wav")
fire_sound.stream = AudioManager.stream_for("rocket_fire", "res://assets/sounds/rocket_fire.wav")
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
+2 -2
View File
@@ -54,12 +54,12 @@ func _build_model() -> void:
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.bus = "SFX"
fire_sound.stream = load("res://assets/sounds/swarm_fire.wav")
fire_sound.stream = AudioManager.stream_for("swarm_fire", "res://assets/sounds/swarm_fire.wav")
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.bus = "SFX"
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _process(delta: float) -> void:
+10 -3
View File
@@ -41,11 +41,13 @@ func _setup_viewmodel_viewport() -> void:
sub_viewport.transparent_bg = true
# Shared world so it renders main scene's objects that have layer 20
# (In Godot 4, own_world_3d is false by default, but let's be explicit)
sub_viewport.own_world_3d = false
sub_viewport.own_world_3d = false
sub_viewport.msaa_3d = Viewport.MSAA_4X # keep ink outlines crisp on the viewmodel
sv_container.add_child(sub_viewport)
vm_camera = Camera3D.new()
vm_camera.cull_mask = 1 << 19 # Only see layer 20
vm_camera.near = 0.01 # viewmodel sits close to the lens; avoid clipping
sub_viewport.add_child(vm_camera)
# Add a light specifically for the viewmodel in case main lights don't reach
@@ -231,7 +233,12 @@ func _spawn_weapon(slot: int, weapon_id: String) -> void:
w.set_meta("script_path", script_path)
_add_procedural_arms(w)
# Cel-shade the view model + arms so the first-person weapon matches
# the world's look. No inverted-hull outline: FBX weapon models have
# hard-edged normals that make the hull tear into scratchy artifacts.
LevelMaterials.apply_toon_recursive(w, 0.0)
# Set weapons to viewmodel layer
_set_layer_recursive(w, 1 << 19)