diff --git a/client/assets/theme/shaders/title_background.gdshader b/client/assets/theme/shaders/title_background.gdshader new file mode 100644 index 0000000..f86bcbc --- /dev/null +++ b/client/assets/theme/shaders/title_background.gdshader @@ -0,0 +1,15 @@ +shader_type canvas_item; +// Warm radial for the Title screen (mock: radial-gradient warm centre -> near +// black edge, centred at 70% 20%). Colours are Palette uniforms set by the title +// script (the DarkBay precedent), so no hex lives in the shader. +uniform vec4 warm : source_color; +uniform vec4 mid : source_color; +uniform vec4 edge : source_color; +uniform vec2 center = vec2(0.7, 0.2); + +void fragment() { + float d = distance(UV, center); + vec3 c = mix(warm.rgb, mid.rgb, clamp(d / 0.45, 0.0, 1.0)); + c = mix(c, edge.rgb, clamp((d - 0.45) / 0.55, 0.0, 1.0)); + COLOR = vec4(c, 1.0); +} diff --git a/client/assets/theme/shaders/title_background.gdshader.uid b/client/assets/theme/shaders/title_background.gdshader.uid new file mode 100644 index 0000000..08ec602 --- /dev/null +++ b/client/assets/theme/shaders/title_background.gdshader.uid @@ -0,0 +1 @@ +uid://cfrs0nt60f2mb diff --git a/client/scenes/title/TitleScreen.tscn b/client/scenes/title/TitleScreen.tscn index 0ee32cc..6d56cf0 100644 --- a/client/scenes/title/TitleScreen.tscn +++ b/client/scenes/title/TitleScreen.tscn @@ -18,6 +18,21 @@ anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 +[node name="Glow" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -420.0 +mouse_filter = 2 + +[node name="Embers" type="GPUParticles2D" parent="."] +position = Vector2(960, 1080) +amount = 24 +lifetime = 9.0 +preprocess = 4.0 + [node name="KeyArt" type="Label" parent="."] layout_mode = 1 anchors_preset = 6 diff --git a/client/scripts/theme/palette.gd b/client/scripts/theme/palette.gd index 401ef83..9151289 100644 --- a/client/scripts/theme/palette.gd +++ b/client/scripts/theme/palette.gd @@ -19,6 +19,10 @@ const BORDER_3 := Color("4a4238") const BORDER_4 := Color("3a352e") const BORDER_5 := Color("1a1512") +# --- Title-screen ambiance (warm radial background) --- +const TITLE_WARM := Color("2a2018") # radial centre +const TITLE_MID := Color("160f0a") # radial midpoint (STAGE_1 is the edge) + # --- Parchment --- const SHEET_TOP := Color("ece2ca") const SHEET_BOTTOM := Color("e4d8bd") diff --git a/client/scripts/ui/title/title_screen.gd b/client/scripts/ui/title/title_screen.gd index 0052ce1..ac51790 100644 --- a/client/scripts/ui/title/title_screen.gd +++ b/client/scripts/ui/title/title_screen.gd @@ -3,7 +3,9 @@ extends Control ## The entry-point screen (mock Title Screen). Editor-first (ADR 0001): the whole ## composition lives in TitleScreen.tscn; this script drives selection, input, and ## dispatches the menu. §2: presentation/state — no AI here (the DM enters at the -## shell). The atmosphere (bg shader, glow, embers) is layered on in a later task. +## shell). The atmosphere (warm bg shader, pulsing glow, drifting embers) is +## configured in code from Palette (the DarkBay precedent) — see _apply_background, +## _configure_embers, _pulse_glow. signal menu_activated(id: StringName) @@ -15,6 +17,8 @@ var _sel: int = 0 @onready var _menu: VBoxContainer = $Content/Menu @onready var _footer_left: Label = $FooterLeft @onready var _background: ColorRect = $Background +@onready var _glow: ColorRect = $Glow +@onready var _embers: GPUParticles2D = $Embers @onready var _rule: ColorRect = $Content/Rule @@ -25,6 +29,10 @@ func _ready() -> void: _background.color = Palette.STAGE_1 _rule.color = Palette.GOLD + _glow.color = Palette.BLOOD + _apply_background() + _configure_embers() + _pulse_glow() _rows = [] for row in _menu.get_children(): @@ -42,6 +50,48 @@ func _fit_to_viewport() -> void: size = get_viewport_rect().size +func _apply_background() -> void: + # Palette-driven shader material set in code (the DarkBay precedent) so no hex + # lives in the shader/scene; the editor shows the ColorRect fallback. + var mat := ShaderMaterial.new() + mat.shader = load("res://assets/theme/shaders/title_background.gdshader") + mat.set_shader_parameter("warm", Palette.TITLE_WARM) + mat.set_shader_parameter("mid", Palette.TITLE_MID) + mat.set_shader_parameter("edge", Palette.STAGE_1) + _background.material = mat + + +func _configure_embers() -> void: + var pm := ParticleProcessMaterial.new() + pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX + pm.emission_box_extents = Vector3(760, 4, 0) # a wide strip along the bottom + pm.direction = Vector3(0, -1, 0) + pm.spread = 12.0 + pm.gravity = Vector3(0, -18, 0) + pm.initial_velocity_min = 24.0 + pm.initial_velocity_max = 52.0 + pm.scale_min = 0.4 + pm.scale_max = 1.0 + pm.color = Palette.GOLD_BRIGHT + # fade out over life + var ramp := Gradient.new() + ramp.set_color(0, Color(Palette.GOLD_BRIGHT, 0.0)) + ramp.set_color(1, Color(Palette.GOLD_BRIGHT, 0.0)) + ramp.add_point(0.15, Palette.GOLD_BRIGHT) + var tex := GradientTexture1D.new() + tex.gradient = ramp + pm.color_ramp = tex + _embers.process_material = pm + + +func _pulse_glow() -> void: + # A slow warm pulse (the off-screen fire). A code tween, not an AnimationPlayer: + # a 2-keyframe modulate loop is trivial in code and motion is runtime-only anyway. + var tween := create_tween().set_loops().set_trans(Tween.TRANS_SINE) + tween.tween_property(_glow, "modulate:a", 0.8, 3.0) + tween.tween_property(_glow, "modulate:a", 0.5, 3.0) + + func _wire_row_mouse() -> void: for i in range(_rows.size()): var idx := i # fresh per-iteration binding for the closures