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:
co-authored by
Claude Fable 5
parent
1358183f98
commit
6c8b0ddd28
+2
-2
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bpijupwnu2dyv
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user