329 lines
11 KiB
GDScript
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)
|