135 lines
4.4 KiB
GDScript
135 lines
4.4 KiB
GDScript
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)
|
|
|
|
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
|
|
# RGB from Palette, low alpha so the fire glow is subtle (the pulse tween varies
|
|
# modulate:a on top of this); a fully-opaque BLOOD read as a solid red slab.
|
|
_glow.color = Color(Palette.BLOOD, 0.22)
|
|
_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()
|
|
_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 _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)
|