Hauptszene scenes/luneraria.tscn, Saumwacht bleibt über --legacy und die alten QA-Schalter erreichbar. Karte aus Bestandsassets plus neuem Turm, HUD mit Planung, Lanes und Minimap, Menü umbenannt, Einstellungen werden aus dem alten Benutzerordner übernommen. Simulation 6,6x schneller (Raster, Caches, faire Staffelung) und exakt spiegelsymmetrisch. Regeln 91/91, echte Anwendung 38/38, Legacy unverändert grün. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
532 lines
21 KiB
GDScript
532 lines
21 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
|
||
## Exposed so a derived menu (Champions of Luneraria) can retitle the shell.
|
||
var title_label: Label
|
||
var subtitle_label: Label
|
||
var lore_label: Label
|
||
var emblem_rect: TextureRect
|
||
var version_label: Label
|
||
var brand_names: VBoxContainer
|
||
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_rect = _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)
|
||
brand_names = names
|
||
title_label = _label("SAUMWACHT", "TitleLabel", names)
|
||
subtitle_label = _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_label = lore
|
||
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_label = version
|
||
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 _setup_shot(), 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()
|
||
|
||
## Stage shot for the setup screen; derived menus pick their own Bund source.
|
||
func _setup_shot() -> String:
|
||
return GameState.factions[0]
|
||
|
||
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()
|