extends Control ## Presentation only: emits player commands and reads snapshots. signal command_requested(command: Dictionary) signal build_selected(type_id: String) signal move_requested signal base_requested signal focus_requested signal menu_requested signal new_match_requested signal starter_requested signal cancel_requested signal ready_requested const INK := Color("#0b1016") const PANEL := Color("#101a22") const TEXT := Color("#f2f6f4") const MUTED := Color("#9bb0aa") const GOLD := Color("#f0d090") const GOLD_DEEP := Color("#c9a05a") const TEAL := Color("#7fe8c8") const TEAL_DEEP := Color("#2a8f7a") const CORAL := Color("#f0a888") const VIOLET := Color("#b8a0e8") var selected_uid: int = -1 var build_type: String = "" var viewed_team: int = 0 var model_inspector: ColorRect var _inspect_model: Button var settings_panel: ColorRect var _settings_paused: bool = false var _settings_focus: Control var _volume_slider: HSlider var _mute_check: CheckButton var _follow_check: CheckButton var _fullscreen_check: CheckButton var _camera_status: Label var _boss_bar: ProgressBar var _boss_label: Label var _phase: Label var _timer: Label var _gold: Label var _sparks: Label var _income: Label var _workers: Label var _core_labels: Array = [] var _core_bars: Array = [] var _wave_title: Label var _wave_body: Label var _selection_icon: TextureRect var _selection_types: HBoxContainer var _selection_title: Label var _selection_body: Label var _upgrade: Button var _sell: Button var _move: Button var _priority: OptionButton var _worker_button: Button var _ready_button: Button var _pause_button: Button var _speed_button: Button var _front_button: Button var _undo: Button var _starter_button: Button var _cards: HBoxContainer var _shop_faction: String = "" var _faction_label: Label var _worker_info: Label var _step_hint: Label var _faction_pickers: Array = [] var _timed_picker: CheckButton var _shop_buttons: Dictionary = {} var _send_picker: OptionButton var _send_button: Button var _cancel_send: Button var _send_info: Label var _toast: Label var _toast_left: float = 0.0 ## Phase 20A: the title menu and match setup live in MainMenu. These names remain the ## entry points used by tools and tests; they point at OptionButton-compatible parts. var main_menu: MainMenu var _overlay: Control var _overlay_title: Label var _overlay_action: Button var _ai_picker: ChoiceGroup var _mode_picker: ChoiceGroup var _reduced_check: CheckButton var _summary: Label var _queue_scroll: ScrollContainer var _queue_row: HBoxContainer var _queue_signature: String = "@" var _refresh_left: float = 0.0 var _shop_panel: PanelContainer var _wave_progress: ProgressBar var _context_panel: PanelContainer var _context_column: VBoxContainer var _game_panels: Array[Control] = [] var _logo_label: Label const SEND_SHORT_NAMES: Dictionary = { "s01": "Späher", "s02": "Rost", "s03": "Dunst", "s04": "Keil", "s05": "Riss"} const MATRIX_TOOLTIP: String = "Schadensmatrix (Zeile → Spalte):\nKlinge: 130 % gegen Leicht · 75 % gegen Schwer\nDurchschlag: 130 % gegen Schwer · 75 % gegen Ätherisch\nImpuls: 130 % gegen Ätherisch · 75 % gegen Leicht" func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE theme = _make_theme() _make_header() _make_sidebar() _make_footer() _make_overlay() _make_settings() model_inspector = preload("res://scripts/ui/model_inspector.gd").new() add_child(model_inspector) GameState.phase_changed.connect(_phase_changed) WaveManager.wave_settled.connect(_show_summary) _phase_changed(GameState.phase) func _style(color: Color, border: Color = Color.TRANSPARENT, width: int = 0) -> StyleBoxFlat: var box := StyleBoxFlat.new() box.bg_color = color box.border_color = border box.set_border_width_all(width) box.set_corner_radius_all(14) box.anti_aliasing = true box.content_margin_left = 16 box.content_margin_right = 16 box.content_margin_top = 10 box.content_margin_bottom = 10 return box func _glass(bg: Color = Color(.05, .09, .12, .88), border: Color = Color(.45, .85, .75, .22)) -> StyleBoxFlat: var box := _style(bg, border, 1) box.set_corner_radius_all(16) box.shadow_color = Color(0, 0, 0, .45) box.shadow_size = 14 box.shadow_offset = Vector2(0, 6) box.content_margin_left = 18 box.content_margin_right = 18 box.content_margin_top = 12 box.content_margin_bottom = 12 return box func _gold_button() -> StyleBoxFlat: var box := _style(Color("#e8c57a"), Color("#ffe2a8"), 1) box.set_corner_radius_all(12) box.shadow_color = Color(.95, .75, .35, .35) box.shadow_size = 16 box.shadow_offset = Vector2(0, 2) box.content_margin_top = 12 box.content_margin_bottom = 12 return box func _make_theme() -> Theme: var result := Theme.new() result.default_font_size = 15 var modern_font := FontVariation.new() modern_font.base_font = preload("res://assets/fonts/Cantarell-VF.otf") modern_font.variation_opentype = {"wght": 550.0} result.default_font = modern_font result.set_color("font_color", "Label", TEXT) result.set_color("font_color", "Button", TEXT) result.set_color("font_hover_color", "Button", Color.WHITE) result.set_color("font_disabled_color", "Button", Color("#6a807c")) result.set_stylebox("normal", "Button", _style(Color("#1c2c36"), Color("#3d5a58"), 1)) result.set_stylebox("hover", "Button", _style(Color("#284852"), TEAL, 1)) result.set_stylebox("pressed", "Button", _style(Color("#1f3c40"), TEAL_DEEP, 2)) result.set_stylebox("focus", "Button", _style(Color(0, 0, 0, 0), GOLD, 2)) result.set_stylebox("disabled", "Button", _style(Color("#152028"))) result.set_stylebox("normal", "PanelContainer", _glass()) result.set_constant("separation", "VBoxContainer", 10) result.set_constant("separation", "HBoxContainer", 12) result.set_color("font_color", "OptionButton", TEXT) result.set_stylebox("normal", "OptionButton", _style(Color("#1c2c36"), Color("#3d5a58"), 1)) result.set_stylebox("hover", "OptionButton", _style(Color("#284852"), TEAL, 1)) result.set_stylebox("focus", "OptionButton", _style(Color("#284852"), GOLD, 1)) var popup := _glass(Color(.06, .10, .14, .97), Color(.4, .7, .65, .3)) result.set_stylebox("panel", "PopupMenu", popup) result.set_color("font_color", "PopupMenu", TEXT) result.set_color("font_hover_color", "PopupMenu", GOLD) return result func _label(text_value: String, size: int, parent: Node, color: Color = TEXT) -> Label: var label := Label.new() label.text = text_value label.add_theme_font_size_override("font_size", size) label.add_theme_color_override("font_color", color) label.mouse_filter = Control.MOUSE_FILTER_IGNORE parent.add_child(label) return label func _button(text_value: String, action: Callable, parent: Node) -> Button: var button := Button.new() button.text = text_value button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button.pressed.connect(action) parent.add_child(button) return button func _panel(parent: Node) -> PanelContainer: var panel := PanelContainer.new() parent.add_child(panel) return panel func _make_header() -> void: var header := _panel(self) _game_panels.append(header) header.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) header.offset_left = 18 header.offset_top = 16 header.offset_right = -18 header.offset_bottom = 92 var row := HBoxContainer.new() row.add_theme_constant_override("separation", 18) header.add_child(row) var title := VBoxContainer.new() title.custom_minimum_size.x = 185 title.size_flags_vertical = Control.SIZE_SHRINK_CENTER row.add_child(title) var logo := _label("S A U M W A C H T", 22, title, Color("#f6e6b8")) _logo_label = logo _faction_label = _label("", 11, title, TEAL) var phase_box := VBoxContainer.new() phase_box.custom_minimum_size.x = 164 phase_box.size_flags_vertical = Control.SIZE_SHRINK_CENTER row.add_child(phase_box) _phase = _label("VORBEREITUNG", 11, phase_box, TEAL) _timer = _label("", 22, phase_box) _wave_progress = ProgressBar.new() _wave_progress.custom_minimum_size.y = 3 _wave_progress.show_percentage = false var progress_fill := _style(TEAL) var progress_back := _style(Color("2c403e")) for style: StyleBoxFlat in [progress_fill, progress_back]: style.content_margin_top = 0 style.content_margin_bottom = 0 _wave_progress.add_theme_stylebox_override("fill", progress_fill) _wave_progress.add_theme_stylebox_override("background", progress_back) phase_box.add_child(_wave_progress) _gold = _metric(row, "GOLD", GOLD, "res_gold") _sparks = _metric(row, "FUNKEN", TEAL, "res_sparks") _income = _metric(row, "PRO WELLE", GOLD, "res_income") _workers = _metric(row, "ARBEITER", TEXT, "worker") var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(spacer) var controls := VBoxContainer.new() controls.add_theme_constant_override("separation", 3) row.add_child(controls) var buttons := HBoxContainer.new() buttons.add_theme_constant_override("separation", 6) controls.add_child(buttons) _pause_button = _button("Pause [P]", toggle_pause, buttons) _speed_button = _button("1×", _toggle_speed, buttons) _button("Optionen", toggle_settings, buttons).tooltip_text = "Einstellungen & Steuerung [F1]" _front_button = _button("Rivalen beobachten [Tab]", func(): focus_requested.emit(), controls) for button: Node in buttons.get_children(): button.add_theme_font_size_override("font_size", 12) _front_button.add_theme_font_size_override("font_size", 12) func _metric(parent: Node, caption: String, color: Color, icon_id: String) -> Label: var panel := PanelContainer.new() panel.add_theme_stylebox_override("panel", _glass(Color(.08, .13, .16, .55), Color(.4, .7, .65, .14))) panel.custom_minimum_size.x = 118 parent.add_child(panel) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 9) panel.add_child(row) _icon(icon_id, row, Vector2(24,24)) var column := VBoxContainer.new() column.add_theme_constant_override("separation", 0) row.add_child(column) _label(caption, 10, column, MUTED) return _label("—", 24, column, color) func _make_sidebar() -> void: var guide := _panel(self) guide.position = Vector2(22, 110) guide.add_theme_stylebox_override("panel", _style(Color(.05,.08,.09,.86))) var location := VBoxContainer.new() guide.add_child(location) _game_panels.append(guide) _label("DIE GRÜNE KLUFT / DEINE FRONT", 11, location, TEAL) _step_hint = _label("", 16, location) _camera_status = _label("Q / E Drehen · R Zurücksetzen · F1 Hilfe", 12, location, MUTED) _boss_label = _label("", 17, location, CORAL) _boss_bar = ProgressBar.new() _boss_bar.custom_minimum_size = Vector2(285, 13) _boss_bar.show_percentage = false _boss_bar.add_theme_stylebox_override("background", _style(Color("#101d25"))) _boss_bar.add_theme_stylebox_override("fill", _style(CORAL)) location.add_child(_boss_bar) _boss_bar.visible = false var sidebar := _panel(self) _context_panel = sidebar _game_panels.append(sidebar) sidebar.anchor_left = 1.0 sidebar.anchor_right = 1.0 sidebar.anchor_bottom = 0.0 sidebar.offset_left = -348 sidebar.offset_right = -20 sidebar.offset_top = 108 sidebar.offset_bottom = 504 var scroll := ScrollContainer.new() scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED sidebar.add_child(scroll) var column := VBoxContainer.new() column.size_flags_horizontal = Control.SIZE_EXPAND_FILL scroll.add_child(column) _context_column = column column.add_theme_constant_override("separation", 6) _ready_button = _button("KAMPF STARTEN [Leertaste]", request_wave_start, column) _ready_button.custom_minimum_size.y = 52 _ready_button.add_theme_font_size_override("font_size", 15) _ready_button.add_theme_stylebox_override("normal", _gold_button()) _ready_button.add_theme_stylebox_override("hover", _gold_button()) _ready_button.add_theme_stylebox_override("pressed", _gold_button()) _ready_button.add_theme_color_override("font_color", INK) _ready_button.add_theme_color_override("font_hover_color", INK) for team in range(2): var color := TEAL if team == 0 else CORAL var caption := _label("DEIN KERN" if team == 0 else "RIVALENKERN", 13, column, color) _core_labels.append(caption) var bar := ProgressBar.new() bar.max_value = 3000 bar.value = 3000 bar.show_percentage = false bar.custom_minimum_size.y = 7 var background := _style(Color("#101d25")) var fill := _style(color) for style: StyleBoxFlat in [background, fill]: style.content_margin_top = 0 style.content_margin_bottom = 0 bar.add_theme_stylebox_override("background", background) bar.add_theme_stylebox_override("fill", fill) column.add_child(bar) _core_bars.append(bar) column.add_child(HSeparator.new()) _wave_title = _label("NÄCHSTE WELLE", 13, column, GOLD) _wave_title.mouse_filter = Control.MOUSE_FILTER_STOP _wave_body = _label("", 14, column, MUTED) _wave_body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _wave_body.custom_minimum_size.x = 278 column.add_child(HSeparator.new()) var selected_row := HBoxContainer.new() column.add_child(selected_row) _selection_icon = _icon("", selected_row, Vector2(64,64)) _selection_icon.visible = false _selection_title = _label("DEINE FORMATION", 18, selected_row) _selection_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL _selection_types = HBoxContainer.new() column.add_child(_selection_types) _selection_title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _selection_body = _label("", 14, column, MUTED) _selection_body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _selection_body.custom_minimum_size = Vector2(278, 45) _upgrade = _button("Aufwerten", _upgrade_selected, column) var actions := HBoxContainer.new() column.add_child(actions) _move = _button("Versetzen", func(): move_requested.emit(), actions) _sell = _button("Verkaufen", func(): _emit_selected("sell"), actions) _priority = OptionButton.new() for title in ["Nächstes Ziel", "Niedrigste HP", "Höchste maximale HP"]: _priority.add_item(title) _priority.item_selected.connect(func(index: int): command_requested.emit({"type": "target", "uid": selected_uid, "policy": index})) column.add_child(_priority) _inspect_model = _button("3D-Modell ansehen [V]", inspect_selected_model, column) _summary = _label("", 13, column, TEAL) _summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART func _make_footer() -> void: var footer := _panel(self) _shop_panel = footer _game_panels.append(footer) footer.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_WIDE) footer.offset_left = 16 footer.offset_right = -364 footer.offset_top = -212 footer.offset_bottom = -18 var shop := VBoxContainer.new() shop.add_theme_constant_override("separation", 7) footer.add_child(shop) var heading := HBoxContainer.new() shop.add_child(heading) var title := _label("WÄCHTER / WÄHLEN & PLATZIEREN", 12, heading, TEAL) title.size_flags_horizontal = Control.SIZE_EXPAND_FILL _starter_button = _button("Startformation", func(): starter_requested.emit(), heading) _starter_button.tooltip_text = "Stellt drei bezahlte Wächter auf: Tank und Nahkämpfer vorn, Schütze dahinter." _undo = _button("↶ Rückgängig", func(): command_requested.emit({"type": "undo"}), heading) _cards = HBoxContainer.new() _cards.add_theme_constant_override("separation", 8) shop.add_child(_cards) _rebuild_shop() _toast = _label("", 13, shop, MUTED) _toast.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART var economy := _panel(self) _game_panels.append(economy) economy.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT) economy.offset_left = -348 economy.offset_right = -20 economy.offset_top = -300 economy.offset_bottom = -18 var column := VBoxContainer.new() column.add_theme_constant_override("separation", 5) economy.add_child(column) var row := HBoxContainer.new() column.add_child(row) _label("BASIS & NACHschub".to_upper(), 12, row, TEAL).size_flags_horizontal = Control.SIZE_EXPAND_FILL _button("Ansehen [C]", func(): base_requested.emit(), row) _worker_button = _button("Arbeiter ausbilden", func(): command_requested.emit({"type": "worker"}), column) _worker_button.icon = AssetLibrary.icon("worker") _worker_button.add_theme_constant_override("icon_max_width", 25) _worker_info = _label("", 12, column, MUTED) _worker_info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART column.add_child(HSeparator.new()) _send_picker = OptionButton.new() for send_id in ["s01", "s02", "s03", "s04", "s05"]: var data: Dictionary = DataCatalog.get_send(send_id, 1) _send_picker.add_icon_item(AssetLibrary.icon(send_id), "%s · %d F · +%d G/Welle" % [data.display_name, data.cost, data.income_gain]) _send_picker.add_theme_constant_override("icon_max_width", 22) _send_picker.get_popup().add_theme_constant_override("icon_max_width", 22) column.add_child(_send_picker) var send_row := HBoxContainer.new() column.add_child(send_row) _send_button = _button("Zum Rivalen senden", _queue_send, send_row) _cancel_send = _button("↶", _cancel_last_send, send_row) _cancel_send.tooltip_text = "Letzten Söldner entfernen; Funken zurückgeben" _send_info = _label("", 12, column, MUTED) _queue_scroll = ScrollContainer.new() _queue_scroll.custom_minimum_size.y = 36 _queue_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED column.add_child(_queue_scroll) _queue_row = HBoxContainer.new() _queue_row.add_theme_constant_override("separation", 4) _queue_scroll.add_child(_queue_row) func _rebuild_shop() -> void: if _shop_faction == GameState.factions[0]: return _shop_faction = GameState.factions[0] _shop_buttons.clear() for child: Node in _cards.get_children(): child.free() var index: int = 0 for type_id: String in DataCatalog.base_ids(): var data: Dictionary = DataCatalog.get_unit(type_id) var button := _button("", func(): build_selected.emit(type_id), _cards) button.custom_minimum_size = Vector2(0, 121) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL var content := VBoxContainer.new() content.mouse_filter = Control.MOUSE_FILTER_IGNORE content.add_theme_constant_override("separation", 1) content.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) content.offset_left = 10 content.offset_right = -10 content.offset_top = 4 content.offset_bottom = -4 button.add_child(content) var top := HBoxContainer.new() top.mouse_filter = Control.MOUSE_FILTER_IGNORE content.add_child(top) _icon(type_id, top, Vector2(68, 68)) var price := VBoxContainer.new() price.mouse_filter = Control.MOUSE_FILTER_IGNORE top.add_child(price) _label("%d G" % data.cost, 20, price, GOLD) _label("[ %d ]" % (index+1), 12, price, MUTED) _label(data.display_name, 15, content).horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _label(DataCatalog.ROLES[index], 12, content, TEAL).horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER button.tooltip_text = "%s · %s %d HP · %d Schaden/s · %.1f m %s" % [DataCatalog.DAMAGE_NAMES[data.attack_type], DataCatalog.ARMOR_NAMES[data.armor_type], data.max_hp, data.damage, data.attack_range, data.ability_description] _shop_buttons[type_id] = button index += 1 func _make_overlay() -> void: main_menu = MainMenu.new() add_child(main_menu) _overlay = main_menu _overlay_action = main_menu.start_button _overlay_title = main_menu.length_summary _faction_pickers = [main_menu.faction_choice, main_menu.rival_choice] _mode_picker = main_menu.mode_choice _ai_picker = main_menu.ai_choice _timed_picker = main_menu.timed_toggle main_menu.start_requested.connect(func() -> void: new_match_requested.emit()) main_menu.settings_requested.connect(toggle_settings) main_menu.models_requested.connect(inspect_selected_model) main_menu.quit_requested.connect(func() -> void: Soundscape.quit_game()) main_menu.selection_changed.connect(_rebuild_shop) func _process(delta: float) -> void: _toast_left -= delta _refresh_left -= delta if _refresh_left <= 0: _refresh_left = 0.12 _refresh() func _refresh() -> void: var team: Dictionary = EconomyManager.get_team(0) if team.is_empty(): return _rebuild_shop() if GameState.phase in [GameState.Phase.MENU, GameState.Phase.GAME_OVER] and is_instance_valid(main_menu): # Match options can also change outside the menu (tools, tests); keep the display honest. main_menu.sync() _faction_label.text = DataCatalog.FACTION_NAMES[GameState.factions[0]].to_upper() + " / " + ("DEINE FRONT" if viewed_team == 0 else "RIVALENFRONT") var building := GameState.phase == GameState.Phase.BUILD _wave_progress.max_value = DataCatalog.wave_count() _wave_progress.value = maxi(0, GameState.wave - 1) _phase.text = GameState.phase_name() + (" · PAUSIERT" if GameState.paused else "") _timer.text = "%02d / %02d · %02d s" % [maxi(1, GameState.wave), DataCatalog.wave_count(), ceili(WaveManager.remaining)] if not GameState.timed_build and GameState.phase in [GameState.Phase.BUILD, GameState.Phase.RESULTS]: _timer.text = "Welle %02d / %02d" % [maxi(1, GameState.wave), DataCatalog.wave_count()] _gold.text = str(team.gold) _sparks.text = "%d" % (int(team.sparks) - EconomyManager.reserved_sparks(0)) _sparks.tooltip_text = "Freie Funken. Vorgemerkte Söldner sind bereits abgezogen." _income.text = "+%d" % team.income _workers.text = "%d%s" % [team.workers, (" +%d" % team.pending_workers) if team.pending_workers else ""] _workers.tooltip_text = "Aktive Arbeiter + bestellte Arbeiter. Je 10 Funken zu Beginn der nächsten Bauphase." for side in range(2): _core_labels[side].text = "%s %d / 3000" % [ "DEIN KERN" if side == 0 else "RIVALENKERN", GameState.core_hp[side]] _core_bars[side].value = GameState.core_hp[side] _pause_button.text = "Fortsetzen [P]" if GameState.paused else "Pause [P]" _speed_button.text = "%d×" % GameState.speed _front_button.text = "Eigene Front [Tab]" if viewed_team else "Rivalen beobachten [Tab]" var w: int = maxi(1, GameState.wave) var wave_data: Dictionary = DataCatalog.get_wave(w) _wave_title.text = "%02d · %s" % [w, str(wave_data.get("title", "Welle"))] _wave_body.text = str(wave_data.get("description", "")) + "\n\n" + DataCatalog.wave_hint(w) _wave_title.tooltip_text = _wave_body.text for type_id in _shop_buttons: var data: Dictionary = DataCatalog.get_unit(type_id) var button: Button = _shop_buttons[type_id] button.disabled = not building or team.gold < data.cost button.modulate = Color.WHITE if build_type == type_id: button.add_theme_stylebox_override("normal", _style(Color("2a5048"), TEAL, 2)) else: var card := _style(Color("1a2c34"), Color("3a5558"), 1) card.set_corner_radius_all(16) card.shadow_color = Color(0, 0, 0, .3) card.shadow_size = 6 button.add_theme_stylebox_override("normal", card) _starter_button.disabled = not building or not team.roster.is_empty() or team.gold < DataCatalog.starter_cost() _starter_button.text = "Startformation · %d G" % DataCatalog.starter_cost() _undo.disabled = not building _worker_button.text = "Arbeiter ausbilden · %d Gold" % EconomyManager.worker_cost(0) _worker_button.disabled = not building or w >= DataCatalog.wave_count() or team.workers + team.pending_workers >= 8 or team.gold < EconomyManager.worker_cost(0) _worker_info.text = "%d / 8 aktiv%s · +%d Funken / Welle\nNeue Arbeiter produzieren ab Welle %d." % [team.workers, " (+%d bestellt)" % team.pending_workers if team.pending_workers > 0 else "", int(team.workers) * 10, w+1] if w >= DataCatalog.wave_count(): _worker_info.text = "Letzte Welle · Gold jetzt in Wächter investieren." var send_cost: int = int(DataCatalog.SENDS["s%02d" % (_send_picker.selected + 1)].cost) _send_button.disabled = not building or w < 3 or team.send_queue.size() >= 8 or mini(int(team.sparks), EconomyManager.send_budget(w)) - EconomyManager.reserved_sparks(0) < send_cost _cancel_send.disabled = not building or team.send_queue.is_empty() _send_picker.disabled = not building or w < 3 _send_info.text = "%d / 8 geplant · %d / %d F Budget" % [ team.send_queue.size(), EconomyManager.reserved_sparks(0), EconomyManager.send_budget(w)] if w < 3: _send_info.text = "Söldner verfügbar ab Welle 3" _refresh_send_queue(team) _ready_button.disabled = not building and GameState.phase != GameState.Phase.RESULTS _ready_button.text = "NÄCHSTE WELLE [Leertaste]" if GameState.phase == GameState.Phase.RESULTS else ("KAMPF LÄUFT …" if GameState.phase == GameState.Phase.COMBAT else "KAMPF STARTEN [Leertaste]") _step_hint.text = ("Erst Startformation setzen oder Wächter wählen." if team.roster.is_empty() else ("Platzieren → Aufwerten → Kampf starten" if build_type.is_empty() else "Freies Feld anklicken · Rechtsklick bricht ab")) if building else ("Welle beendet · Abrechnung rechts" if GameState.phase == GameState.Phase.RESULTS else "Deine Formation kämpft automatisch") _refresh_selection(team, building) if is_instance_valid(_context_column): _context_panel.size.y = minf(_context_column.get_combined_minimum_size().y + 24, size.y - 422) if _toast_left <= 0: _toast.text = "1–5 Bauen · Linksklick Auswählen · Rechtsklick Abbrechen · Mausrad Zoom" _toast.add_theme_color_override("font_color", MUTED) func _refresh_selection(team: Dictionary, building: bool) -> void: var selected: Dictionary = {} for entry in team.roster: if entry.uid == selected_uid: selected = entry break var has_selection := not selected.is_empty() _wave_title.visible = not has_selection _summary.visible = not has_selection _wave_body.visible = not has_selection _inspect_model.visible = has_selection _upgrade.visible = has_selection _move.visible = has_selection _sell.visible = has_selection _priority.visible = has_selection if not has_selection: _selection_icon.visible = not build_type.is_empty() _selection_icon.texture = AssetLibrary.icon(build_type) _selection_types.visible = false _selection_title.text = "DEINE FORMATION" _selection_body.text = "%d / 48 Plätze · Tank vorn, Schützen dahinter.\nKlicke einen Wächter an, um ihn aufzuwerten." % team.roster.size() if not build_type.is_empty(): var candidate: Dictionary = DataCatalog.get_unit(build_type) _selection_title.text = candidate.display_name _selection_body.text = "%d Gold · %d HP · %d Schaden/s\n%s\n\nLinksklick setzt einen Wächter. Shift setzt mehrere." % [candidate.cost, candidate.max_hp, candidate.damage, candidate.ability_description] return var data: Dictionary = DataCatalog.get_unit(selected.type_id) _selection_icon.visible = true _selection_icon.texture = AssetLibrary.icon(str(data.id)) _selection_types.visible = true _refresh_type_icons(data) _selection_title.text = data.display_name _selection_body.text = "%d HP · %d Grund-DPS · %.1f m\n\n%s" % [ data.max_hp, data.damage, data.attack_range, str(data.get("ability_description", ""))] _selection_body.tooltip_text = "%s · %s" % [data.display_name, MATRIX_TOOLTIP] var upgrade_id: String = str(data.get("upgrade_id", "")) var upgraded: Dictionary = DataCatalog.get_unit(upgrade_id) if not upgrade_id.is_empty() else {} _upgrade.disabled = not building or upgraded.is_empty() or team.gold < int(upgraded.get("cost", 0)) _upgrade.text = "Vollständig aufgewertet" if upgraded.is_empty() else "AUFWERTEN · %d GOLD\n%s" % [upgraded.cost, upgraded.display_name] _upgrade.add_theme_font_size_override("font_size", 13) _upgrade.tooltip_text = "" if upgraded.is_empty() else "%d → %d HP · %d → %d Schaden/s\n%s%s" % [data.max_hp, upgraded.max_hp, data.damage, upgraded.damage, upgraded.ability_description, "\nEs fehlen %d Gold." % (int(upgraded.cost)-int(team.gold)) if team.gold < int(upgraded.cost) else ""] _sell.text = "Verkaufen · %d G" % floori(selected.invested * 0.75) _sell.disabled = not building _move.disabled = not building _priority.disabled = not building _priority.select(selected.target_policy) func _refresh_send_queue(team: Dictionary) -> void: var signature: String = ",".join(PackedStringArray(team.send_queue)) if signature == _queue_signature: return _queue_signature = signature for child in _queue_row.get_children(): child.free() var index := 0 for send_id: String in team.send_queue: var chip := _button(SEND_SHORT_NAMES.get(send_id, send_id), func(): command_requested.emit({"type": "cancel_send", "index": index}), _queue_row) chip.add_theme_font_size_override("font_size", 11) for state in ["normal", "hover", "pressed"]: var compact := _style(Color("#26343b"), Color("#40544f") if state == "normal" else TEAL, 1) compact.content_margin_left = 8 compact.content_margin_right = 8 compact.content_margin_top = 3 compact.content_margin_bottom = 3 chip.add_theme_stylebox_override(state, compact) chip.tooltip_text = "%s entfernen · gibt die Funken vollständig frei" % DataCatalog.SENDS[send_id]["display_name"] index += 1 _queue_scroll.visible = not team.send_queue.is_empty() func _phase_changed(phase: int) -> void: for panel: Control in _game_panels: panel.visible = phase not in [GameState.Phase.MENU, GameState.Phase.GAME_OVER] var fighting: bool = phase == GameState.Phase.COMBAT _cards.visible = not fighting _starter_button.visible = not fighting _undo.visible = not fighting _shop_panel.offset_top = -84 if fighting else -212 if phase in [GameState.Phase.MENU, GameState.Phase.GAME_OVER]: main_menu.occluder = model_inspector main_menu.enter(phase == GameState.Phase.GAME_OVER) else: main_menu.leave() if phase == GameState.Phase.BUILD: if GameState.wave == 1: _summary.text = "" _queue_signature = "@" notify("Bauphase: neue Wächter, Aufwertungen und Arbeiter sind verfügbar.") elif phase == GameState.Phase.COMBAT: notify("Die Formation kämpft automatisch. Deine Kämpfer kehren zur nächsten Welle zurück.") _refresh() func _show_summary(reports: Array) -> void: var report: Dictionary = reports[0] _summary.text = "LETZTE ABRECHNUNG\n+%d Gold: %d Grundbetrag + %d Einkommen + %d Prämien\n%d Lecks · %d Kernschaden" % [ report.get("total_gold", 0), report.get("base_gold", 0), report.get("income", 0), report.get("bounty", 0), report.get("leaks", 0), report.get("core_damage", 0)] func notify(message: String, is_error: bool = false) -> void: _toast.text = message _toast.add_theme_color_override("font_color", CORAL if is_error else TEAL) _toast_left = 6.0 func toggle_pause() -> void: if GameState.phase in [GameState.Phase.MENU, GameState.Phase.GAME_OVER]: return GameState.paused = not GameState.paused if GameState.paused: Soundscape.stop_combat() _refresh() func _toggle_speed() -> void: GameState.speed = 2 if GameState.speed == 1 else 1 _refresh() func _emit_selected(type: String) -> void: command_requested.emit({"type": type, "uid": selected_uid}) func _upgrade_selected() -> void: _emit_selected("upgrade") func _queue_send() -> void: command_requested.emit({"type": "queue_send", "type_id": "s%02d" % (_send_picker.selected + 1)}) func _cancel_last_send() -> void: var queue: Array = EconomyManager.get_team(0).send_queue if not queue.is_empty(): command_requested.emit({"type": "cancel_send", "index": queue.size() - 1}) func _icon(id: String, parent: Node, minimum: Vector2) -> TextureRect: var image := TextureRect.new() image.texture = AssetLibrary.icon(id) image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED image.custom_minimum_size = minimum image.mouse_filter = Control.MOUSE_FILTER_IGNORE parent.add_child(image) return image func _refresh_type_icons(data: Dictionary) -> void: var key: String = "%s:%s" % [data.attack_type, data.armor_type] if _selection_types.get_meta("key", "") == key: return _selection_types.set_meta("key", key) for child: Node in _selection_types.get_children(): child.free() _icon(["type_damage_blade","type_damage_pierce","type_damage_pulse"][int(data.attack_type)], _selection_types, Vector2(25,25)) _label(DataCatalog.DAMAGE_NAMES[data.attack_type],12,_selection_types,GOLD) _icon(["type_armor_light","type_armor_heavy","type_armor_aether"][int(data.armor_type)], _selection_types, Vector2(25,25)) _label(DataCatalog.ARMOR_NAMES[data.armor_type],12,_selection_types,MUTED) func _make_settings() -> void: settings_panel = ColorRect.new() settings_panel.color = Color(.02, .03, .04, .9) settings_panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) # Reached from the title menu as well as in play: it shares the menu theme. settings_panel.theme = SaumwachtTheme.shared() add_child(settings_panel) var center := CenterContainer.new() center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) settings_panel.add_child(center) var panel := PanelContainer.new() panel.theme_type_variation = "MenuPanel" panel.custom_minimum_size.x = 760 center.add_child(panel) var column := VBoxContainer.new() column.add_theme_constant_override("separation", 12) panel.add_child(column) var heading := _themed_label("Optionen & Steuerung", "HeadingLabel", column) heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _themed_label("Eine laufende Partie pausiert, solange dieses Fenster geöffnet ist.", "MutedLabel", column).horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _themed_label("LAUTSTÄRKE", "SectionLabel", column) _volume_slider = HSlider.new() _volume_slider.max_value = 100 _volume_slider.step = 1 _volume_slider.custom_minimum_size.y = 28 column.add_child(_volume_slider) _volume_slider.value_changed.connect(func(value: float): Preferences.volume = value / 100.0 Preferences.commit()) _volume_slider.drag_ended.connect(func(_changed: bool): Soundscape.play("place")) var toggles := GridContainer.new() toggles.columns = 2 toggles.add_theme_constant_override("h_separation", 28) toggles.add_theme_constant_override("v_separation", 4) column.add_child(toggles) _mute_check = CheckButton.new() _mute_check.text = "Ton stumm [M]" toggles.add_child(_mute_check) _mute_check.toggled.connect(func(value: bool): Preferences.muted = value Preferences.commit()) _follow_check = CheckButton.new() _follow_check.text = "Kamera folgt dem Kampf" toggles.add_child(_follow_check) _follow_check.toggled.connect(func(value: bool): Preferences.follow_camera = value Preferences.commit()) _fullscreen_check = CheckButton.new() _fullscreen_check.text = "Vollbild [F11]" toggles.add_child(_fullscreen_check) _fullscreen_check.toggled.connect(func(value: bool): Preferences.fullscreen = value Preferences.commit()) _reduced_check = CheckButton.new() _reduced_check.text = "Bewegung reduzieren" _reduced_check.tooltip_text = "Menü ohne Kamerafahrten, Kameraatmen, Partikel und Überblendungen." toggles.add_child(_reduced_check) _reduced_check.toggled.connect(func(value: bool): Preferences.reduced_motion = value Preferences.commit()) column.add_child(HSeparator.new()) _themed_label("STEUERUNG", "SectionLabel", column) _themed_label("1–5 Wächter wählen Linksklick Bauen / Auswählen\nRechtsklick / Esc Bauauftrag abbrechen\nLeertaste Kampf / nächste Welle P Pause\nTab Front wechseln C Arbeiter & Kristall\nMausrad Zoomen Shift + Klick Mehrfach bauen\nQ / E Kamera drehen Mittlere Maustaste ziehen Verschieben\nR Kamera zurücksetzen und dem Kampf wieder folgen\nV 3D-Modelle ansehen, drehen und Bewegungen abspielen", "Label", column) _themed_label("Startformation: Tank, Nahkämpfer und Schütze.\nFunkenleser liefern ab der nächsten Welle Funken.\nSöldner schickst du ab Welle 3 zum Rivalen.", "MutedLabel", column) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 12) column.add_child(row) var back := _button("ZURÜCK [Esc]", toggle_settings, row) back.theme_type_variation = "PrimaryButton" back.size_flags_horizontal = Control.SIZE_EXPAND_FILL _button("Neue Partie", func(): settings_panel.hide() main_menu.pending_screen = MainMenu.Screen.SETUP menu_requested.emit(), row).tooltip_text = "Beendet diese Partie und öffnet die Bundauswahl." _button("Spiel beenden", func(): Soundscape.quit_game(), row) settings_panel.visible = false func _themed_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 sync_settings() -> void: _volume_slider.set_value_no_signal(Preferences.volume * 100.0) _mute_check.set_pressed_no_signal(Preferences.muted) _follow_check.set_pressed_no_signal(Preferences.follow_camera) _fullscreen_check.set_pressed_no_signal(Preferences.fullscreen) _reduced_check.set_pressed_no_signal(Preferences.reduced_motion) func toggle_settings() -> void: if settings_panel.visible: settings_panel.hide() GameState.paused = _settings_paused if is_instance_valid(_settings_focus) and _settings_focus.is_visible_in_tree(): _settings_focus.grab_focus() else: _settings_focus = get_viewport().gui_get_focus_owner() _settings_paused = GameState.paused GameState.paused = true Soundscape.stop_combat() sync_settings() settings_panel.show() _volume_slider.grab_focus() _refresh() func update_battle_readout(units: Array, manual_camera: bool) -> void: _camera_status.text = ("Freie Kamera · R Zurücksetzen" if manual_camera else "Q / E Drehen · Mausrad Zoom") + " · F1 Hilfe" var boss = null if GameState.phase == GameState.Phase.COMBAT: for unit in units: if unit.alive and unit.is_attacker and unit.team_id == viewed_team and unit._is_boss(): boss = unit break _boss_bar.visible = boss != null _boss_label.visible = boss != null if boss != null: _boss_bar.max_value = boss.max_hp _boss_bar.value = boss.hp _boss_label.text = "%s · %d / %d HP" % [boss.data.display_name, ceili(boss.hp), ceili(boss.max_hp)] func inspect_selected_model() -> void: var id: String = build_type for entry: Dictionary in EconomyManager.get_team(0).roster: if entry.uid == selected_uid: id = str(entry.type_id) break if id.is_empty(): id = "w10" if GameState.factions[0] == "wurzelbund" else "g04" model_inspector.show_model(id) func request_wave_start() -> void: if GameState.paused: notify("Setze das Spiel mit P fort, bevor du eine Welle startest.") return if GameState.phase == GameState.Phase.BUILD and EconomyManager.get_team(0).roster.is_empty(): notify("Setze zuerst Wächter oder nutze die Startformation.", true) return ready_requested.emit()