Finish the button-label audit with a surface-by-surface table, clarify Quit game vs Quit to menu, rename Apply to Apply settings, and use string-table keys for the new labels. Extend the smoke checklist with UI copy/button checks. Headless 250/250, docs check OK.
250 lines
8.4 KiB
GDScript
250 lines
8.4 KiB
GDScript
class_name StartMenu
|
|
extends CanvasLayer
|
|
## Startup menu: pick a venue, start the shift, open settings, or quit.
|
|
## Keyboard and mouse both work. Replaces `V` as the player-facing venue path
|
|
## (the `V` bind stays as a debug shortcut).
|
|
|
|
signal venue_selected(scenario_id: String)
|
|
signal continue_requested()
|
|
signal help_requested()
|
|
signal settings_requested()
|
|
signal quit_requested()
|
|
|
|
const UiArt := preload("res://src/game/ui/art.gd")
|
|
const Strings := preload("res://src/core/strings.gd")
|
|
|
|
const VENUES := [
|
|
{"id": "hollywood_building", "label": "Hollywood Boulevard", "blurb": "Building entrance, tourists and late-night trouble."},
|
|
{"id": "joke_shop", "label": "The Joke Shop", "blurb": "Guest list, IDs and wand screening."},
|
|
{"id": "tv_studio", "label": "Late Night TV Studio", "blurb": "Credentials, filming boundary, celebrity arrivals."},
|
|
]
|
|
|
|
var _index := 0
|
|
var _buttons: Array[Button] = []
|
|
var _root: Control = null
|
|
var _status: Label = null
|
|
var _continue_button: Button = null
|
|
var _campaign: Dictionary = {}
|
|
|
|
func _ready() -> void:
|
|
layer = 20
|
|
_build()
|
|
hide()
|
|
|
|
func _build() -> void:
|
|
_root = Control.new()
|
|
_root.name = "StartMenuRoot"
|
|
_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
_root.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
add_child(_root)
|
|
|
|
var art := UiArt.new()
|
|
var backdrop_texture := art.texture("start_menu")
|
|
if backdrop_texture != null:
|
|
var backdrop := TextureRect.new()
|
|
backdrop.name = "MenuArt"
|
|
backdrop.texture = backdrop_texture
|
|
backdrop.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
backdrop.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
|
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
backdrop.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_root.add_child(backdrop)
|
|
# The mockup includes its own title/buttons; cover that panel with an
|
|
# opaque rounded panel and keep the real controls on top.
|
|
var panel := ColorRect.new()
|
|
panel.name = "MenuPanel"
|
|
panel.color = Color(0.075, 0.08, 0.095, 1.0)
|
|
panel.set_anchor(SIDE_LEFT, 0.148)
|
|
panel.set_anchor(SIDE_TOP, 0.061)
|
|
panel.set_anchor(SIDE_RIGHT, 0.781)
|
|
panel.set_anchor(SIDE_BOTTOM, 0.921)
|
|
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_root.add_child(panel)
|
|
var dim := ColorRect.new()
|
|
dim.color = Color(0.02, 0.03, 0.05, 0.18)
|
|
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
dim.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_root.add_child(dim)
|
|
else:
|
|
var bg := ColorRect.new()
|
|
bg.color = Color(0.03, 0.04, 0.07, 0.96)
|
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_root.add_child(bg)
|
|
|
|
var center := CenterContainer.new()
|
|
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
_root.add_child(center)
|
|
|
|
var column := VBoxContainer.new()
|
|
column.name = "Column"
|
|
column.custom_minimum_size = Vector2(460, 0)
|
|
column.add_theme_constant_override("separation", 14)
|
|
center.add_child(column)
|
|
|
|
var title := Label.new()
|
|
title.text = "GIG LINE"
|
|
title.add_theme_font_size_override("font_size", 44)
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
column.add_child(title)
|
|
|
|
var subtitle := Label.new()
|
|
subtitle.text = "Pick a venue to start your shift."
|
|
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
subtitle.add_theme_color_override("font_color", Color(0.72, 0.75, 0.82))
|
|
column.add_child(subtitle)
|
|
|
|
_status = Label.new()
|
|
_status.name = "CampaignStatus"
|
|
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
_status.add_theme_color_override("font_color", Color(0.62, 0.78, 0.9))
|
|
column.add_child(_status)
|
|
|
|
_continue_button = Button.new()
|
|
_continue_button.name = "ContinueButton"
|
|
_continue_button.custom_minimum_size = Vector2(460, 52)
|
|
_continue_button.pressed.connect(_on_continue_pressed)
|
|
_style_venue_button(_continue_button)
|
|
column.add_child(_continue_button)
|
|
|
|
for i in VENUES.size():
|
|
var venue: Dictionary = VENUES[i]
|
|
var button := Button.new()
|
|
button.name = "Venue_%s" % venue["id"]
|
|
button.text = "%s\n%s" % [venue["label"], venue["blurb"]]
|
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
button.custom_minimum_size = Vector2(460, 62)
|
|
button.pressed.connect(_on_venue_pressed.bind(i))
|
|
_style_venue_button(button)
|
|
column.add_child(button)
|
|
_buttons.append(button)
|
|
|
|
var spacer := Control.new()
|
|
spacer.custom_minimum_size = Vector2(0, 10)
|
|
column.add_child(spacer)
|
|
|
|
var row := HBoxContainer.new()
|
|
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
row.add_theme_constant_override("separation", 12)
|
|
column.add_child(row)
|
|
|
|
var help_button := Button.new()
|
|
help_button.name = "HelpButton"
|
|
help_button.text = "Help"
|
|
help_button.pressed.connect(func(): help_requested.emit())
|
|
row.add_child(help_button)
|
|
|
|
var settings_button := Button.new()
|
|
settings_button.name = "SettingsButton"
|
|
settings_button.text = "Settings"
|
|
settings_button.pressed.connect(func(): settings_requested.emit())
|
|
row.add_child(settings_button)
|
|
|
|
var quit_button := Button.new()
|
|
quit_button.name = "QuitButton"
|
|
quit_button.text = Strings.lookup("ui.quit_game")
|
|
quit_button.pressed.connect(func(): quit_requested.emit())
|
|
row.add_child(quit_button)
|
|
|
|
var hint := Label.new()
|
|
hint.text = "Up/Down to choose, Enter to start, Esc to quit"
|
|
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
hint.add_theme_color_override("font_color", Color(0.55, 0.58, 0.65))
|
|
column.add_child(hint)
|
|
|
|
_focus_index(0)
|
|
|
|
## Match the dark, rounded rows in the `start_menu` mockup.
|
|
func _style_venue_button(button: Button) -> void:
|
|
button.add_theme_font_size_override("font_size", 18)
|
|
button.add_theme_color_override("font_color", Color(0.94, 0.94, 0.92))
|
|
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
|
var normal := StyleBoxFlat.new()
|
|
normal.bg_color = Color(0.115, 0.125, 0.15, 0.96)
|
|
normal.corner_radius_top_left = 12
|
|
normal.corner_radius_top_right = 12
|
|
normal.corner_radius_bottom_left = 12
|
|
normal.corner_radius_bottom_right = 12
|
|
normal.content_margin_left = 22
|
|
normal.content_margin_right = 22
|
|
normal.content_margin_top = 10
|
|
normal.content_margin_bottom = 10
|
|
var hover := normal.duplicate() as StyleBoxFlat
|
|
hover.bg_color = Color(0.16, 0.175, 0.21, 0.98)
|
|
var focus := normal.duplicate() as StyleBoxFlat
|
|
focus.bg_color = Color(0.14, 0.155, 0.19, 0.98)
|
|
focus.border_color = Color(0.85, 0.86, 0.88)
|
|
focus.border_width_left = 2
|
|
focus.border_width_top = 2
|
|
focus.border_width_right = 2
|
|
focus.border_width_bottom = 2
|
|
button.add_theme_stylebox_override("normal", normal)
|
|
button.add_theme_stylebox_override("hover", hover)
|
|
button.add_theme_stylebox_override("focus", focus)
|
|
button.add_theme_stylebox_override("pressed", hover)
|
|
|
|
func _focus_index(i: int) -> void:
|
|
if _buttons.is_empty():
|
|
return
|
|
_index = clampi(i, 0, _buttons.size() - 1)
|
|
_buttons[_index].grab_focus()
|
|
|
|
func open(campaign: Dictionary = {}) -> void:
|
|
_campaign = campaign.duplicate(true)
|
|
visible = true
|
|
_root.visible = true
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
var next_id := str(_campaign.get("next_venue", ""))
|
|
var complete := bool(_campaign.get("complete", false))
|
|
if _status:
|
|
if complete:
|
|
_status.text = "Campaign complete — pick any venue for a free shift."
|
|
elif next_id != "":
|
|
_status.text = "Campaign shift %d · next: %s" % [
|
|
int(_campaign.get("shift_number", 1)), _venue_label(next_id)]
|
|
if _continue_button:
|
|
var next_label := _venue_label(next_id)
|
|
_continue_button.text = "Continue shift — %s" % next_label if next_id != "" else "Campaign complete"
|
|
_continue_button.visible = not complete
|
|
_focus_index(_index)
|
|
|
|
func _venue_label(id: String) -> String:
|
|
for venue in VENUES:
|
|
if str(venue["id"]) == id:
|
|
return str(venue["label"])
|
|
return id.replace("_", " ").capitalize()
|
|
|
|
func _on_continue_pressed() -> void:
|
|
close()
|
|
continue_requested.emit()
|
|
|
|
func close() -> void:
|
|
visible = false
|
|
_root.visible = false
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if not visible:
|
|
return
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
match event.keycode:
|
|
KEY_UP, KEY_W:
|
|
_focus_index(_index - 1)
|
|
get_viewport().set_input_as_handled()
|
|
KEY_DOWN, KEY_S:
|
|
_focus_index(_index + 1)
|
|
get_viewport().set_input_as_handled()
|
|
KEY_ENTER, KEY_KP_ENTER, KEY_SPACE:
|
|
_on_venue_pressed(_index)
|
|
get_viewport().set_input_as_handled()
|
|
KEY_ESCAPE:
|
|
quit_requested.emit()
|
|
get_viewport().set_input_as_handled()
|
|
|
|
func _on_venue_pressed(i: int) -> void:
|
|
if i < 0 or i >= VENUES.size():
|
|
return
|
|
var venue: Dictionary = VENUES[i]
|
|
close()
|
|
venue_selected.emit(str(venue["id"]))
|