class_name GameUI extends CanvasLayer signal host_requested(settings: Dictionary) signal join_requested(address: String) signal leave_requested signal preferences_changed var game: Node3D var root: Control var panel: VBoxContainer var status: Label var page := "" var fields := {} var browser_rows: VBoxContainer var refresh_clock := 0.0 const INK := Color("101e27") const PAPER := Color("e6eee9") const MINT := Color("a5dec8") func _ready() -> void: layer = 10 root = Control.new() root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(root) var theme := Theme.new() var font := SystemFont.new() font.font_names = PackedStringArray(["Bahnschrift", "Segoe UI", "Arial"]) theme.default_font = font theme.default_font_size = 20 for state in ["normal", "hover", "pressed", "focus"]: var style := StyleBoxFlat.new() style.bg_color = Color("233943") if state == "normal" else Color("3c625f") style.border_width_bottom = 1 style.border_color = Color("52776f") style.content_margin_left = 20 style.content_margin_right = 20 style.content_margin_top = 14 style.content_margin_bottom = 14 theme.set_stylebox(state, "Button", style) theme.set_stylebox(state, "OptionButton", style) theme.set_color("font_color", "Button", PAPER) theme.set_color("font_color", "Label", PAPER) root.theme = theme main_menu() func clear(title: String, subtitle: String) -> void: root.show() for child in root.get_children(): root.remove_child(child) child.queue_free() fields.clear() browser_rows = null var shade := ColorRect.new() shade.color = Color(0.025, 0.07, 0.1, 0.985) shade.position = Vector2.ZERO shade.size = Vector2(630, 900) root.add_child(shade) var rail := ColorRect.new() rail.color = MINT rail.position = Vector2(630, 0) rail.size = Vector2(3, 900) root.add_child(rail) panel = VBoxContainer.new() panel.position = Vector2(60, 44) panel.size = Vector2(510, 800) panel.add_theme_constant_override("separation", 12) root.add_child(panel) label("B / L • LOCAL OPERATIONS", 16, MINT) space(12) label(title, 57) label(subtitle, 17, Color("93aaa9")) space(18) status = Label.new() status.position = Vector2(60, 825) status.size = Vector2(515, 60) status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART status.add_theme_font_size_override("font_size", 16) status.add_theme_color_override("font_color", Color("f2c78e")) root.add_child(status) var caption_bg := ColorRect.new() caption_bg.color = Color(0.025, 0.07, 0.1, 0.87) caption_bg.position = Vector2(670, 732) caption_bg.size = Vector2(760, 135) root.add_child(caption_bg) var caption := Label.new() caption.position = Vector2(695, 744) caption.text = "MAPS / QUARRY + DUST 2\nKARTE VOR DEM MATCH AUSWÄHLEN\n2–8 OPERATORS / LAN" if title == "LOADOUT": caption.text = "ARMORY / ORIGINAL WEAPON MODELS\nMAUS ZIEHEN: MODELL DREHEN\nMetall / Polymer / Optik / bewegliche Bauteile" caption.add_theme_font_size_override("font_size", 24) caption.add_theme_color_override("font_color", PAPER) root.add_child(caption) func label(text: String, size: int = 20, color: Color = PAPER) -> Label: var l := Label.new() l.text = text l.add_theme_font_size_override("font_size", size) l.add_theme_color_override("font_color", color) panel.add_child(l) return l func space(height: float) -> void: var c := Control.new() c.custom_minimum_size.y = height panel.add_child(c) func button(text: String, action: Callable) -> Button: var b := Button.new() b.text = text b.alignment = HORIZONTAL_ALIGNMENT_LEFT b.custom_minimum_size.y = 55 b.pressed.connect(action) panel.add_child(b) return b func message(text: String) -> void: if is_instance_valid(status): status.text = text func main_menu() -> void: page = "main" root.show() for child in root.get_children(): root.remove_child(child) child.queue_free() fields.clear() browser_rows = null var front := FrontEnd.new() front.ui = self root.add_child(front) func multiplayer_menu() -> void: page = "multiplayer" clear("MULTIPLAYER", "LAN / DEDICATED SERVER") button("HOST GAME →", host_menu) button("JOIN GAME →", browser_menu) button("DIRECT CONNECT →", direct_menu) space(15) button("← BACK", main_menu) func text_field(key: String, title: String, value: String) -> void: label(title, 15, MINT) var field := LineEdit.new() field.text = value field.custom_minimum_size.y = 42 field.max_length = 40 panel.add_child(field) fields[key] = field func option(key: String, title: String, items: Array, selected: int = 0) -> void: label(title, 15, MINT) var field := OptionButton.new() for item in items: field.add_item(str(item)) field.select(selected) panel.add_child(field) fields[key] = field func play_menu() -> void: page = "play" clear("PLAY", "LOKALES MATCH / MIT BOTS") option("map", "MAP AUSWÄHLEN", MapCatalog.NAMES, maxi(0, MapCatalog.IDS.find(game.config.get("map", "quarry")))) option("difficulty", "BOT-SCHWIERIGKEIT", BotSkill.NAMES, int(game.config.get("difficulty", 0))) button("START GAME →", func(): var settings: Dictionary = game.config.duplicate() settings["map"] = MapCatalog.IDS[fields.map.selected] settings["difficulty"] = fields.difficulty.selected host_requested.emit(settings)) button("MATCH EINSTELLEN →", host_menu) button("← BACK", main_menu) func host_menu() -> void: page = "host" clear("HOST GAME", "MAP / MATCH / UDP %s" % game.port) var scroll := ScrollContainer.new() scroll.position = Vector2(60, 216) scroll.size = Vector2(555, 590) root.add_child(scroll) var form := VBoxContainer.new() form.custom_minimum_size.x = 510 form.add_theme_constant_override("separation", 4) scroll.add_child(form) panel = form option("map", "MAP AUSWÄHLEN", MapCatalog.NAMES, maxi(0, MapCatalog.IDS.find(game.config.get("map", "quarry")))) text_field("name", "SERVER NAME", game.config.server_name) option("mode", "GAME MODE", ["Team Deathmatch", "Free For All", "Domination", "Kill Confirmed"], ["TDM", "FFA", "DOM", "KC"].find(game.config.mode)) option("slots", "MAXIMUM PLAYERS", [2, 4, 6, 8], 3) option("bots", "BOTS · FREIE PLÄTZE AUFFÜLLEN", [0, 1, 3, 5, 7], 4) option("difficulty", "BOT-SCHWIERIGKEIT", BotSkill.NAMES, int(game.config.get("difficulty", 0))) option("score", "SCORE LIMIT", [25, 50, 75, 100], 1) fields.mode.item_selected.connect(func(index): fields.score.select(0 if index == 1 else 1)) option("time", "TIME LIMIT", ["5 Minuten", "10 Minuten", "15 Minuten"], 1) button("START GAME →", func(): host_requested.emit({"server_name": fields.name.text.left(30), "mode": ["TDM", "FFA", "DOM", "KC"][fields.mode.selected], "max_players": [2, 4, 6, 8][fields.slots.selected], "bots": [0, 1, 3, 5, 7][fields.bots.selected], "difficulty": fields.difficulty.selected, "map": MapCatalog.IDS[fields.map.selected], "score_limit": [25, 50, 75, 100][fields.score.selected], "time_limit": [300, 600, 900][fields.time.selected]})) button("← BACK", multiplayer_menu) func browser_menu() -> void: page = "browser" clear("JOIN GAME", "AUTOMATISCHE LAN-SUCHE / MAP DES HOSTS") browser_rows = VBoxContainer.new() browser_rows.custom_minimum_size.y = 260 panel.add_child(browser_rows) refresh_browser() button("DIRECT CONNECT →", direct_menu) button("← BACK", multiplayer_menu) message("Server werden jede Sekunde aktualisiert. Discovery nutzt UDP 27841.") func refresh_browser() -> void: for child in browser_rows.get_children(): browser_rows.remove_child(child) child.queue_free() if game.discovery.servers.is_empty(): var empty := Label.new() empty.text = "Suche nach lokalen Servern …\n\nNoch kein Host gefunden.\nDirect Connect ist ebenfalls verfügbar." empty.add_theme_font_size_override("font_size", 18) browser_rows.add_child(empty) for address in game.discovery.servers: var info: Dictionary = game.discovery.servers[address] var b := Button.new() var ping := "%s ms" % info.get("ping", -1) if info.get("ping", -1) >= 0 else "Ping …" b.text = "%s · %s/%s · %s\n%s / %s JOIN →" % [str(info.get("name", "Server")).left(22), info.get("players", 0), info.get("max", 8), ping, str(info.get("mode", "TDM")) + " · " + MapCatalog.title(str(info.get("map", "quarry"))), address] b.custom_minimum_size.y = 85 b.pressed.connect(func(): join_requested.emit(address)) browser_rows.add_child(b) func direct_menu() -> void: page = "direct" clear("CONNECT", "DIREKT ZUM HOST / IPv4") text_field("ip", "SERVER-ADRESSE · OPTIONAL :PORT", game.last_address) button("CONNECT →", func(): join_requested.emit(fields.ip.text)) button("← BACK", multiplayer_menu) space(15) label("LAN- oder erreichbare Server-IP eingeben.\nDer Spielport muss per UDP erreichbar sein.\nFür Tests auf diesem PC: 127.0.0.1", 18, Color("93aaa9")) func loadout_menu() -> void: page = "loadout" clear("KLASSENEDITOR", "DEINE WAFFE. DEIN AUFTRAG.") for child in root.get_children(): root.remove_child(child) child.queue_free() var editor := ClassEditor.new() editor.ui = self root.add_child(editor) func operator_menu() -> void: page = "operator" clear("OPERATOR", "STEVE / GAS MASK") for child in root.get_children(): root.remove_child(child) child.queue_free() var gallery := OperatorGallery.new() gallery.ui = self root.add_child(gallery) func settings_menu() -> void: page = "settings" clear("SETTINGS", "BEWEGUNG / SICHT / AUDIO") button("STEUERUNG / TASTEN FREI BELEGEN →", controls_menu) option("graphics", "GRAFIKQUALITÄT · %s VOLLBILD" % ControlBindings.hint("fullscreen"), GraphicsSettings.NAMES, game.graphics_quality) fields.graphics.item_selected.connect(func(index): GraphicsSettings.apply(game, index) await ShaderPrewarm.run(game)) if not game.active or game.is_host: option("difficulty", "BOT-SCHWIERIGKEIT · DIREKT AKTIV", BotSkill.NAMES, int(game.config.get("difficulty", 0))) fields.difficulty.item_selected.connect(func(index): game.config["difficulty"] = index) slider("MAUSEMPFINDLICHKEIT", 0.0005, 0.005, game.sensitivity, func(v): game.sensitivity = v) slider("SICHTFELD", 70, 110, game.base_fov, func(v): game.base_fov = v) slider("LAUTSTÄRKE", 0, 1, db_to_linear(AudioServer.get_bus_volume_db(0)), func(v): AudioServer.set_bus_volume_db(0, linear_to_db(maxf(0.0001, v)))) slider("MUSIK", 0, 1, MusicDirector.volume, func(v): MusicDirector.volume = v) label("Musik: Luca Baradel · ASSAULT\nSowjetische Waffenmodelle: radint", 14, Color("93aaa9")) space(14) label("%s / %s Feuer / Zielen\n%s Nachladen %s Waffe wechseln\n%s Springen %s Ducken\n%s Interagieren %s Menü" % [ControlBindings.hint("fire"), ControlBindings.hint("aim"), ControlBindings.hint("reload"), ControlBindings.hint("switch"), ControlBindings.hint("jump"), ControlBindings.hint("crouch"), ControlBindings.hint("interact"), ControlBindings.hint("pause")], 18, Color("93aaa9")) button("SAVE & BACK", func(): preferences_changed.emit() if game.active: pause_menu() else: main_menu()) func controls_menu() -> void: page = "controls" clear("STEUERUNG", "TASTATUR / MAUS") for child in root.get_children(): root.remove_child(child) child.queue_free() var editor := ControlsEditor.new() editor.ui = self root.add_child(editor) func slider(title: String, minimum: float, maximum: float, value: float, callback: Callable) -> void: label(title, 15, MINT) var control := HSlider.new() control.min_value = minimum control.max_value = maximum control.step = (maximum - minimum) / 100 control.value = value control.custom_minimum_size.y = 35 control.value_changed.connect(callback) panel.add_child(control) func pause_menu() -> void: page = "pause" clear("OPERATIONS", "MATCH LÄUFT WEITER") button("RESUME →", func(): game.paused = false Input.mouse_mode = Input.MOUSE_MODE_CAPTURED hide_menu()) button("SETTINGS →", settings_menu) button("LEAVE MATCH", func(): leave_requested.emit()) if game.is_host: label("Host verlassen beendet das Match für alle.", 16, Color("93aaa9")) var addresses := [] for address in IP.get_local_addresses(): if address.contains(".") and not address.begins_with("127."): addresses.append(address) label("HOST IP\n" + "\n".join(addresses), 18, MINT) func hide_menu() -> void: page = "" root.hide() func _process(dt: float) -> void: refresh_clock += dt if page == "browser" and refresh_clock > 1: refresh_clock = 0 refresh_browser() func account_placeholder() -> void: var box := PanelContainer.new() box.position = Vector2(1030, 38) box.size = Vector2(490, 165) root.add_child(box) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 12) box.add_child(content) for words in ["ACCOUNT / BALD VERFÜGBAR", "Gastprofil · kein Konto verbunden", "GUTHABEN: — / SHOP: GESPERRT"]: var line := Label.new() line.text = words line.add_theme_font_size_override("font_size", 20) content.add_child(line) var disabled := Button.new() disabled.text = "ANMELDEN / REGISTRIEREN · PLATZHALTER" disabled.disabled = true content.add_child(disabled) func skin_menu(weapon: int) -> void: page = "skins" clear("WEAPON LAB", "LOKALE DESIGNS / KEIN SHOP ERFORDERLICH") var names := [] for entry: Dictionary in Arsenal.DATA: names.append(entry.name) option("skin_weapon", "WAFFE", names, weapon) fields.skin_weapon.item_selected.connect(skin_menu) var showcase := WeaponShowcase.new() root.add_child(showcase) showcase.select_weapon(weapon) var design := WeaponSkins.get_design(weapon) var parts := WeaponSkins.LABELS.duplicate() parts.append("Beschriftung") option("skin_part", "BAUTEIL / FARBE", parts) var picker := ColorPickerButton.new() picker.custom_minimum_size.y = 42 picker.edit_alpha = false picker.color = design.get("receiver", Color("8c8064")) panel.add_child(picker) fields.skin_part.item_selected.connect(func(index): var key: String = WeaponSkins.PARTS[index] if index < 8 else "text_color" picker.color = design.get(key, Color.WHITE)) picker.color_changed.connect(func(color): var index: int = fields.skin_part.selected var key: String = WeaponSkins.PARTS[index] if index < 8 else "text_color" design[key] = color WeaponSkins.apply(showcase.model, design)) text_field("skin_text", "EIGENER TEXT / BIS 32 ZEICHEN", design.get("text", "")) fields.skin_text.max_length = 32 fields.skin_text.text_changed.connect(func(words): design.text = words WeaponSkins.apply(showcase.model, design)) label("Mit der Maus die Vorschau drehen.\nFarben ändern keine Waffenwerte.", 16, Color("93aaa9")) button("DESIGN SPEICHERN", func(): WeaponSkins.set_design(weapon, design) if PlayerSettings.save(game) == OK: message("Design lokal gespeichert. Für diese Waffe aktiv.") else: message("Design aktiv, konnte aber nicht dauerhaft gespeichert werden.")) button("WERKSDESIGN WIEDERHERSTELLEN", func(): WeaponSkins.set_design(weapon, {}) preferences_changed.emit() skin_menu(weapon)) button("← ZURÜCK / UNGESPEICHERTES VERWERFEN", loadout_menu)