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
@@ -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)
|
||||
Reference in New Issue
Block a user