scopedvr-map-core/ui/panels/loadout_panel.gd
2026-11-01 19:31:33 +01:00

329 lines
11 KiB
GDScript

extends "res://core/ui/panels/editor_panel.gd"
## Loadout-Bereich im Map-Tab (Karte „Loadout pro Map"): pro Map wählbar, ob die
## Spieler ihr eigenes Lobby-Loadout mitbringen oder die Map ein festes Loadout
## (Primär-/Sekundärwaffe + Aufsätze) vorschreibt. Gespeichert als optionaler
## Block doc["loadout"] (Regeln: core/svmap/loadout_katalog.gd, V18). Der Server
## setzt ein festes Loadout durch, der Client zeigt es an. In beiden Modi lässt
## sich zusätzlich der Granaten-/Minen-Vorrat pro Leben festlegen (auch für Bots).
##
## Jede Änderung ist ein Undo-Schritt (History-Block "loadout"; fehlt der Block,
## ist das Spieler-Loadout gemeint).
const KEINE := ""
var mode_option: OptionButton
var primary_option: OptionButton
var secondary_option: OptionButton
var fixed_box: VBoxContainer
var attach_box: VBoxContainer
var hint_label: Label
var vorrat_check: CheckBox
var vorrat_box: VBoxContainer
var granaten_spins: Dictionary = {} # Granaten-ID -> SpinBox
var minen_spin: SpinBox
var _loading := false
var _ids: Array[String] = []
static func spec() -> Dictionary:
return {"titel": "Loadout", "gruppe": "map", "order": 25}
func panel_title() -> String:
return "Loadout"
func build() -> void:
add_child(_header_label("Loadout"))
add_child(_muted_label("Legt fest, mit welchen Waffen die Spieler diese Karte spielen."))
mode_option = OptionButton.new()
mode_option.tooltip_text = "Eigenes Loadout: Spieler nehmen ihre Auswahl aus der Lobby mit. Festes Loadout: alle bekommen genau diese Waffen, der Server setzt das durch."
mode_option.add_item("Eigenes Loadout der Spieler (Lobby)")
mode_option.add_item("Festes Map-Loadout")
add_child(mode_option)
fixed_box = VBoxContainer.new()
fixed_box.add_theme_constant_override("separation", UIScale.px(8))
add_child(fixed_box)
fixed_box.add_child(_field_label("Primärwaffe (Rücken)"))
primary_option = OptionButton.new()
fixed_box.add_child(primary_option)
fixed_box.add_child(_field_label("Sekundärwaffe (Hüfte)"))
secondary_option = OptionButton.new()
fixed_box.add_child(secondary_option)
_ids = LoadoutKatalog.waffen_ids()
for id in _ids:
primary_option.add_item(LoadoutKatalog.anzeigename(id))
secondary_option.add_item(LoadoutKatalog.anzeigename(id))
secondary_option.add_item("— keine —")
attach_box = VBoxContainer.new()
attach_box.add_theme_constant_override("separation", UIScale.px(6))
fixed_box.add_child(attach_box)
hint_label = _muted_label("Messer bleiben unverändert.")
fixed_box.add_child(hint_label)
add_child(_header_label("Granaten und Minen"))
vorrat_check = CheckBox.new()
vorrat_check.text = "Anzahl für diese Karte festlegen"
vorrat_check.tooltip_text = "Aus: es gilt der Standard des Servers. An: Spieler und Bots bekommen pro Leben genau diese Anzahl."
vorrat_check.toggled.connect(func(an: bool) -> void:
if not _loading:
set_vorrat_aktiv(an))
add_child(vorrat_check)
vorrat_box = VBoxContainer.new()
vorrat_box.add_theme_constant_override("separation", UIScale.px(6))
add_child(vorrat_box)
for gid in LoadoutKatalog.GRANATEN_PANEL:
var spin := _anzahl_zeile(str(LoadoutKatalog.GRANATEN[gid]), LoadoutKatalog.MAX_GRANATEN_JE_ART)
spin.value_changed.connect(func(v: float) -> void:
if not _loading:
set_granaten(gid, int(v)))
granaten_spins[gid] = spin
minen_spin = _anzahl_zeile("Minen", LoadoutKatalog.MAX_MINEN)
minen_spin.value_changed.connect(func(v: float) -> void:
if not _loading:
set_minen(int(v)))
mode_option.item_selected.connect(func(i: int) -> void: set_fixed(i == 1))
primary_option.item_selected.connect(func(i: int) -> void:
set_weapons(_ids[i], _secondary_id()))
secondary_option.item_selected.connect(func(_i: int) -> void:
set_weapons(_primary_id(), _secondary_id()))
refresh_values()
func _anzahl_zeile(titel: String, max_n: int) -> SpinBox:
var row := HBoxContainer.new()
var lab := _field_label(titel)
lab.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(lab)
var spin := SpinBox.new()
spin.min_value = 0
spin.max_value = max_n
spin.step = 1
row.add_child(spin)
vorrat_box.add_child(row)
return spin
# ------------------------------------------------------------- Lesen -------
func _primary_id() -> String:
return _ids[primary_option.selected] if primary_option.selected >= 0 else ""
func _secondary_id() -> String:
var i := secondary_option.selected
return _ids[i] if i >= 0 and i < _ids.size() else KEINE
func current() -> Dictionary:
return main.doc.get("loadout", {}) if main != null and not main.doc.is_empty() else {}
func is_fixed() -> bool:
return str(current().get("mode", LoadoutKatalog.MODE_PLAYER)) == LoadoutKatalog.MODE_FIXED
# ------------------------------------------------------------- Ändern ------
## Modus wechseln. Auf „fest" ohne vorherige Wahl: M40 + Pistole 1 als Start.
func set_fixed(fixed: bool) -> void:
if main.doc.is_empty():
return
if not fixed:
_commit(_nur_vorrat(current()), "Loadout: Spieler-Loadout")
return
var cur := current()
if cur.get("mode", "") == LoadoutKatalog.MODE_FIXED:
return
var prim := "m40" if LoadoutKatalog.hat_waffe("m40") else (_ids[0] if not _ids.is_empty() else "")
var sec := "pistol1" if LoadoutKatalog.hat_waffe("pistol1") else KEINE
var neu := {"mode": LoadoutKatalog.MODE_FIXED, "primary": prim, "secondary": sec, "attachments": {}}
var vorrat: Variant = _nur_vorrat(cur)
if vorrat != null:
neu.merge(vorrat as Dictionary, false)
_commit(neu, "Loadout: festes Map-Loadout")
## Spieler-Loadout mit Vorrat bleibt als Block erhalten; ohne Vorrat kein Block.
func _nur_vorrat(lo: Dictionary) -> Variant:
if not lo.has("grenades") and not lo.has("mines"):
return null
var out := {"mode": LoadoutKatalog.MODE_PLAYER}
for key in ["grenades", "mines"]:
if lo.has(key):
out[key] = lo[key]
return out
func hat_vorrat() -> bool:
return current().has("grenades") or current().has("mines")
## Vorrat festlegen (Start = Server-Standard) oder wieder dem Server überlassen.
func set_vorrat_aktiv(an: bool) -> void:
if main.doc.is_empty() or an == hat_vorrat():
return
var lo := current().duplicate(true)
if an:
if lo.is_empty():
lo["mode"] = LoadoutKatalog.MODE_PLAYER
lo["grenades"] = {"he1": 1, "blend1": 1, "smoke1": 1}
lo["mines"] = LoadoutKatalog.STANDARD_MINEN
_commit(lo, "Loadout: Granaten und Minen festgelegt")
return
lo.erase("grenades")
lo.erase("mines")
var leer := str(lo.get("mode", LoadoutKatalog.MODE_PLAYER)) == LoadoutKatalog.MODE_PLAYER
_commit(null if leer else lo, "Loadout: Granaten und Minen wie Server")
func set_granaten(gid: String, anzahl: int) -> void:
if main.doc.is_empty() or not hat_vorrat() or not LoadoutKatalog.GRANATEN.has(gid):
return
var lo := current().duplicate(true)
var g: Dictionary = lo.get("grenades", {})
if anzahl > 0:
g[gid] = clampi(anzahl, 0, LoadoutKatalog.MAX_GRANATEN_JE_ART)
else:
g.erase(gid)
lo["grenades"] = g
_commit(lo, "Loadout: %s" % str(LoadoutKatalog.GRANATEN[gid]))
func set_minen(anzahl: int) -> void:
if main.doc.is_empty() or not hat_vorrat():
return
var lo := current().duplicate(true)
lo["mines"] = clampi(anzahl, 0, LoadoutKatalog.MAX_MINEN)
_commit(lo, "Loadout: Minen")
## Waffen setzen. Aufsätze von Waffen, die nicht mehr im Loadout sind, fallen weg.
func set_weapons(primary: String, secondary: String) -> void:
if main.doc.is_empty() or not is_fixed():
return
if not LoadoutKatalog.hat_waffe(primary) or (secondary != KEINE and not LoadoutKatalog.hat_waffe(secondary)):
return
var lo := current().duplicate(true)
lo["primary"] = primary
lo["secondary"] = secondary
var att: Dictionary = lo.get("attachments", {})
for w in att.keys():
if w != primary and w != secondary:
att.erase(w)
lo["attachments"] = att
_commit(lo, "Loadout: %s / %s" % [LoadoutKatalog.anzeigename(primary),
LoadoutKatalog.anzeigename(secondary) if secondary != KEINE else "keine"])
## Ein Aufsatz-Slot einer Loadout-Waffe setzen (part = Teil-ID, "" = nichts).
func set_part(weapon: String, slot: String, part: String) -> void:
if main.doc.is_empty() or not is_fixed():
return
var lo := current().duplicate(true)
if weapon != str(lo.get("primary", "")) and weapon != str(lo.get("secondary", "")):
return
var att: Dictionary = lo.get("attachments", {})
var sel := LoadoutKatalog.auswahl(weapon, str(att.get(weapon, "")))
if not sel.has(slot):
return
sel[slot] = part
var wire := LoadoutKatalog.encode(weapon, sel)
if LoadoutKatalog.pruefe_aufsaetze(weapon, wire) != "":
return
if wire.is_empty():
att.erase(weapon)
else:
att[weapon] = wire
lo["attachments"] = att
_commit(lo, "Loadout: Aufsatz %s" % slot)
func _commit(neu: Variant, text: String) -> void:
var alt: Variant = main.doc.get("loadout", null)
if alt != null:
alt = (alt as Dictionary).duplicate(true)
if neu == null:
main.doc.erase("loadout")
else:
main.doc["loadout"] = (neu as Dictionary).duplicate(true)
var danach: Variant = main.doc.get("loadout", null)
if danach != null:
danach = (danach as Dictionary).duplicate(true)
if alt == danach:
return
main.history.commit(text, {"loadout": alt}, {"loadout": danach})
main.dirty = true
main.call("_refresh_file_label")
main.call("_refresh_history_buttons")
refresh_values()
main.call("_set_status", "%s." % text)
# ------------------------------------------------------------ Anzeige ------
## UI aus dem doc auffüllen (Laden, Undo/Redo) — löst keinen Schreibzugriff aus.
func refresh_values() -> void:
if mode_option == null or main == null:
return
_loading = true
var lo := current()
var fixed := is_fixed()
mode_option.select(1 if fixed else 0)
fixed_box.visible = fixed
if fixed:
var prim := str(lo.get("primary", ""))
var sec := str(lo.get("secondary", ""))
primary_option.select(maxi(0, _ids.find(prim)))
secondary_option.select(_ids.find(sec) if sec != KEINE and _ids.has(sec) else _ids.size())
_rebuild_attachments(prim, sec, lo.get("attachments", {}))
var vorrat := hat_vorrat()
vorrat_check.button_pressed = vorrat
vorrat_box.visible = vorrat
for gid in granaten_spins:
(granaten_spins[gid] as SpinBox).value = int((lo.get("grenades", {}) as Dictionary).get(gid, 0))
minen_spin.value = int(lo.get("mines", LoadoutKatalog.STANDARD_MINEN))
_loading = false
func _rebuild_attachments(prim: String, sec: String, att: Dictionary) -> void:
for c in attach_box.get_children():
attach_box.remove_child(c)
c.queue_free()
for w in [prim, sec]:
if w == "" or LoadoutKatalog.slots(w).is_empty():
continue
attach_box.add_child(_header_label("Aufsätze: %s" % LoadoutKatalog.anzeigename(w)))
var sel := LoadoutKatalog.auswahl(w, str(att.get(w, "")))
var sl := LoadoutKatalog.slots(w)
var slot_names: Array = sl.keys()
slot_names.sort()
for s in slot_names:
var sd: Dictionary = sl[s]
var row := HBoxContainer.new()
var lab := _field_label(str(s).replace("_", " "))
lab.custom_minimum_size = UIScale.v2(90, 0)
row.add_child(lab)
var ob := OptionButton.new()
ob.name = "Teil_%s_%s" % [w, s]
ob.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var parts: Array = []
if bool(sd.get("optional", false)):
parts.append("")
parts.append_array(sd.get("options", []))
for p in parts:
ob.add_item(LoadoutKatalog.teil_name(str(p)))
ob.select(maxi(0, parts.find(sel.get(s, ""))))
ob.item_selected.connect(func(i: int) -> void:
if not _loading:
set_part(w, str(s), str(parts[i])))
row.add_child(ob)
attach_box.add_child(row)