Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
518 lines
20 KiB
GDScript
518 lines
20 KiB
GDScript
class_name MainMenu
|
||
extends Control
|
||
## Title screen and match setup in front of the menu diorama.
|
||
## Presentation state (screen, camera shot, fades) lives here only. Match options are
|
||
## written through GameState exactly as before; nothing from the stage reaches a match.
|
||
|
||
signal start_requested
|
||
signal settings_requested
|
||
signal models_requested
|
||
signal quit_requested
|
||
signal selection_changed
|
||
|
||
const Stage = preload("res://scripts/presentation/menu_stage.gd")
|
||
const FADE := .22
|
||
enum Screen { TITLE, SETUP }
|
||
|
||
var screen: int = Screen.TITLE
|
||
var pending_screen: int = Screen.TITLE
|
||
var stage: Node3D
|
||
var stage_view: SubViewport
|
||
var occluder: Control
|
||
var start_button: Button
|
||
var new_game_button: Button
|
||
var faction_choice: FactionChoice
|
||
var rival_choice: ChoiceGroup
|
||
var mode_choice: ChoiceGroup
|
||
var ai_choice: ChoiceGroup
|
||
var timed_toggle: CheckButton
|
||
var length_summary: Label
|
||
var result_title: Label
|
||
var result_body: Label
|
||
var reduced_motion: bool = false
|
||
var _stage_rect: TextureRect
|
||
var _title_screen: Control
|
||
var _setup_screen: Control
|
||
var _title_scrim: TextureRect
|
||
var _setup_scrim: TextureRect
|
||
var _shield: Control
|
||
var _shield_left: float = 0.0
|
||
var _fade: Tween
|
||
var _locked: bool = false
|
||
var _active: bool = false
|
||
var _leaving: bool = false
|
||
var _result_panel: Control
|
||
var _change_button: Button
|
||
var _models_button: Button
|
||
var _options_button: Button
|
||
var _advanced: VBoxContainer
|
||
var _advanced_button: Button
|
||
var _advanced_summary: Label
|
||
var _timed_hint: Label
|
||
var _title_caption: Label
|
||
var _style_heading: Label
|
||
var _style_facts: Label
|
||
var _setup_caption: Label
|
||
|
||
func _ready() -> void:
|
||
name = "MainMenu"
|
||
theme = SaumwachtTheme.shared()
|
||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||
_build_stage()
|
||
_title_scrim = _scrim(Vector2(0, 1), Vector2(.78, .18), [.92, .62, 0.0], [0.0, .42, 1.0])
|
||
_setup_scrim = _scrim(Vector2(0, .5), Vector2(1, .5), [.9, .62, 0.0], [0.0, .36, .6])
|
||
var top := _scrim(Vector2(.5, 0), Vector2(.5, 1), [.55, 0.0, 0.0], [0.0, .2, 1.0])
|
||
top.name = "TopVignette"
|
||
_build_title()
|
||
_build_setup()
|
||
_shield = Control.new()
|
||
_shield.name = "InputShield"
|
||
_shield.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
_shield.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
_shield.visible = false
|
||
add_child(_shield)
|
||
get_tree().root.size_changed.connect(_resize_stage)
|
||
_resize_stage()
|
||
Preferences.changed.connect(_apply_preferences)
|
||
_apply_preferences()
|
||
sync()
|
||
|
||
# ---------------------------------------------------------------- building
|
||
func _build_stage() -> void:
|
||
stage_view = SubViewport.new()
|
||
stage_view.name = "StageViewport"
|
||
stage_view.own_world_3d = true
|
||
stage_view.msaa_3d = Viewport.MSAA_2X
|
||
stage_view.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||
add_child(stage_view)
|
||
stage = Stage.new()
|
||
stage_view.add_child(stage)
|
||
_stage_rect = TextureRect.new()
|
||
_stage_rect.name = "StageImage"
|
||
_stage_rect.texture = stage_view.get_texture()
|
||
_stage_rect.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
_stage_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
_stage_rect.stretch_mode = TextureRect.STRETCH_SCALE
|
||
_stage_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(_stage_rect)
|
||
|
||
func _scrim(from: Vector2, to: Vector2, alphas: Array, offsets: Array) -> TextureRect:
|
||
var gradient := Gradient.new()
|
||
gradient.offsets = PackedFloat32Array(offsets)
|
||
var colors := PackedColorArray()
|
||
for alpha: float in alphas:
|
||
colors.append(Color(SaumwachtTheme.NIGHT, alpha))
|
||
gradient.colors = colors
|
||
var texture := GradientTexture2D.new()
|
||
texture.gradient = gradient
|
||
texture.fill_from = from
|
||
texture.fill_to = to
|
||
texture.width = 256
|
||
texture.height = 256
|
||
var rect := TextureRect.new()
|
||
rect.texture = texture
|
||
rect.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
rect.stretch_mode = TextureRect.STRETCH_SCALE
|
||
rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(rect)
|
||
return rect
|
||
|
||
func _label(text_value: String, variation: String, parent: Node) -> Label:
|
||
var label := Label.new()
|
||
label.text = text_value
|
||
label.theme_type_variation = variation
|
||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
parent.add_child(label)
|
||
return label
|
||
|
||
func _button(text_value: String, variation: String, parent: Node, action: Callable) -> Button:
|
||
var button := Button.new()
|
||
button.text = text_value
|
||
button.theme_type_variation = variation
|
||
button.focus_mode = Control.FOCUS_ALL
|
||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
button.pressed.connect(action)
|
||
parent.add_child(button)
|
||
return button
|
||
|
||
func _emblem(path: String, size: float, parent: Node) -> TextureRect:
|
||
var image := TextureRect.new()
|
||
image.texture = load(path)
|
||
image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
image.custom_minimum_size = Vector2(size, size)
|
||
image.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
parent.add_child(image)
|
||
return image
|
||
|
||
func _build_title() -> void:
|
||
_title_screen = Control.new()
|
||
_title_screen.name = "TitleScreen"
|
||
_title_screen.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
_title_screen.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(_title_screen)
|
||
var column := VBoxContainer.new()
|
||
column.name = "TitleColumn"
|
||
column.anchor_top = 1.0
|
||
column.anchor_bottom = 1.0
|
||
column.offset_left = 88
|
||
column.offset_right = 760
|
||
column.offset_bottom = -72
|
||
column.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||
column.add_theme_constant_override("separation", 10)
|
||
_title_screen.add_child(column)
|
||
_result_panel = PanelContainer.new()
|
||
_result_panel.theme_type_variation = "CaptionPanel"
|
||
_result_panel.visible = false
|
||
column.add_child(_result_panel)
|
||
var result := VBoxContainer.new()
|
||
result.add_theme_constant_override("separation", 2)
|
||
_result_panel.add_child(result)
|
||
result_title = _label("", "HeadingLabel", result)
|
||
result_body = _label("", "MutedLabel", result)
|
||
var spacer := Control.new()
|
||
spacer.custom_minimum_size.y = 12
|
||
column.add_child(spacer)
|
||
var brand := HBoxContainer.new()
|
||
brand.add_theme_constant_override("separation", 22)
|
||
column.add_child(brand)
|
||
_emblem("res://assets/ui/emblems/saumwacht.svg", 112, brand)
|
||
var names := VBoxContainer.new()
|
||
names.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
names.add_theme_constant_override("separation", 0)
|
||
brand.add_child(names)
|
||
_label("SAUMWACHT", "TitleLabel", names)
|
||
_label("DIE GRÜNE KLUFT", "SubtitleLabel", names)
|
||
var lore := _label("Stelle deine Wächter auf, schütze den Saumkern und überstehe den Rivalen.", "LoreLabel", column)
|
||
lore.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
lore.custom_minimum_size.x = 560
|
||
var gap := Control.new()
|
||
gap.custom_minimum_size.y = 22
|
||
column.add_child(gap)
|
||
var primary_row := HBoxContainer.new()
|
||
primary_row.add_theme_constant_override("separation", 14)
|
||
column.add_child(primary_row)
|
||
new_game_button = _button("NEUE PARTIE", "PrimaryButton", primary_row, _on_title_primary)
|
||
new_game_button.name = "NewGame"
|
||
new_game_button.custom_minimum_size = Vector2(340, 64)
|
||
_change_button = _button("Bund und Partie ändern", "Button", primary_row, func() -> void: show_screen(Screen.SETUP))
|
||
_change_button.custom_minimum_size.y = 64
|
||
_change_button.visible = false
|
||
var secondary := HBoxContainer.new()
|
||
secondary.add_theme_constant_override("separation", 12)
|
||
column.add_child(secondary)
|
||
_models_button = _button("Modelle ansehen · V", "Button", secondary, func() -> void: models_requested.emit())
|
||
_options_button = _button("Optionen & Steuerung · F1", "Button", secondary, func() -> void: settings_requested.emit())
|
||
var quit := _button("Spiel beenden", "QuietButton", column, func() -> void: quit_requested.emit())
|
||
quit.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||
var version := _label("Demo %s · Einzelspieler" % str(ProjectSettings.get_setting("application/config/version", "")), "MutedLabel", _title_screen)
|
||
version.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
|
||
version.offset_left = -360
|
||
version.offset_right = -40
|
||
version.offset_top = 30
|
||
version.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
_title_caption = _caption(_title_screen, "Glutpakt und Wurzelbund an den Ufern der Grünen Kluft")
|
||
|
||
func _caption(parent: Control, text_value: String) -> Label:
|
||
var plate := PanelContainer.new()
|
||
plate.theme_type_variation = "CaptionPanel"
|
||
plate.anchor_left = 1.0
|
||
plate.anchor_right = 1.0
|
||
plate.anchor_top = 1.0
|
||
plate.anchor_bottom = 1.0
|
||
plate.offset_right = -40
|
||
plate.offset_bottom = -36
|
||
plate.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||
plate.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||
plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
parent.add_child(plate)
|
||
var label := _label(text_value, "MutedLabel", plate)
|
||
label.add_theme_font_size_override("font_size", 15)
|
||
return label
|
||
|
||
func _build_setup() -> void:
|
||
_setup_screen = Control.new()
|
||
_setup_screen.name = "SetupScreen"
|
||
_setup_screen.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
_setup_screen.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_setup_screen.visible = false
|
||
add_child(_setup_screen)
|
||
var panel := PanelContainer.new()
|
||
panel.name = "SetupPanel"
|
||
panel.theme_type_variation = "MenuPanel"
|
||
panel.anchor_bottom = 1.0
|
||
panel.offset_left = 40
|
||
panel.offset_top = 30
|
||
panel.offset_right = 700
|
||
panel.offset_bottom = -30
|
||
_setup_screen.add_child(panel)
|
||
var layout := VBoxContainer.new()
|
||
layout.add_theme_constant_override("separation", 12)
|
||
panel.add_child(layout)
|
||
var top := HBoxContainer.new()
|
||
layout.add_child(top)
|
||
var back := _button("‹ Hauptmenü Esc", "QuietButton", top, func() -> void: show_screen(Screen.TITLE))
|
||
back.name = "Back"
|
||
var eyebrow := _label("NEUE PARTIE", "EyebrowLabel", top)
|
||
eyebrow.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
eyebrow.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
_label("Wähle deinen Bund", "HeadingLabel", layout)
|
||
var scroll := ScrollContainer.new()
|
||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
layout.add_child(scroll)
|
||
# Inner padding keeps focus rings inside the scroll clip.
|
||
var padding := MarginContainer.new()
|
||
padding.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
for side: String in ["left", "right", "top", "bottom"]:
|
||
padding.add_theme_constant_override("margin_" + side, 8)
|
||
scroll.add_child(padding)
|
||
var body := VBoxContainer.new()
|
||
body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
body.add_theme_constant_override("separation", 12)
|
||
padding.add_child(body)
|
||
faction_choice = FactionChoice.new()
|
||
faction_choice.name = "FactionChoice"
|
||
body.add_child(faction_choice)
|
||
faction_choice.item_selected.connect(func(index: int) -> void:
|
||
if GameState.select_faction(0, DataCatalog.FACTIONS[index]):
|
||
selection_changed.emit()
|
||
sync())
|
||
# One shared play-style description for the selected Bund, next to one shared 3D preview.
|
||
_style_heading = _label("", "SectionLabel", body)
|
||
_style_facts = _label("", "CardFactLabel", body)
|
||
_style_facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_style_facts.add_theme_constant_override("line_spacing", 4)
|
||
_label("PARTIELÄNGE", "SectionLabel", body)
|
||
mode_choice = ChoiceGroup.new(true)
|
||
mode_choice.name = "ModeChoice"
|
||
mode_choice.add_option("Expedition", "20 Wellen · 4 Bosse")
|
||
mode_choice.add_option("Kurzpartie", "8 Wellen · 2 Bosse")
|
||
body.add_child(mode_choice)
|
||
mode_choice.item_selected.connect(func(index: int) -> void:
|
||
GameState.select_mode(GameState.MODE_EXPEDITION if index == 0 else GameState.MODE_SHORT)
|
||
sync())
|
||
length_summary = _label("", "MutedLabel", body)
|
||
var disclosure := HBoxContainer.new()
|
||
body.add_child(disclosure)
|
||
_advanced_button = _button("+ Weitere Einstellungen", "QuietButton", disclosure, _toggle_advanced)
|
||
_advanced_button.name = "AdvancedToggle"
|
||
_advanced_summary = _label("", "MutedLabel", disclosure)
|
||
_advanced_summary.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_advanced_summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
_advanced_summary.add_theme_font_size_override("font_size", 15)
|
||
_advanced = VBoxContainer.new()
|
||
_advanced.name = "AdvancedOptions"
|
||
_advanced.add_theme_constant_override("separation", 8)
|
||
_advanced.visible = false
|
||
body.add_child(_advanced)
|
||
_label("RIVALENBUND", "SectionLabel", _advanced)
|
||
rival_choice = ChoiceGroup.new(true)
|
||
rival_choice.name = "RivalChoice"
|
||
for id: String in DataCatalog.FACTIONS:
|
||
rival_choice.add_option(DataCatalog.FACTION_NAMES[id])
|
||
_advanced.add_child(rival_choice)
|
||
rival_choice.item_selected.connect(func(index: int) -> void:
|
||
GameState.select_faction(1, DataCatalog.FACTIONS[index])
|
||
sync())
|
||
_label("KI-STUFE", "SectionLabel", _advanced)
|
||
ai_choice = ChoiceGroup.new(true)
|
||
ai_choice.name = "AiChoice"
|
||
ai_choice.add_option("Lernmodus", "feste Formationen, milde Söldnerangriffe")
|
||
ai_choice.add_option("Standard", "verbessert sich nach Lecks, volle Söldnerangriffe")
|
||
_advanced.add_child(ai_choice)
|
||
ai_choice.item_selected.connect(func(index: int) -> void:
|
||
GameState.ai_level = index
|
||
sync())
|
||
timed_toggle = CheckButton.new()
|
||
timed_toggle.name = "TimedBuild"
|
||
timed_toggle.text = "Bauzeit begrenzen (45 / 30 Sekunden)"
|
||
timed_toggle.focus_mode = Control.FOCUS_ALL
|
||
_advanced.add_child(timed_toggle)
|
||
timed_toggle.toggled.connect(func(value: bool) -> void:
|
||
GameState.timed_build = value
|
||
sync())
|
||
start_button = _button("PARTIE BEGINNEN ›", "PrimaryButton", layout, _on_start)
|
||
start_button.name = "StartMatch"
|
||
start_button.custom_minimum_size.y = 64
|
||
_timed_hint = _label("", "MutedLabel", layout)
|
||
_timed_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_timed_hint.add_theme_font_size_override("font_size", 15)
|
||
_setup_caption = _caption(_setup_screen, "")
|
||
|
||
# ---------------------------------------------------------------- state
|
||
func sync() -> void:
|
||
if faction_choice == null:
|
||
return
|
||
faction_choice.select(DataCatalog.FACTIONS.find(GameState.factions[0]))
|
||
rival_choice.select(DataCatalog.FACTIONS.find(GameState.factions[1]))
|
||
var full: bool = GameState.match_mode == GameState.MODE_EXPEDITION
|
||
mode_choice.select(0 if full else 1)
|
||
ai_choice.select(clampi(GameState.ai_level, 0, 1))
|
||
timed_toggle.set_pressed_no_signal(GameState.timed_build)
|
||
length_summary.text = "Zwanzig Wellen · vier Bosse · etwa 20 bis 30 Minuten" if full else "Acht Wellen · zwei Bosse · etwa 7 bis 10 Minuten"
|
||
_advanced_summary.text = "Rivale %s · %s · Bauzeit %s" % [DataCatalog.FACTION_NAMES[GameState.factions[1]],
|
||
"Lernmodus" if GameState.ai_level == GameState.AI_LEARNING else "Standard", "begrenzt" if GameState.timed_build else "frei"]
|
||
_timed_hint.text = "Bauphasen enden nach 45, danach je 30 Sekunden; der Kampf startet dann selbst." if GameState.timed_build else "Du startest jede Welle selbst mit der Leertaste."
|
||
var own: String = GameState.factions[0]
|
||
_style_heading.text = "SPIELWEISE · " + DataCatalog.FACTION_NAMES[own].to_upper()
|
||
var lines: PackedStringArray = []
|
||
for fact: String in FactionChoice.PROFILES[own].facts:
|
||
lines.append("• " + fact)
|
||
lines.append("• Startformation mit Tank, Nahkämpfer und Schütze für %d Gold" % FactionChoice.starter_cost(own))
|
||
_style_facts.text = "\n".join(lines)
|
||
var names: PackedStringArray = []
|
||
for entry: Array in Stage.SETUPS[GameState.factions[0]].figures:
|
||
names.append(str(DataCatalog.get_unit(entry[0]).display_name))
|
||
names.append("Saumkern")
|
||
_setup_caption.text = "IM BILD " + " · ".join(names)
|
||
if screen == Screen.SETUP and _active and stage.current_shot() != GameState.factions[0]:
|
||
stage.set_shot_named(GameState.factions[0], reduced_motion)
|
||
|
||
func is_transitioning() -> bool:
|
||
return _shield.visible
|
||
|
||
func _toggle_advanced() -> void:
|
||
_advanced.visible = not _advanced.visible
|
||
if _advanced.visible:
|
||
var scroll: ScrollContainer = _advanced.get_parent().get_parent().get_parent()
|
||
scroll.ensure_control_visible.call_deferred(_advanced)
|
||
_advanced_button.text = ("– Weniger Einstellungen" if _advanced.visible else "+ Weitere Einstellungen")
|
||
_advanced_summary.visible = not _advanced.visible
|
||
|
||
func _apply_preferences() -> void:
|
||
reduced_motion = Preferences.reduced_motion
|
||
stage.set_reduced_motion(reduced_motion)
|
||
|
||
func _resize_stage() -> void:
|
||
var pixels: Vector2i = get_tree().root.size
|
||
if pixels.x < 16 or pixels.y < 16:
|
||
pixels = Vector2i(get_viewport_rect().size)
|
||
stage_view.size = pixels
|
||
|
||
func _set_stage_active(on: bool) -> void:
|
||
_active = on
|
||
stage.process_mode = Node.PROCESS_MODE_INHERIT if on else Node.PROCESS_MODE_DISABLED
|
||
stage_view.render_target_update_mode = SubViewport.UPDATE_ALWAYS if on else SubViewport.UPDATE_DISABLED
|
||
# The arena is hidden behind the opaque stage image; skip rendering it meanwhile.
|
||
get_tree().root.disable_3d = on
|
||
|
||
func _process(delta: float) -> void:
|
||
if _shield_left > 0.0:
|
||
_shield_left -= delta
|
||
if _shield_left <= 0.0 and not _leaving:
|
||
_shield.visible = false
|
||
if _active:
|
||
var covered: bool = is_instance_valid(occluder) and occluder.visible
|
||
stage_view.render_target_update_mode = SubViewport.UPDATE_DISABLED if covered else SubViewport.UPDATE_ALWAYS
|
||
|
||
func _guard(seconds: float) -> void:
|
||
_shield.visible = true
|
||
_shield_left = maxf(_shield_left, seconds)
|
||
|
||
func _input(event: InputEvent) -> void:
|
||
# While a screen fades, clicks and confirm keys must not reach half-hidden controls.
|
||
if visible and _shield.visible and (event is InputEventMouseButton or event.is_action("ui_accept")):
|
||
get_viewport().set_input_as_handled()
|
||
|
||
func _unhandled_input(event: InputEvent) -> void:
|
||
if not visible or _shield.visible or not event.is_action_pressed("ui_cancel"):
|
||
return
|
||
if screen == Screen.SETUP:
|
||
show_screen(Screen.TITLE)
|
||
get_viewport().set_input_as_handled()
|
||
|
||
# ---------------------------------------------------------------- flow
|
||
func enter(game_over: bool = false) -> void:
|
||
if _fade != null:
|
||
_fade.kill()
|
||
_leaving = false
|
||
_locked = false
|
||
visible = true
|
||
modulate.a = 1.0
|
||
_set_stage_active(true)
|
||
_result_panel.visible = game_over
|
||
_change_button.visible = game_over
|
||
new_game_button.text = "NOCHMAL SPIELEN" if game_over else "NEUE PARTIE"
|
||
if game_over:
|
||
result_title.text = ["SIEG AM SAUM", "NIEDERLAGE AM SAUM"][GameState.outcome] if GameState.outcome >= 0 else "GLEICHSTAND AM SAUM"
|
||
result_body.text = "Welle %d von %d · Dein Kern %d HP · Rivalenkern %d HP" % [GameState.wave, DataCatalog.wave_count(), GameState.core_hp[0], GameState.core_hp[1]]
|
||
var target: int = Screen.TITLE if game_over else pending_screen
|
||
pending_screen = Screen.TITLE
|
||
screen = -1
|
||
sync()
|
||
show_screen(target, true)
|
||
|
||
func leave() -> void:
|
||
if not visible or _leaving:
|
||
return
|
||
_leaving = true
|
||
get_tree().root.disable_3d = false
|
||
if reduced_motion or DisplayServer.get_name() == "headless":
|
||
_finish_leave()
|
||
return
|
||
_guard(FADE + .1)
|
||
_fade = create_tween()
|
||
_fade.tween_property(self, "modulate:a", 0.0, FADE)
|
||
_fade.tween_callback(_finish_leave)
|
||
|
||
func _finish_leave() -> void:
|
||
visible = false
|
||
_shield.visible = false
|
||
_shield_left = 0.0
|
||
_leaving = false
|
||
modulate.a = 1.0
|
||
_set_stage_active(false)
|
||
|
||
func show_screen(next: int, instant: bool = false) -> void:
|
||
var from: Control = _title_screen if screen == Screen.TITLE else (_setup_screen if screen == Screen.SETUP else null)
|
||
var to: Control = _title_screen if next == Screen.TITLE else _setup_screen
|
||
screen = next
|
||
stage.set_shot_named("title" if next == Screen.TITLE else GameState.factions[0], instant or reduced_motion)
|
||
_title_scrim.visible = next == Screen.TITLE
|
||
_setup_scrim.visible = next == Screen.SETUP
|
||
if _fade != null and _fade.is_valid():
|
||
_fade.kill()
|
||
for node: Control in [_title_screen, _setup_screen]:
|
||
if node != to and node != from:
|
||
node.visible = false
|
||
if instant or reduced_motion or from == null or from == to:
|
||
if from != null and from != to:
|
||
from.visible = false
|
||
to.visible = true
|
||
to.modulate.a = 1.0
|
||
_focus_default.call_deferred()
|
||
return
|
||
_guard(FADE + .05)
|
||
to.visible = true
|
||
to.modulate.a = 0.0
|
||
_fade = create_tween()
|
||
_fade.tween_property(from, "modulate:a", 0.0, FADE * .6)
|
||
_fade.parallel().tween_property(to, "modulate:a", 1.0, FADE).set_delay(FADE * .25)
|
||
_fade.tween_callback(func() -> void:
|
||
from.visible = false
|
||
from.modulate.a = 1.0)
|
||
_focus_default.call_deferred()
|
||
|
||
func _focus_default() -> void:
|
||
if not is_visible_in_tree():
|
||
return
|
||
if screen == Screen.SETUP:
|
||
faction_choice.focus_selected()
|
||
else:
|
||
new_game_button.grab_focus()
|
||
|
||
func _on_title_primary() -> void:
|
||
if GameState.phase == GameState.Phase.GAME_OVER:
|
||
_on_start()
|
||
else:
|
||
show_screen(Screen.SETUP)
|
||
|
||
func _on_start() -> void:
|
||
# One start per menu visit; a second click during the fade lands on the shield.
|
||
if _locked:
|
||
return
|
||
_locked = true
|
||
start_requested.emit()
|