Files

391 lines
15 KiB
GDScript
Raw Permalink Normal View History

2026-09-12 11:01:37 +02:00
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\n28 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)