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) # Screen-space ink edge pass: drawn outlines on every model for whatever # camera renders (fullscreen POSITION-override quad, never culled). if not level.has_node("InkEdgePost"): var ink := MeshInstance3D.new() ink.name = "InkEdgePost" var quad := QuadMesh.new() quad.size = Vector2(2, 2) var ink_mat := ShaderMaterial.new() ink_mat.shader = load("res://assets/shaders/ink_edge.gdshader") quad.material = ink_mat ink.mesh = quad ink.extra_cull_margin = 16384.0 level.add_child(ink) # 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 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 with a high white point: near-linear through the cel bands (they # stay crisp) but a soft shoulder above 1.0. LINEAR clipped lit white # surfaces and then bloomed the clip — every facade glowed like porcelain. environment.tonemap_mode = Environment.TONE_MAPPER_FILMIC environment.tonemap_white = 2.4 # Bloom for genuine emissives only (tracers, plasma, neon) — lit geometry # must never cross this threshold or it reads as glowing glass. environment.glow_enabled = true environment.glow_intensity = 0.4 environment.glow_bloom = 0.05 environment.glow_hdr_threshold = 1.45 # Cel color grade: a LIGHT saturation lift only. Anything stronger shifts # hues — authored ginger hair grades into fire-truck red, pastels go neon — # and the whole game reads "filtered". Let the textures' own colors carry # the vibrancy. environment.adjustment_enabled = true environment.adjustment_saturation = 1.08 environment.adjustment_contrast = 1.03 # 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