Compare commits
2
Commits
2c7b5a1aca
...
5bcbcfdc3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bcbcfdc3f | ||
|
|
37c15bed1a |
@@ -0,0 +1,38 @@
|
||||
shader_type canvas_item;
|
||||
|
||||
// Anime-style radial speed lines. Fullscreen ColorRect overlay; `intensity`
|
||||
// (0..1) is driven by the player controller from horizontal speed + dash.
|
||||
// Lines live at the screen edges and jitter inward, center stays clear.
|
||||
|
||||
uniform float intensity : hint_range(0.0, 1.0) = 0.0;
|
||||
uniform vec4 line_color : source_color = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
|
||||
float hash(float n) {
|
||||
return fract(sin(n * 4310.17) * 43758.5453);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 uv = UV - vec2(0.5);
|
||||
uv.x *= 1.6; // widescreen: keep the clear zone round-ish
|
||||
float dist = length(uv);
|
||||
float ang = atan(uv.y, uv.x);
|
||||
|
||||
// Quantize the circle into spokes; each spoke gets a stable random phase.
|
||||
float spokes = 90.0;
|
||||
float id = floor((ang / 6.2831853 + 0.5) * spokes);
|
||||
float rnd = hash(id);
|
||||
|
||||
// Only some spokes draw, and they flicker over time.
|
||||
float alive = step(0.55, fract(rnd + floor(TIME * 9.0) * 0.13));
|
||||
|
||||
// Thin line across the spoke's angular width.
|
||||
float local = fract((ang / 6.2831853 + 0.5) * spokes);
|
||||
float line = smoothstep(0.5, 0.05, abs(local - 0.5)) ;
|
||||
|
||||
// Radial extent: start further out for weak intensity, reach inward as it grows.
|
||||
float start = mix(0.62, 0.34, intensity) + rnd * 0.12;
|
||||
float mask = smoothstep(start, start + 0.25, dist);
|
||||
|
||||
float a = intensity * alive * line * mask;
|
||||
COLOR = vec4(line_color.rgb, a * line_color.a * 0.55);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://l1bq145iht8
|
||||
@@ -0,0 +1,80 @@
|
||||
shader_type spatial;
|
||||
|
||||
// Cel/toon surface shader used across the game.
|
||||
// - Banded (stepped) diffuse with a tinted shadow color instead of black
|
||||
// - Soft rim light for silhouette pop
|
||||
// - Optional world-space triplanar albedo (level geometry: no UVs needed,
|
||||
// grid stays world-scaled) — otherwise standard UV sampling (characters)
|
||||
|
||||
uniform vec4 albedo_color : source_color = vec4(1.0);
|
||||
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap, repeat_enable;
|
||||
uniform bool has_texture = true;
|
||||
uniform bool use_triplanar = false;
|
||||
uniform float triplanar_tile = 2.0; // world units per texture tile
|
||||
|
||||
uniform float band_edge : hint_range(-1.0, 1.0) = 0.05; // NdotL where light band starts
|
||||
uniform float band_softness : hint_range(0.001, 0.5) = 0.04;
|
||||
uniform float mid_band_edge : hint_range(-1.0, 1.0) = 0.55; // second, brighter band
|
||||
uniform vec4 shadow_color : source_color = vec4(0.62, 0.65, 0.78, 1.0); // cool shadow tint
|
||||
uniform float rim_strength : hint_range(0.0, 2.0) = 0.35;
|
||||
uniform float rim_width : hint_range(0.0, 1.0) = 0.65;
|
||||
uniform float specular_strength : hint_range(0.0, 1.0) = 0.25;
|
||||
uniform float specular_shininess : hint_range(1.0, 128.0) = 24.0;
|
||||
|
||||
varying vec3 world_pos;
|
||||
varying vec3 world_normal;
|
||||
|
||||
void vertex() {
|
||||
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||
}
|
||||
|
||||
vec3 sample_triplanar(vec3 p, vec3 n) {
|
||||
vec3 w = abs(n);
|
||||
w = pow(w, vec3(4.0));
|
||||
w /= (w.x + w.y + w.z);
|
||||
vec2 uv_x = p.zy / triplanar_tile;
|
||||
vec2 uv_y = p.xz / triplanar_tile;
|
||||
vec2 uv_z = p.xy / triplanar_tile;
|
||||
vec3 cx = texture(albedo_texture, uv_x).rgb;
|
||||
vec3 cy = texture(albedo_texture, uv_y).rgb;
|
||||
vec3 cz = texture(albedo_texture, uv_z).rgb;
|
||||
return cx * w.x + cy * w.y + cz * w.z;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec3 base = albedo_color.rgb;
|
||||
if (has_texture) {
|
||||
if (use_triplanar) {
|
||||
base *= sample_triplanar(world_pos, world_normal);
|
||||
} else {
|
||||
base *= texture(albedo_texture, UV).rgb;
|
||||
}
|
||||
}
|
||||
ALBEDO = base;
|
||||
ROUGHNESS = 1.0;
|
||||
SPECULAR = 0.0;
|
||||
|
||||
// Rim: brighten grazing angles for that inked-silhouette pop.
|
||||
float rim = 1.0 - clamp(dot(normalize(VIEW), NORMAL), 0.0, 1.0);
|
||||
rim = smoothstep(1.0 - rim_width, 1.0, rim);
|
||||
EMISSION = base * rim * rim_strength;
|
||||
}
|
||||
|
||||
void light() {
|
||||
float ndotl = dot(NORMAL, LIGHT);
|
||||
// Shadowing folds into the band test so shadow edges band too.
|
||||
float lit = ndotl * ATTENUATION;
|
||||
float band = smoothstep(band_edge - band_softness, band_edge + band_softness, lit);
|
||||
float mid = smoothstep(mid_band_edge - band_softness, mid_band_edge + band_softness, lit);
|
||||
// 3 tones: shadow tint -> base band (0.82) -> full light.
|
||||
float tone = mix(0.82, 1.0, mid);
|
||||
vec3 shade = mix(shadow_color.rgb, vec3(tone), band);
|
||||
DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade;
|
||||
|
||||
// Stepped specular dot for glossy toon highlights.
|
||||
vec3 h = normalize(VIEW + LIGHT);
|
||||
float spec = pow(clamp(dot(NORMAL, h), 0.0, 1.0), specular_shininess);
|
||||
spec = smoothstep(0.5 - band_softness, 0.5 + band_softness, spec);
|
||||
SPECULAR_LIGHT += LIGHT_COLOR * spec * specular_strength * band;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://rm8cyl45cawk
|
||||
@@ -0,0 +1,17 @@
|
||||
shader_type spatial;
|
||||
render_mode cull_front, unshaded;
|
||||
|
||||
// Inverted-hull outline. Assign as material_overlay on a GeometryInstance3D:
|
||||
// the mesh renders a second time, grown along its normals with front faces
|
||||
// culled, leaving a colored shell visible only at the silhouette.
|
||||
|
||||
uniform vec4 outline_color : source_color = vec4(0.06, 0.05, 0.09, 1.0);
|
||||
uniform float outline_width : hint_range(0.0, 0.1) = 0.02;
|
||||
|
||||
void vertex() {
|
||||
VERTEX += NORMAL * outline_width;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
ALBEDO = outline_color.rgb;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dn2biq23l8yuv
|
||||
@@ -44,10 +44,12 @@ const CLIP_FALLBACKS := {
|
||||
"Grapple": ["Grapple", "Fall", "Jump", "Idle"],
|
||||
"Dash": ["Dash", "Sprint", "Run", "Idle"],
|
||||
"Death": ["Death", "Fall"],
|
||||
"Hit": ["Hit", "Idle"],
|
||||
"Dance": ["Dance", "Idle"],
|
||||
}
|
||||
|
||||
const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
|
||||
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple"]
|
||||
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple", "Dance"]
|
||||
|
||||
const BLEND_TIME := 0.15
|
||||
|
||||
@@ -108,6 +110,9 @@ func load_model(path: String) -> void:
|
||||
_pose_mod = ShooterPoseModifier.new()
|
||||
_pose_mod.name = "ShooterPose"
|
||||
skeleton.add_child(_pose_mod)
|
||||
|
||||
# Cel-shaded look: toon shading over the imported textures + ink outline.
|
||||
LevelMaterials.apply_toon_recursive(scene)
|
||||
if animation_player:
|
||||
_index_animations()
|
||||
else:
|
||||
@@ -188,7 +193,23 @@ func _set_shadow_mode_recursive(node: Node, mode: int) -> void:
|
||||
# ── Animation state ───────────────────────────────────────────────────────────
|
||||
|
||||
var _prev_state: String = ""
|
||||
var _land_lock: float = 0.0 # seconds left where the Land one-shot owns playback
|
||||
var _oneshot_lock: float = 0.0 # seconds left where a one-shot owns playback
|
||||
var _dancing: bool = false
|
||||
|
||||
|
||||
## Play a one-shot clip (Hit reaction, Land, ...) over locomotion for
|
||||
## `lock_time` seconds; locomotion resumes afterwards.
|
||||
func play_oneshot(canonical: String, lock_time: float = 0.35) -> void:
|
||||
if not loaded or not _resolved_clips.has(canonical):
|
||||
return
|
||||
_oneshot_lock = lock_time
|
||||
_play_clip(canonical, true)
|
||||
|
||||
|
||||
## Emote toggle (Dance). Shown while grounded and near-idle; any real
|
||||
## movement breaks it (the controller clears the flag too).
|
||||
func set_dancing(on: bool) -> void:
|
||||
_dancing = on
|
||||
|
||||
|
||||
## Same contract as HumanoidModel.update_state(). Called by the movement
|
||||
@@ -197,15 +218,16 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
if not loaded or not animation_player:
|
||||
return
|
||||
|
||||
# A heavy landing plays the Land one-shot before locomotion resumes.
|
||||
if _land_lock > 0.0:
|
||||
_land_lock -= get_process_delta_time()
|
||||
if _land_lock > 0.0 and state in ["ground", "idle"]:
|
||||
# One-shots (Land, Hit) own playback briefly.
|
||||
if _oneshot_lock > 0.0:
|
||||
_oneshot_lock -= get_process_delta_time()
|
||||
if _oneshot_lock > 0.0:
|
||||
_prev_state = state
|
||||
return
|
||||
# A heavy landing plays the Land one-shot before locomotion resumes.
|
||||
if state in ["ground", "idle"] and _prev_state == "air" \
|
||||
and _vertical_speed() < -12.0 and _resolved_clips.has("Land"):
|
||||
_land_lock = 0.25
|
||||
_oneshot_lock = 0.25
|
||||
_play_clip("Land")
|
||||
_prev_state = state
|
||||
return
|
||||
@@ -214,7 +236,9 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
var clip := "Idle"
|
||||
match state:
|
||||
"ground", "idle":
|
||||
if is_crouching:
|
||||
if _dancing and speed < 0.5 and not is_crouching:
|
||||
clip = "Dance"
|
||||
elif is_crouching:
|
||||
clip = "CrouchWalk" if speed > 0.5 else "Crouch"
|
||||
elif speed > run_anim_reference_speed * 1.35:
|
||||
clip = "Sprint"
|
||||
@@ -288,12 +312,14 @@ func _process(delta: float) -> void:
|
||||
_pose_mod.wall = _cur_wall
|
||||
|
||||
|
||||
func _play_clip(canonical: String) -> void:
|
||||
func _play_clip(canonical: String, restart: bool = false) -> void:
|
||||
if not animation_player or not _resolved_clips.has(canonical):
|
||||
return
|
||||
var clip_name: String = _resolved_clips[canonical]
|
||||
if _current_clip == clip_name and animation_player.is_playing():
|
||||
if not restart and _current_clip == clip_name and animation_player.is_playing():
|
||||
return
|
||||
if restart:
|
||||
animation_player.stop()
|
||||
animation_player.play(clip_name, BLEND_TIME)
|
||||
_current_clip = clip_name
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ func _build_geometry() -> void:
|
||||
var env = get_node_or_null("WorldEnvironment")
|
||||
if env:
|
||||
var environment = env.environment
|
||||
environment.tonemap_mode = Environment.TONE_MAPPER_ACES
|
||||
# (keep the shared linear tonemap — ACES crushes the cel bands)
|
||||
if environment.sky and environment.sky.sky_material:
|
||||
var sky_mat = environment.sky.sky_material
|
||||
sky_mat.sky_top_color = Color(0.3, 0.5, 0.8)
|
||||
|
||||
@@ -96,29 +96,9 @@ func _ramp_static(pos: Vector3, size: Vector3, rot_deg: Vector3, color: Color, n
|
||||
# ── Environment ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_environment() -> void:
|
||||
var env := WorldEnvironment.new()
|
||||
env.name = "WorldEnvironment"
|
||||
var environment := Environment.new()
|
||||
environment.background_mode = Environment.BG_SKY
|
||||
var sky := Sky.new()
|
||||
var sky_mat := ProceduralSkyMaterial.new()
|
||||
sky_mat.sky_top_color = Color(0.15, 0.2, 0.35)
|
||||
sky_mat.sky_horizon_color = Color(0.45, 0.5, 0.65)
|
||||
sky_mat.ground_bottom_color = Color(0.1, 0.08, 0.06)
|
||||
sky_mat.ground_horizon_color = Color(0.35, 0.3, 0.25)
|
||||
sky.sky_material = sky_mat
|
||||
environment.sky = sky
|
||||
environment.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||||
environment.ambient_light_energy = 0.4
|
||||
environment.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||
environment.glow_enabled = true
|
||||
environment.glow_intensity = 0.3
|
||||
environment.glow_bloom = 0.1
|
||||
environment.fog_enabled = true
|
||||
environment.fog_light_color = Color(0.5, 0.55, 0.65)
|
||||
environment.fog_density = 0.002
|
||||
env.environment = environment
|
||||
add_child(env)
|
||||
# Shared stylized environment (anime sky, bloom, cel color grade).
|
||||
# Also creates the Sun/FillLight pair since none exists yet.
|
||||
LevelEnvironment.add_to(self)
|
||||
|
||||
|
||||
# ── Floor ─────────────────────────────────────────────────────────────────────
|
||||
@@ -261,24 +241,9 @@ func _build_target_dummy() -> void:
|
||||
# ── Lighting ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_lighting() -> void:
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.name = "Sun"
|
||||
sun.rotation_degrees = Vector3(-50, 30, 0)
|
||||
sun.light_color = Color(1.0, 0.95, 0.85)
|
||||
sun.light_energy = 1.2
|
||||
sun.shadow_enabled = true
|
||||
sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS
|
||||
sun.directional_shadow_max_distance = 100.0
|
||||
add_child(sun)
|
||||
|
||||
# Fill light (opposite side)
|
||||
var fill := DirectionalLight3D.new()
|
||||
fill.name = "FillLight"
|
||||
fill.rotation_degrees = Vector3(-30, -150, 0)
|
||||
fill.light_color = Color(0.6, 0.7, 0.9)
|
||||
fill.light_energy = 0.3
|
||||
fill.shadow_enabled = false
|
||||
add_child(fill)
|
||||
# Sun and fill are created by LevelEnvironment.add_to in _build_environment;
|
||||
# kept as a hook for subclasses that re-style them (see dust2).
|
||||
pass
|
||||
|
||||
|
||||
# ── Player ────────────────────────────────────────────────────────────────────
|
||||
@@ -316,6 +281,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
client_rep_config.add_property(":synced_is_crouching")
|
||||
client_rep_config.add_property(":synced_is_ads")
|
||||
client_rep_config.add_property(":synced_wall_side")
|
||||
client_rep_config.add_property(":synced_is_dancing")
|
||||
client_rep_config.add_property(":synced_grapple_point")
|
||||
client_rep_config.add_property(":synced_is_grapple_shooting")
|
||||
client_rep_config.add_property(":synced_skin_id")
|
||||
|
||||
@@ -75,6 +75,11 @@ var synced_position: Vector3 = Vector3.ZERO
|
||||
var synced_velocity: Vector3 = Vector3.ZERO
|
||||
var synced_is_ads: bool = false
|
||||
var synced_wall_side: float = 0.0 # -1 wall left, +1 wall right (wall-run lean)
|
||||
var synced_is_dancing: bool = false # dance emote (B), shown on the model
|
||||
|
||||
# Anime speed-lines overlay (local player only)
|
||||
var _speedlines: ColorRect = null
|
||||
var _speedline_burst: float = 0.0
|
||||
@export var synced_skin_id: String = ""
|
||||
@export var synced_weapon_path: String = ""
|
||||
|
||||
@@ -130,6 +135,7 @@ func _ready() -> void:
|
||||
add_child(_damage_layer)
|
||||
_setup_hit_marker()
|
||||
_setup_hud()
|
||||
_setup_speedlines()
|
||||
else:
|
||||
call_deferred("_hide_remote_weapons")
|
||||
|
||||
@@ -315,6 +321,18 @@ func _setup_audio() -> void:
|
||||
add_child(grapple_swing_player)
|
||||
grapple_swing_player.play()
|
||||
|
||||
func _setup_speedlines() -> void:
|
||||
_speedlines = ColorRect.new()
|
||||
_speedlines.name = "SpeedLines"
|
||||
_speedlines.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_speedlines.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var mat := ShaderMaterial.new()
|
||||
mat.shader = load("res://assets/shaders/speed_lines.gdshader")
|
||||
mat.set_shader_parameter("intensity", 0.0)
|
||||
_speedlines.material = mat
|
||||
_damage_layer.add_child(_speedlines)
|
||||
|
||||
|
||||
func _setup_hit_marker() -> void:
|
||||
hit_marker = Control.new()
|
||||
hit_marker.set_anchors_preset(Control.PRESET_CENTER)
|
||||
@@ -665,6 +683,12 @@ func rpc_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_n
|
||||
if attacker_id != 0:
|
||||
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
|
||||
|
||||
# Flinch: the victim's model plays a hit reaction on every screen.
|
||||
if amount > 0.0:
|
||||
var vm = get_visual_model()
|
||||
if vm and vm.has_method("play_oneshot"):
|
||||
vm.play_oneshot("Hit", 0.3)
|
||||
|
||||
# Knockback lands on the simulating peer (movement is client-authoritative)
|
||||
if is_multiplayer_authority() and impulse.length_squared() > 0.01:
|
||||
apply_impulse(impulse)
|
||||
@@ -817,6 +841,22 @@ func _physics_process(_delta: float) -> void:
|
||||
if Input.is_action_just_pressed("toggle_camera_view"):
|
||||
set_third_person(not third_person)
|
||||
|
||||
# Dance emote (B): toggles while grounded and idle-ish; any
|
||||
# movement/jump/crouch input breaks it.
|
||||
if Input.is_action_just_pressed("emote"):
|
||||
var m := _ensure_machine()
|
||||
var slow: bool = Vector2(velocity.x, velocity.z).length() < 1.0
|
||||
if not synced_is_dancing and m and m.current_state == "ground" and slow:
|
||||
synced_is_dancing = true
|
||||
else:
|
||||
synced_is_dancing = false
|
||||
if synced_is_dancing:
|
||||
var m2 := _ensure_machine()
|
||||
var moving := raw_input.length() > 0.1 or input_jump or input_crouch or input_dash
|
||||
var airborne: bool = m2 and m2.current_state != "ground"
|
||||
if moving or airborne:
|
||||
synced_is_dancing = false
|
||||
|
||||
var machine := _ensure_machine()
|
||||
if machine:
|
||||
machine.input_dir = raw_input
|
||||
@@ -834,6 +874,16 @@ func _physics_process(_delta: float) -> void:
|
||||
if not wind_player.playing and wind_factor > 0.0:
|
||||
wind_player.play()
|
||||
|
||||
# Anime speed lines: fade in past ~1.2x walk speed, spike on dash.
|
||||
if is_instance_valid(_speedlines):
|
||||
_speedline_burst = maxf(_speedline_burst - _delta * 2.5, 0.0)
|
||||
var hs := Vector2(velocity.x, velocity.z).length()
|
||||
var speed_intensity := clampf((hs - params.walk_speed * 1.2) / 12.0, 0.0, 0.85)
|
||||
var target := maxf(speed_intensity, _speedline_burst)
|
||||
var mat := _speedlines.material as ShaderMaterial
|
||||
var cur: float = mat.get_shader_parameter("intensity")
|
||||
mat.set_shader_parameter("intensity", lerpf(cur, target, 1.0 - exp(-10.0 * _delta)))
|
||||
|
||||
var sm := _ensure_machine()
|
||||
if not sm:
|
||||
return
|
||||
@@ -879,6 +929,8 @@ func _physics_process(_delta: float) -> void:
|
||||
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
|
||||
if visual.has_method("set_wall_side"):
|
||||
visual.set_wall_side(sm.wall_side)
|
||||
if visual.has_method("set_dancing"):
|
||||
visual.set_dancing(synced_is_dancing)
|
||||
|
||||
# Publish state for remote peers
|
||||
synced_movement_state = sm.current_state
|
||||
@@ -918,6 +970,8 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
grapple_shoot_player.play()
|
||||
elif ev == "grapple_latch":
|
||||
grapple_latch_player.play()
|
||||
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.
|
||||
@@ -960,6 +1014,8 @@ func _process(delta: float) -> void:
|
||||
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
|
||||
if visual.has_method("set_wall_side"):
|
||||
visual.set_wall_side(synced_wall_side)
|
||||
if visual.has_method("set_dancing"):
|
||||
visual.set_dancing(synced_is_dancing)
|
||||
# Check for weapon changes
|
||||
if synced_weapon_path != "" and synced_weapon_path != visual.get_meta("current_weapon_path", ""):
|
||||
visual.set_weapon(synced_weapon_path)
|
||||
|
||||
@@ -113,6 +113,11 @@ toggle_camera_view={
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":86,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
emote={
|
||||
"deadzone": 0.0,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":66,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[physics]
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
extends Object
|
||||
class_name LevelEnvironment
|
||||
|
||||
## Shared stylized environment for every level: saturated anime-sky gradient,
|
||||
## bloom for emissives (tracers, plasma), and a color grade that pushes the
|
||||
## cel-shaded look (high saturation, slight contrast lift). Builders call
|
||||
## add_to(level) instead of hand-rolling WorldEnvironment + sun.
|
||||
|
||||
|
||||
## Creates and adds a WorldEnvironment + key sun + cool fill light.
|
||||
## Returns the WorldEnvironment node.
|
||||
static func add_to(level: Node, sky_variant: String = "day") -> WorldEnvironment:
|
||||
var env := WorldEnvironment.new()
|
||||
env.name = "WorldEnvironment"
|
||||
env.environment = make_environment(sky_variant)
|
||||
level.add_child(env)
|
||||
|
||||
if not level.has_node("Sun"):
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.name = "Sun"
|
||||
sun.rotation_degrees = Vector3(-52, 38, 0)
|
||||
sun.light_color = Color(1.0, 0.97, 0.88)
|
||||
sun.light_energy = 1.4
|
||||
sun.shadow_enabled = true
|
||||
sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS
|
||||
sun.directional_shadow_max_distance = 120.0
|
||||
level.add_child(sun)
|
||||
|
||||
var fill := DirectionalLight3D.new()
|
||||
fill.name = "FillLight"
|
||||
fill.rotation_degrees = Vector3(-30, -142, 0)
|
||||
fill.light_color = Color(0.6, 0.7, 1.0)
|
||||
fill.light_energy = 0.25
|
||||
fill.shadow_enabled = false
|
||||
level.add_child(fill)
|
||||
return env
|
||||
|
||||
|
||||
static func make_environment(sky_variant: String = "day") -> Environment:
|
||||
var environment := Environment.new()
|
||||
environment.background_mode = Environment.BG_SKY
|
||||
var sky := Sky.new()
|
||||
var sky_mat := ProceduralSkyMaterial.new()
|
||||
match sky_variant:
|
||||
"sunset":
|
||||
sky_mat.sky_top_color = Color(0.25, 0.2, 0.5)
|
||||
sky_mat.sky_horizon_color = Color(0.95, 0.6, 0.45)
|
||||
sky_mat.ground_bottom_color = Color(0.18, 0.12, 0.2)
|
||||
sky_mat.ground_horizon_color = Color(0.85, 0.55, 0.45)
|
||||
sky_mat.sun_curve = 0.12
|
||||
_:
|
||||
# Bold anime day sky: deep saturated blue up top, bright cyan horizon.
|
||||
sky_mat.sky_top_color = Color(0.18, 0.4, 0.85)
|
||||
sky_mat.sky_horizon_color = Color(0.72, 0.88, 0.98)
|
||||
sky_mat.ground_bottom_color = Color(0.22, 0.2, 0.24)
|
||||
sky_mat.ground_horizon_color = Color(0.62, 0.7, 0.75)
|
||||
sky_mat.sun_curve = 0.1
|
||||
sky_mat.sun_angle_max = 20.0
|
||||
sky.sky_material = sky_mat
|
||||
environment.sky = sky
|
||||
|
||||
# Flat-ish ambient keeps toon shadow bands readable (sun does the shaping).
|
||||
environment.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||||
environment.ambient_light_energy = 0.55
|
||||
|
||||
# Filmic crushes the cel bands; linear-ish keeps them crisp.
|
||||
environment.tonemap_mode = Environment.TONE_MAPPER_LINEAR
|
||||
|
||||
# Bloom sells emissives (tracers, plasma, rim highlights).
|
||||
environment.glow_enabled = true
|
||||
environment.glow_intensity = 0.5
|
||||
environment.glow_bloom = 0.05
|
||||
environment.glow_hdr_threshold = 1.1
|
||||
|
||||
# Cel color grade: punchy saturation, hint of contrast.
|
||||
environment.adjustment_enabled = true
|
||||
environment.adjustment_saturation = 1.22
|
||||
environment.adjustment_contrast = 1.05
|
||||
|
||||
# A touch of depth haze for scale; far enough to not gray the arena.
|
||||
environment.fog_enabled = true
|
||||
environment.fog_light_color = Color(0.65, 0.75, 0.9)
|
||||
environment.fog_density = 0.0012
|
||||
return environment
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpl4d7ndkc4ft
|
||||
@@ -1,41 +1,93 @@
|
||||
extends Object
|
||||
class_name LevelMaterials
|
||||
|
||||
## Shared prototype-grid materials for code-built levels.
|
||||
## Shared cel-shaded materials for code-built levels and characters.
|
||||
##
|
||||
## Every level builder makes its geometry from flat-colored boxes; this gives
|
||||
## them all the same world-space grid surface (0.5 m cells via triplanar
|
||||
## mapping) so speed and distance stay readable at movement-shooter velocity,
|
||||
## while keeping each builder's color coding as a tint.
|
||||
## Level geometry gets the toon shader with world-space triplanar grid
|
||||
## (0.5 m cells) tinted per surface; characters get the same toon shading
|
||||
## over their own textures via convert_to_toon(), plus an inverted-hull
|
||||
## outline overlay for the inked silhouette.
|
||||
|
||||
const GRID_GRAY := "res://assets/textures/prototype/grid_gray.png"
|
||||
const GRID_DARK := "res://assets/textures/prototype/grid_dark.png"
|
||||
const TOON_SHADER := "res://assets/shaders/toon.gdshader"
|
||||
const OUTLINE_SHADER := "res://assets/shaders/toon_outline.gdshader"
|
||||
|
||||
## One texture tile = 2 m of world, so one grid cell = 0.5 m.
|
||||
const WORLD_UNITS_PER_TILE := 2.0
|
||||
|
||||
static var _cache: Dictionary = {}
|
||||
static var _outline_cache: Dictionary = {}
|
||||
|
||||
|
||||
## A tinted grid material. Cached per (tint, dark) so identical surfaces share
|
||||
## one material (fewer draw-state changes, and edits apply everywhere).
|
||||
static func tinted(tint: Color, dark: bool = false) -> StandardMaterial3D:
|
||||
## A tinted toon grid material for level geometry. Cached per (tint, dark) so
|
||||
## identical surfaces share one material.
|
||||
static func tinted(tint: Color, dark: bool = false) -> Material:
|
||||
var key := "%s|%s" % [tint.to_html(), dark]
|
||||
if _cache.has(key):
|
||||
return _cache[key]
|
||||
var mat := StandardMaterial3D.new()
|
||||
var shader: Shader = load(TOON_SHADER)
|
||||
var mat := ShaderMaterial.new()
|
||||
mat.shader = shader
|
||||
var tex_path := GRID_DARK if dark else GRID_GRAY
|
||||
var tex: Texture2D = load(tex_path) if ResourceLoader.exists(tex_path) else null
|
||||
if tex:
|
||||
mat.albedo_texture = tex
|
||||
# World triplanar: boxes need no UVs and the grid stays world-scaled.
|
||||
mat.uv1_triplanar = true
|
||||
mat.uv1_world_triplanar = true
|
||||
mat.uv1_scale = Vector3.ONE / WORLD_UNITS_PER_TILE
|
||||
mat.set_shader_parameter("albedo_texture", tex)
|
||||
mat.set_shader_parameter("has_texture", true)
|
||||
mat.set_shader_parameter("use_triplanar", true)
|
||||
mat.set_shader_parameter("triplanar_tile", WORLD_UNITS_PER_TILE)
|
||||
else:
|
||||
mat.set_shader_parameter("has_texture", false)
|
||||
# The texture is grayscale ~mid value; multiply by ~2x-brightened tint to
|
||||
# land near the original flat color while keeping the grid contrast.
|
||||
mat.albedo_color = Color(
|
||||
minf(tint.r * 1.9, 1.0), minf(tint.g * 1.9, 1.0), minf(tint.b * 1.9, 1.0))
|
||||
mat.roughness = 0.85
|
||||
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)))
|
||||
_cache[key] = mat
|
||||
return mat
|
||||
|
||||
|
||||
## Toon version of an arbitrary material (usually a character's imported
|
||||
## StandardMaterial3D): keeps its albedo texture/color, swaps the shading.
|
||||
static func toonify(src: Material) -> Material:
|
||||
var mat := ShaderMaterial.new()
|
||||
mat.shader = load(TOON_SHADER)
|
||||
var tex: Texture2D = null
|
||||
var col := Color.WHITE
|
||||
if src is BaseMaterial3D:
|
||||
tex = src.albedo_texture
|
||||
col = src.albedo_color
|
||||
mat.set_shader_parameter("albedo_texture", tex)
|
||||
mat.set_shader_parameter("has_texture", tex != null)
|
||||
mat.set_shader_parameter("use_triplanar", false)
|
||||
mat.set_shader_parameter("albedo_color", col)
|
||||
# Characters read best with a slightly wider lit band and stronger rim.
|
||||
mat.set_shader_parameter("rim_strength", 0.45)
|
||||
return mat
|
||||
|
||||
|
||||
## Swap every mesh surface under `node` to toon shading and add an
|
||||
## inverted-hull outline overlay. Safe on skinned meshes (material_overlay
|
||||
## re-renders the same deformed mesh).
|
||||
static func apply_toon_recursive(node: Node, outline_width: float = 0.015) -> void:
|
||||
if node is MeshInstance3D:
|
||||
var mi := node as MeshInstance3D
|
||||
var surface_count: int = mi.mesh.get_surface_count() if mi.mesh else 0
|
||||
for s in range(surface_count):
|
||||
var src := mi.get_active_material(s)
|
||||
if src and not (src is ShaderMaterial):
|
||||
mi.set_surface_override_material(s, toonify(src))
|
||||
if outline_width > 0.0:
|
||||
mi.material_overlay = outline(outline_width)
|
||||
for child in node.get_children():
|
||||
apply_toon_recursive(child, outline_width)
|
||||
|
||||
|
||||
static func outline(width: float = 0.015) -> ShaderMaterial:
|
||||
var key := "%.4f" % width
|
||||
if _outline_cache.has(key):
|
||||
return _outline_cache[key]
|
||||
var mat := ShaderMaterial.new()
|
||||
mat.shader = load(OUTLINE_SHADER)
|
||||
mat.set_shader_parameter("outline_width", width)
|
||||
_outline_cache[key] = mat
|
||||
return mat
|
||||
|
||||
@@ -119,6 +119,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
client_rep_config.add_property(":synced_is_crouching")
|
||||
client_rep_config.add_property(":synced_is_ads")
|
||||
client_rep_config.add_property(":synced_wall_side")
|
||||
client_rep_config.add_property(":synced_is_dancing")
|
||||
client_rep_config.add_property(":synced_grapple_point")
|
||||
client_rep_config.add_property(":synced_is_grapple_shooting")
|
||||
client_rep_config.add_property(":synced_skin_id")
|
||||
|
||||
@@ -19,6 +19,16 @@ func _ready() -> void:
|
||||
# Hide mouse
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
# Cel-shaded presentation: stylized sky/grade + toon materials over the
|
||||
# arena's baked geometry. The scene ships its own DirectionalLight3D;
|
||||
# rename it to Sun so the helper doesn't stack extra suns.
|
||||
var existing_light := get_node_or_null("DirectionalLight3D")
|
||||
if existing_light:
|
||||
existing_light.name = "Sun"
|
||||
if not has_node("WorldEnvironment"):
|
||||
LevelEnvironment.add_to(self)
|
||||
LevelMaterials.apply_toon_recursive(self, 0.0)
|
||||
|
||||
_build_hud()
|
||||
|
||||
# Multiplayer Spawning
|
||||
@@ -83,6 +93,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
client_rep_config.add_property(":synced_is_crouching")
|
||||
client_rep_config.add_property(":synced_is_ads")
|
||||
client_rep_config.add_property(":synced_wall_side")
|
||||
client_rep_config.add_property(":synced_is_dancing")
|
||||
client_rep_config.add_property(":synced_grapple_point")
|
||||
client_rep_config.add_property(":synced_is_grapple_shooting")
|
||||
client_rep_config.add_property(":synced_skin_id")
|
||||
|
||||
Reference in New Issue
Block a user