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,16 @@
|
||||
# Sound sources
|
||||
|
||||
All current WAVs are forged (pitch/layer/trim via ffmpeg — see repo history)
|
||||
from these CC0 packs, no attribution required:
|
||||
|
||||
- Kenney Impact Sounds — https://kenney.nl/assets/impact-sounds (CC0)
|
||||
- Kenney Sci-Fi Sounds — https://kenney.nl/assets/sci-fi-sounds (CC0)
|
||||
- Kenney Interface Sounds — https://kenney.nl/assets/interface-sounds (CC0)
|
||||
- Kenney Music Jingles — https://kenney.nl/assets/music-jingles (CC0)
|
||||
|
||||
Exceptions (still procedural from `generate_sounds.py`): `wind.wav`,
|
||||
`wallrun.wav`, `slide.wav` — seamless noise loops that the Kenney one-shots
|
||||
can't replace.
|
||||
|
||||
Menu music: `music_menu.wav` — "Menu Music" by mrpoly, CC0,
|
||||
https://opengameart.org/content/menu-music
|
||||
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.
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.
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.
@@ -0,0 +1,93 @@
|
||||
Copyright 2010 The Bangers Project Authors (https://github.com/googlefonts/bangers)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -163,6 +163,11 @@ func _ready() -> void:
|
||||
calf_r.cast_shadow = shadow_setting
|
||||
calf_r_pivot.add_child(calf_r)
|
||||
|
||||
# Cel-shade + ink outline the blocky fallback body so it matches skinned
|
||||
# player models (shadows-only bodies skip it: outlines would cast shadows)
|
||||
if not shadows_only:
|
||||
LevelMaterials.apply_toon_recursive(root_pivot, 0.012)
|
||||
|
||||
## Skin System
|
||||
var current_skin: PlayerSkin
|
||||
var skin_model: Node3D
|
||||
@@ -222,6 +227,9 @@ func _apply_color(col: Color) -> void:
|
||||
hmat.albedo_color = Color(0.95, 0.85, 0.78)
|
||||
hmat.roughness = 0.8
|
||||
head.mesh.material = hmat
|
||||
# Re-toon after the material swap (fresh StandardMaterials above)
|
||||
if not shadows_only:
|
||||
LevelMaterials.apply_toon_recursive(root_pivot, 0.012)
|
||||
|
||||
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
||||
current_state = state
|
||||
@@ -257,7 +265,10 @@ func set_weapon(script_path: String) -> void:
|
||||
w.set_process_input(false)
|
||||
if shadows_only:
|
||||
_set_shadows_recursive(w)
|
||||
|
||||
else:
|
||||
# Toon shading only — FBX weapon normals tear inverted-hull outlines
|
||||
LevelMaterials.apply_toon_recursive(w, 0.0)
|
||||
|
||||
# Force position after the weapon's _build_model() sets it for 1st person
|
||||
w.position = Vector3(-0.15, 1.0, 0.4)
|
||||
w.rotation_degrees = Vector3(0, 180, 0)
|
||||
|
||||
@@ -353,7 +353,10 @@ func set_weapon(script_path: String) -> void:
|
||||
w.ready.connect(func():
|
||||
w.set_process(false)
|
||||
w.set_process_input(false)
|
||||
if shadows_only:
|
||||
# Owner's first-person view must not see their own held weapon (it
|
||||
# sits right in front of the lens as a huge blob) — shadows only,
|
||||
# same as the body. set_owner_visible reveals it in third person.
|
||||
if shadows_only or first_person_mode:
|
||||
_set_shadows_recursive(w)
|
||||
# Undo the first-person viewmodel placement from the weapon's _ready.
|
||||
w.position = Vector3(0.0, 0.08, 0.03)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
extends SceneTree
|
||||
|
||||
## Dev tool: launch with rendering, screenshot the main menu and a level,
|
||||
## then quit. Run:
|
||||
## godot --path . -s res://debug/visual_capture.gd -- <output_dir>
|
||||
## Screenshots land in <output_dir>/shot_menu.png and shot_level.png.
|
||||
|
||||
var _frames := 0
|
||||
var _out_dir := "."
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.size() > 0:
|
||||
_out_dir = args[0]
|
||||
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
_frames += 1
|
||||
if _frames == 60:
|
||||
_shot("menu")
|
||||
var nm = root.get_node_or_null("NetworkManager")
|
||||
if nm and nm.has_method("start_singleplayer_match"):
|
||||
nm.start_singleplayer_match("Deathmatch")
|
||||
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
|
||||
elif _frames == 240:
|
||||
_shot("level")
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _shot(tag: String) -> void:
|
||||
var img := root.get_viewport().get_texture().get_image()
|
||||
var path := _out_dir + "/shot_" + tag + ".png"
|
||||
img.save_png(path)
|
||||
print("visual_capture: saved ", path)
|
||||
@@ -124,6 +124,21 @@ func has_sound(id: String) -> bool:
|
||||
return _library.has(id)
|
||||
|
||||
|
||||
## Build an AudioStreamRandomizer for a registered id: every play() picks a
|
||||
## random variation with random pitch. For nodes that own their own player
|
||||
## (weapons) instead of going through the pools. Falls back to a plain load
|
||||
## of fallback_path if the id isn't registered.
|
||||
func stream_for(id: String, fallback_path: String = "") -> AudioStream:
|
||||
var entry = _library.get(id)
|
||||
if entry == null:
|
||||
return load(fallback_path) if fallback_path != "" and ResourceLoader.exists(fallback_path) else null
|
||||
var r := AudioStreamRandomizer.new()
|
||||
r.random_pitch = 1.0 + entry["pitch_var"]
|
||||
for s in entry["streams"]:
|
||||
r.add_stream(-1, s)
|
||||
return r
|
||||
|
||||
|
||||
# ── Playback ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_pools() -> void:
|
||||
|
||||
@@ -36,6 +36,8 @@ var death_screen: Control
|
||||
var ragdoll_instance: Node3D
|
||||
# Audio
|
||||
var footstep_player: AudioStreamPlayer
|
||||
var footstep_streams: Array = []
|
||||
var land_player: AudioStreamPlayer
|
||||
var dash_player: AudioStreamPlayer
|
||||
var recent_attackers: Dictionary = {} # attacker_id: timestamp
|
||||
var hit_player: AudioStreamPlayer
|
||||
@@ -182,6 +184,7 @@ func set_third_person(on: bool) -> void:
|
||||
return
|
||||
third_person = on
|
||||
if is_instance_valid(_tp_camera):
|
||||
_tp_camera.fov = SettingsManager.world_fov # stay in sync with settings
|
||||
_tp_camera.current = on
|
||||
if is_instance_valid(camera):
|
||||
camera.current = not on
|
||||
@@ -250,8 +253,18 @@ func _hide_remote_weapons() -> void:
|
||||
func _setup_audio() -> void:
|
||||
footstep_player = AudioStreamPlayer.new()
|
||||
footstep_player.bus = "SFX"
|
||||
footstep_player.stream = load("res://assets/sounds/footstep.wav")
|
||||
for path in ["res://assets/sounds/footstep.wav", "res://assets/sounds/footstep_01.wav",
|
||||
"res://assets/sounds/footstep_02.wav", "res://assets/sounds/footstep_03.wav",
|
||||
"res://assets/sounds/footstep_04.wav"]:
|
||||
if ResourceLoader.exists(path):
|
||||
footstep_streams.append(load(path))
|
||||
footstep_player.stream = footstep_streams[0] if not footstep_streams.is_empty() else null
|
||||
add_child(footstep_player)
|
||||
|
||||
land_player = AudioStreamPlayer.new()
|
||||
land_player.bus = "SFX"
|
||||
land_player.stream = load("res://assets/sounds/land.wav")
|
||||
add_child(land_player)
|
||||
|
||||
jump_player = AudioStreamPlayer.new()
|
||||
jump_player.bus = "SFX"
|
||||
@@ -492,34 +505,7 @@ func rpc_play_fire_effects(origin: Vector3, target_or_dir: Vector3, _weapon_name
|
||||
_spawn_remote_projectile(origin, target_or_dir, _weapon_name)
|
||||
|
||||
func _spawn_remote_tracer(origin: Vector3, target: Vector3) -> void:
|
||||
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 = target
|
||||
|
||||
if origin.distance_squared_to(target) > 0.1:
|
||||
get_tree().current_scene.add_child(tracer)
|
||||
tracer.global_position = origin
|
||||
|
||||
var up_vec = Vector3.UP
|
||||
if abs((target - origin).normalized().dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
tracer.look_at(target, up_vec)
|
||||
HitscanTracer.spawn_bolt(get_tree().current_scene, origin, target)
|
||||
|
||||
func _spawn_remote_projectile(origin: Vector3, fire_dir: Vector3, weapon_name: String) -> void:
|
||||
# Try to find the actual weapon to spawn its custom projectile so it looks identical!
|
||||
@@ -620,36 +606,7 @@ func server_play_explosion(pos: Vector3, radius: float) -> void:
|
||||
func rpc_play_explosion(pos: Vector3, radius: float) -> void:
|
||||
if is_multiplayer_authority(): return
|
||||
|
||||
var visual = MeshInstance3D.new()
|
||||
var s_mesh = SphereMesh.new()
|
||||
s_mesh.radius = radius
|
||||
s_mesh.height = 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)
|
||||
|
||||
var audio = AudioStreamPlayer3D.new()
|
||||
audio.stream = load("res://assets/sounds/shotgun_fire.wav")
|
||||
audio.bus = "SFX"
|
||||
audio.pitch_scale = 0.7
|
||||
audio.max_distance = 150.0
|
||||
get_tree().current_scene.add_child(audio)
|
||||
audio.global_position = pos
|
||||
audio.play()
|
||||
audio.finished.connect(audio.queue_free)
|
||||
ExplosionVFX.spawn(get_tree(), pos, radius)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func server_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_name: String, impulse: Vector3) -> void:
|
||||
@@ -973,13 +930,11 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
elif ev == "dash":
|
||||
_speedline_burst = 1.0
|
||||
elif ev == "land":
|
||||
# Landing thud: reuse the footstep sample, pitched down and louder
|
||||
# with impact. Ground state resets pitch/volume before each step.
|
||||
if footstep_player:
|
||||
if land_player:
|
||||
var heavy: bool = data.get("heavy", false)
|
||||
footstep_player.pitch_scale = 0.55 if heavy else 0.7
|
||||
footstep_player.volume_db = 2.0 if heavy else -2.0
|
||||
footstep_player.play()
|
||||
land_player.pitch_scale = (0.8 if heavy else 1.0) * randf_range(0.95, 1.05)
|
||||
land_player.volume_db = 2.0 if heavy else -3.0
|
||||
land_player.play()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Remote players: interpolate toward the owner's synced transform and
|
||||
|
||||
@@ -82,8 +82,9 @@ func update(delta: float) -> void:
|
||||
_footstep_timer -= delta
|
||||
if _footstep_timer <= 0.0:
|
||||
if player.footstep_player:
|
||||
# Reset pitch/volume (the landing thud reuses this player) and
|
||||
# add slight variation so steps don't machine-gun.
|
||||
# Random sample + slight pitch variation so steps don't machine-gun.
|
||||
if "footstep_streams" in player and not player.footstep_streams.is_empty():
|
||||
player.footstep_player.stream = player.footstep_streams.pick_random()
|
||||
player.footstep_player.pitch_scale = randf_range(0.92, 1.08)
|
||||
player.footstep_player.volume_db = -6.0
|
||||
player.footstep_player.play()
|
||||
|
||||
@@ -126,3 +126,4 @@ emote={
|
||||
[rendering]
|
||||
|
||||
rendering_device/driver.windows="d3d12"
|
||||
anti_aliasing/quality/msaa_3d=2
|
||||
|
||||
@@ -33,6 +33,20 @@ static func add_to(level: Node, sky_variant: String = "day") -> WorldEnvironment
|
||||
fill.light_energy = 0.25
|
||||
fill.shadow_enabled = false
|
||||
level.add_child(fill)
|
||||
|
||||
# Quiet ambient bed so the arena never sits in dead air.
|
||||
if not level.has_node("AmbientBed") and ResourceLoader.exists("res://assets/sounds/wind.wav"):
|
||||
var amb := AudioStreamPlayer.new()
|
||||
amb.name = "AmbientBed"
|
||||
amb.stream = load("res://assets/sounds/wind.wav")
|
||||
if amb.stream is AudioStreamWAV:
|
||||
amb.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
amb.stream.loop_end = amb.stream.data.size() / 2
|
||||
amb.volume_db = -30.0
|
||||
amb.pitch_scale = 0.6
|
||||
amb.bus = "SFX"
|
||||
amb.autoplay = true
|
||||
level.add_child(amb)
|
||||
return env
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,11 @@ static func tinted(tint: Color, dark: bool = false) -> Material:
|
||||
# land near the original flat color while keeping the grid contrast.
|
||||
mat.set_shader_parameter("albedo_color", Color(
|
||||
minf(tint.r * 1.9, 1.0), minf(tint.g * 1.9, 1.0), minf(tint.b * 1.9, 1.0)))
|
||||
# Level surfaces are huge; the toon rim/specular reads as a giant soft
|
||||
# "blob" highlight smeared across floors and walls. Keep those effects
|
||||
# for characters/props only.
|
||||
mat.set_shader_parameter("rim_strength", 0.0)
|
||||
mat.set_shader_parameter("specular_strength", 0.0)
|
||||
_cache[key] = mat
|
||||
return mat
|
||||
|
||||
|
||||
@@ -18,7 +18,12 @@ var _debug_ui_panel: PanelContainer
|
||||
func _ready() -> void:
|
||||
# Hide mouse
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
|
||||
# Maps that don't ship their own environment get the shared stylized one
|
||||
# (sky gradient, sun+fill, glow, cel color grade, ambient bed).
|
||||
if not has_node("WorldEnvironment"):
|
||||
LevelEnvironment.add_to(self)
|
||||
|
||||
_build_hud()
|
||||
|
||||
# Multiplayer Spawning
|
||||
|
||||
+38
-19
@@ -9,7 +9,23 @@ var _host_lobby_players_list: ItemList
|
||||
|
||||
func _ready() -> void:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
|
||||
|
||||
# Comic/cel theme for every Control in the game (root window inherits down)
|
||||
UITheme.apply_global(get_tree())
|
||||
|
||||
# Menu music (scene-owned: stops automatically when a level loads)
|
||||
var music_path := "res://assets/sounds/music_menu.wav"
|
||||
if ResourceLoader.exists(music_path):
|
||||
var music := AudioStreamPlayer.new()
|
||||
music.stream = load(music_path)
|
||||
if music.stream is AudioStreamWAV:
|
||||
music.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
music.stream.loop_end = music.stream.data.size() / 4 # 16-bit stereo frames
|
||||
music.bus = "Music"
|
||||
music.volume_db = -8.0
|
||||
music.autoplay = true
|
||||
add_child(music)
|
||||
|
||||
var nm = get_node_or_null("/root/NetworkManager")
|
||||
if nm:
|
||||
nm.connection_succeeded.connect(_on_connection_succeeded)
|
||||
@@ -25,6 +41,7 @@ func _ready() -> void:
|
||||
|
||||
var viewport = SubViewport.new()
|
||||
viewport.own_world_3d = true
|
||||
viewport.msaa_3d = Viewport.MSAA_4X
|
||||
right_panel.add_child(viewport)
|
||||
|
||||
_build_3d_diorama(viewport)
|
||||
@@ -40,13 +57,14 @@ func _ready() -> void:
|
||||
_ui_vbox.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50)
|
||||
add_child(_ui_vbox)
|
||||
|
||||
# Title
|
||||
# Title — big comic wordmark with a slight kinetic tilt
|
||||
var title = Label.new()
|
||||
title.text = "Papaya Shooter"
|
||||
title.add_theme_font_size_override("font_size", 72)
|
||||
title.add_theme_color_override("font_color", Color(1, 0.6, 0.2)) # Papaya orange
|
||||
title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||
title.add_theme_constant_override("outline_size", 12)
|
||||
title.text = "PAPAYA SHOOTER"
|
||||
title.add_theme_font_size_override("font_size", 92)
|
||||
title.add_theme_color_override("font_color", UITheme.PAPAYA)
|
||||
title.add_theme_color_override("font_outline_color", UITheme.INK)
|
||||
title.add_theme_constant_override("outline_size", 16)
|
||||
title.rotation_degrees = -2.5
|
||||
_ui_vbox.add_child(title)
|
||||
|
||||
# Spacer
|
||||
@@ -269,6 +287,9 @@ func _ready() -> void:
|
||||
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
|
||||
hl_right.add_child(_host_lobby_players_list)
|
||||
|
||||
# Hover/click sounds on every menu button
|
||||
UITheme.wire_sounds(self)
|
||||
|
||||
func _load_maps_modularly(target_hbox: Container, is_multiplayer: bool, card_size: Vector2) -> void:
|
||||
var maps_dir = "res://scenes/maps/"
|
||||
var dir = DirAccess.open(maps_dir)
|
||||
@@ -492,13 +513,9 @@ func _build_3d_diorama(viewport: SubViewport) -> void:
|
||||
var world = Node3D.new()
|
||||
viewport.add_child(world)
|
||||
|
||||
# Lighting and Env
|
||||
# Lighting and Env — same stylized sunset sky as in-game for continuity
|
||||
var env = WorldEnvironment.new()
|
||||
var environment = Environment.new()
|
||||
environment.background_mode = Environment.BG_COLOR
|
||||
environment.background_color = Color(0.1, 0.15, 0.2) # Dark stylish background
|
||||
environment.tonemap_mode = Environment.TONE_MAPPER_ACES
|
||||
env.environment = environment
|
||||
env.environment = LevelEnvironment.make_environment("sunset")
|
||||
world.add_child(env)
|
||||
|
||||
var sun = DirectionalLight3D.new()
|
||||
@@ -514,22 +531,21 @@ func _build_3d_diorama(viewport: SubViewport) -> void:
|
||||
fill.light_energy = 0.5
|
||||
world.add_child(fill)
|
||||
|
||||
# Floor
|
||||
# Floor — toon world-grid like the levels
|
||||
var floor_mesh = MeshInstance3D.new()
|
||||
floor_mesh.mesh = PlaneMesh.new()
|
||||
floor_mesh.mesh.size = Vector2(20, 20)
|
||||
var floor_mat = StandardMaterial3D.new()
|
||||
floor_mat.albedo_color = Color(0.2, 0.2, 0.2)
|
||||
floor_mesh.mesh.surface_set_material(0, floor_mat)
|
||||
floor_mesh.mesh.surface_set_material(0, LevelMaterials.tinted(Color(0.32, 0.3, 0.38)))
|
||||
world.add_child(floor_mesh)
|
||||
|
||||
|
||||
# Player Model (Sliding Pose)
|
||||
var player_model = Node3D.new()
|
||||
player_model.position = Vector3(0, 0.5, 0) # Lowered for slide
|
||||
world.add_child(player_model)
|
||||
|
||||
|
||||
var body_mat = StandardMaterial3D.new()
|
||||
body_mat.albedo_color = Color(0.3, 0.6, 0.9)
|
||||
# Toonified below via apply_toon_recursive once the model is assembled
|
||||
|
||||
# Torso (Capsule tilted back)
|
||||
var torso = MeshInstance3D.new()
|
||||
@@ -576,6 +592,9 @@ func _build_3d_diorama(viewport: SubViewport) -> void:
|
||||
if shotgun.has_method("set_process_input"):
|
||||
shotgun.set_process_input(false)
|
||||
|
||||
# Cel-shade the diorama (no hull outlines — the FBX shotgun tears them)
|
||||
LevelMaterials.apply_toon_recursive(player_model, 0.0)
|
||||
|
||||
# Camera
|
||||
var camera = Camera3D.new()
|
||||
world.add_child(camera)
|
||||
|
||||
+7
-6
@@ -16,7 +16,10 @@ var _scoreboard_grid: GridContainer
|
||||
|
||||
func _ready() -> void:
|
||||
_nm = get_node_or_null("/root/NetworkManager")
|
||||
|
||||
# Ensure the comic theme is active even when a level is loaded directly
|
||||
# (tests, debug runs) without passing through the main menu.
|
||||
UITheme.apply_global(get_tree())
|
||||
|
||||
# Top HUD
|
||||
var top_hbox = HBoxContainer.new()
|
||||
top_hbox.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||
@@ -37,8 +40,8 @@ func _ready() -> void:
|
||||
_local_kills_label = Label.new()
|
||||
_local_kills_label.text = "0 Kills"
|
||||
_local_kills_label.add_theme_font_size_override("font_size", 64)
|
||||
_local_kills_label.add_theme_color_override("font_color", Color(1, 0.8, 0.2))
|
||||
_local_kills_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||
_local_kills_label.add_theme_color_override("font_color", UITheme.PAPAYA)
|
||||
_local_kills_label.add_theme_color_override("font_outline_color", UITheme.INK)
|
||||
_local_kills_label.add_theme_constant_override("outline_size", 12)
|
||||
top_hbox.add_child(_local_kills_label)
|
||||
|
||||
@@ -68,9 +71,7 @@ func _ready() -> void:
|
||||
|
||||
_scoreboard_panel = PanelContainer.new()
|
||||
_scoreboard_panel.custom_minimum_size = Vector2(800, 600)
|
||||
var sb_style = StyleBoxFlat.new()
|
||||
sb_style.bg_color = Color(0, 0, 0, 0.8)
|
||||
_scoreboard_panel.add_theme_stylebox_override("panel", sb_style)
|
||||
# Styled by the shared UITheme PanelContainer stylebox (ink-border card)
|
||||
_scoreboard_panel.hide()
|
||||
center_container.add_child(_scoreboard_panel)
|
||||
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
extends Object
|
||||
class_name UITheme
|
||||
|
||||
## Shared comic/cel UI theme: Bangers display font, paper panels with thick
|
||||
## ink borders and hard drop shadows, papaya accent. Built once, applied to
|
||||
## the root Window so every Control in the game inherits it.
|
||||
|
||||
const FONT_PATH := "res://assets/ui/fonts/Bangers-Regular.ttf"
|
||||
|
||||
const INK := Color(0.09, 0.08, 0.12)
|
||||
const PAPER := Color(0.98, 0.96, 0.9)
|
||||
const PAPER_DIM := Color(0.92, 0.9, 0.84)
|
||||
const PAPAYA := Color(1.0, 0.55, 0.15)
|
||||
const PAPAYA_HOT := Color(1.0, 0.68, 0.25)
|
||||
const TEAL := Color(0.16, 0.72, 0.68)
|
||||
|
||||
static var _theme: Theme = null
|
||||
|
||||
|
||||
## Comic panel stylebox: flat fill, thick ink border, hard offset shadow.
|
||||
static func box(bg: Color, border: Color = INK, radius: int = 10,
|
||||
border_w: int = 3, shadow: bool = true) -> StyleBoxFlat:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = bg
|
||||
sb.border_color = border
|
||||
sb.set_border_width_all(border_w)
|
||||
sb.set_corner_radius_all(radius)
|
||||
sb.content_margin_left = 16
|
||||
sb.content_margin_right = 16
|
||||
sb.content_margin_top = 8
|
||||
sb.content_margin_bottom = 8
|
||||
if shadow:
|
||||
sb.shadow_color = Color(INK.r, INK.g, INK.b, 0.55)
|
||||
sb.shadow_size = 0
|
||||
sb.shadow_offset = Vector2(4, 4)
|
||||
sb.shadow_size = 2
|
||||
return sb
|
||||
|
||||
|
||||
static func build() -> Theme:
|
||||
if _theme:
|
||||
return _theme
|
||||
var t := Theme.new()
|
||||
var font: Font = load(FONT_PATH) if ResourceLoader.exists(FONT_PATH) else null
|
||||
if font:
|
||||
t.default_font = font
|
||||
t.default_font_size = 26
|
||||
|
||||
# ── Buttons: paper card, papaya on hover, squash on press ────────────
|
||||
t.set_stylebox("normal", "Button", box(PAPER))
|
||||
t.set_stylebox("hover", "Button", box(PAPAYA_HOT))
|
||||
t.set_stylebox("pressed", "Button", box(PAPAYA, INK, 10, 3, false))
|
||||
t.set_stylebox("focus", "Button", box(PAPER, TEAL))
|
||||
t.set_stylebox("disabled", "Button", box(PAPER_DIM, Color(0.4, 0.4, 0.45)))
|
||||
t.set_color("font_color", "Button", INK)
|
||||
t.set_color("font_hover_color", "Button", INK)
|
||||
t.set_color("font_pressed_color", "Button", PAPER)
|
||||
t.set_color("font_focus_color", "Button", INK)
|
||||
t.set_color("font_disabled_color", "Button", Color(0.45, 0.45, 0.5))
|
||||
|
||||
# OptionButton / CheckBox get the same treatment explicitly (Godot theme
|
||||
# types don't inherit styling from Button)
|
||||
for cls in ["OptionButton", "MenuButton", "CheckBox", "CheckButton"]:
|
||||
t.set_stylebox("normal", cls, box(PAPER))
|
||||
t.set_stylebox("hover", cls, box(PAPAYA_HOT))
|
||||
t.set_stylebox("pressed", cls, box(PAPAYA, INK, 10, 3, false))
|
||||
t.set_stylebox("focus", cls, box(PAPER, TEAL))
|
||||
t.set_stylebox("disabled", cls, box(PAPER_DIM, Color(0.4, 0.4, 0.45)))
|
||||
t.set_color("font_color", cls, INK)
|
||||
t.set_color("font_hover_color", cls, INK)
|
||||
t.set_color("font_pressed_color", cls, PAPER)
|
||||
t.set_color("font_focus_color", cls, INK)
|
||||
t.set_color("font_disabled_color", cls, Color(0.45, 0.45, 0.5))
|
||||
|
||||
# ── Labels: ink outline everywhere for that inked-cel readability ────
|
||||
t.set_color("font_outline_color", "Label", INK)
|
||||
t.set_constant("outline_size", "Label", 8)
|
||||
t.set_color("font_color", "Label", PAPER)
|
||||
|
||||
# RichTextLabel (killfeed)
|
||||
t.set_color("default_color", "RichTextLabel", PAPER)
|
||||
t.set_color("font_outline_color", "RichTextLabel", INK)
|
||||
t.set_constant("outline_size", "RichTextLabel", 6)
|
||||
|
||||
# ── Inputs ───────────────────────────────────────────────────────────
|
||||
t.set_stylebox("normal", "LineEdit", box(PAPER, INK, 8, 3, false))
|
||||
t.set_stylebox("focus", "LineEdit", box(PAPER, TEAL, 8, 3, false))
|
||||
t.set_color("font_color", "LineEdit", INK)
|
||||
t.set_color("caret_color", "LineEdit", INK)
|
||||
|
||||
# ── Panels / lists ───────────────────────────────────────────────────
|
||||
t.set_stylebox("panel", "PanelContainer", box(Color(0.13, 0.12, 0.18, 0.92), INK, 14, 4))
|
||||
t.set_stylebox("panel", "ItemList", box(Color(0.13, 0.12, 0.18, 0.92), INK, 10, 3, false))
|
||||
t.set_color("font_color", "ItemList", PAPER)
|
||||
|
||||
# ── Popup menus (OptionButton dropdowns) ─────────────────────────────
|
||||
t.set_stylebox("panel", "PopupMenu", box(PAPER, INK, 8, 3, false))
|
||||
t.set_color("font_color", "PopupMenu", INK)
|
||||
t.set_color("font_hover_color", "PopupMenu", PAPAYA)
|
||||
|
||||
_theme = t
|
||||
return t
|
||||
|
||||
|
||||
## Apply the theme to the whole root window (inherited by every Control in
|
||||
## every scene from then on). Idempotent.
|
||||
static func apply_global(tree: SceneTree) -> void:
|
||||
if tree and tree.root and tree.root.theme != build():
|
||||
tree.root.theme = build()
|
||||
|
||||
|
||||
## Attach hover/click UI sounds to every BaseButton under `root` (recursive).
|
||||
static func wire_sounds(root: Node) -> void:
|
||||
var am = root.get_tree().root.get_node_or_null("AudioManager")
|
||||
if am == null:
|
||||
return
|
||||
for btn in root.find_children("*", "BaseButton", true, false):
|
||||
if btn.has_meta("ui_sfx_wired"):
|
||||
continue
|
||||
btn.set_meta("ui_sfx_wired", true)
|
||||
btn.mouse_entered.connect(func(): am.play_ui("ui_hover"))
|
||||
btn.pressed.connect(func(): am.play_ui("ui_click"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://doqqa8s8bal5u
|
||||
+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