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"]))