391 lines
15 KiB
GDScript
391 lines
15 KiB
GDScript
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)
|