class_name TitleScreen 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 (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) const SHELL := "res://scenes/shell/MainWindowShell.tscn" var _rows: Array = [] 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 func _ready() -> void: # Root Control run via F6 is not sized by full-rect anchors — size explicitly. _fit_to_viewport() get_viewport().size_changed.connect(_fit_to_viewport) _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(): if row.has_meta("action"): _rows.append(row) (row.get_node("Marker") as Label).add_theme_color_override("font_color", Palette.GOLD) _wire_row_mouse() _footer_left.text = Version.new().footer() menu_activated.connect(_on_menu_activated) _update_selection() 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 var row: Control = _rows[idx] row.mouse_entered.connect(func(): _sel = idx _update_selection()) row.gui_input.connect(func(e: InputEvent): if e is InputEventMouseButton and e.pressed and e.button_index == MOUSE_BUTTON_LEFT: _activate(idx)) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("ui_down"): _move(1) elif event.is_action_pressed("ui_up"): _move(-1) elif event.is_action_pressed("ui_accept"): _activate(_sel) func _move(delta: int) -> void: if _rows.is_empty(): return _sel = wrapi(_sel + delta, 0, _rows.size()) _update_selection() func _activate(i: int) -> void: if i < 0 or i >= _rows.size(): return menu_activated.emit(_rows[i].get_meta("action")) func _on_menu_activated(id: StringName) -> void: match id: &"new_game": get_tree().change_scene_to_file(SHELL) &"quit": get_tree().quit() _: pass # inert placeholder — the screen lands in a later milestone func _update_selection() -> void: for i in range(_rows.size()): var row: Control = _rows[i] var selected := i == _sel (row.get_node("Marker") as Label).modulate.a = 1.0 if selected else 0.0 var label := row.get_node("Body/Label") as Label label.add_theme_color_override("font_color", Palette.CREAM_BRIGHT if selected else Palette.MUTED_MONO)