Loadout-Bereich: Granaten und Minen pro Karte festlegen (V18, Undo, auch im Spieler-Modus, gilt für Bots); Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkB4i3mQaxfg6aEncovy2Q
This commit is contained in:
parent
330365e59b
commit
91c66a0b98
3 changed files with 184 additions and 5 deletions
|
|
@ -7,6 +7,8 @@ extends RefCounted
|
|||
## doc["loadout"] fehlt oder mode "player" = Spieler-Loadout aus der Lobby.
|
||||
## mode "fixed" = {primary, secondary ("" = keine), attachments:{waffe: "slot=teil;…"}}
|
||||
## — Aufsatz-Text nur mit Abweichungen vom Standard (Draht-Format wie im Client).
|
||||
## In beiden Modi optional: grenades:{id: anzahl} und mines:anzahl = Vorrat pro
|
||||
## Leben für Spieler und Bots; fehlen sie, gilt der Standard des Servers.
|
||||
## Katalog-Quelle: library/loadout_katalog.json (tools/sync_loadout_katalog.py).
|
||||
|
||||
const PATH := "res://library/loadout_katalog.json"
|
||||
|
|
@ -20,6 +22,21 @@ const TEIL_NAMEN := {
|
|||
"sight_offset": "Schrägvisier (seitlich, Punkt)",
|
||||
}
|
||||
|
||||
## Granatenarten des Servers (library/grenades.json); die ersten drei stellt das Panel zur Wahl.
|
||||
const GRANATEN := {
|
||||
"he1": "Splittergranate",
|
||||
"blend1": "Blendgranate",
|
||||
"smoke1": "Rauchgranate",
|
||||
"he2": "Splittergranate leicht",
|
||||
"blend2": "Blendgranate kurz",
|
||||
"smoke2": "Nebelgranate",
|
||||
"bomb": "Bombe",
|
||||
}
|
||||
const GRANATEN_PANEL: Array[String] = ["he1", "blend1", "smoke1"]
|
||||
const MAX_GRANATEN_JE_ART := 5
|
||||
const MAX_MINEN := 3 # Spiegel zu MineSystem.MAX_ACTIVE_PER_OWNER
|
||||
const STANDARD_MINEN := 2 # Spiegel zu MineSystem.START_STOCK
|
||||
|
||||
static var _data: Dictionary = {}
|
||||
static var _loaded := false
|
||||
|
||||
|
|
@ -135,6 +152,7 @@ static func validate(lo: Variant) -> PackedStringArray:
|
|||
if mode != MODE_FIXED and mode != MODE_PLAYER:
|
||||
errors.append("V18: loadout.mode muss 'fixed' oder 'player' sein (ist '%s')." % mode)
|
||||
return errors
|
||||
errors.append_array(_pruefe_vorrat(d))
|
||||
if mode == MODE_PLAYER:
|
||||
return errors
|
||||
var prim := str(d.get("primary", "")).strip_edges().to_lower()
|
||||
|
|
@ -158,3 +176,26 @@ static func validate(lo: Variant) -> PackedStringArray:
|
|||
if why != "":
|
||||
errors.append("V18: loadout.attachments['%s']: %s." % [w, why])
|
||||
return errors
|
||||
|
||||
|
||||
static func _pruefe_vorrat(d: Dictionary) -> PackedStringArray:
|
||||
var errors := PackedStringArray()
|
||||
if d.has("grenades"):
|
||||
var g: Variant = d["grenades"]
|
||||
if not (g is Dictionary):
|
||||
errors.append("V18: loadout.grenades muss ein Objekt sein.")
|
||||
else:
|
||||
for gid in (g as Dictionary).keys():
|
||||
if not GRANATEN.has(str(gid)):
|
||||
errors.append("V18: loadout.grenades['%s'] ist keine bekannte Granate." % str(gid))
|
||||
elif not _ist_anzahl(g[gid], MAX_GRANATEN_JE_ART):
|
||||
errors.append("V18: loadout.grenades['%s'] muss eine ganze Zahl von 0 bis %d sein." % [str(gid), MAX_GRANATEN_JE_ART])
|
||||
if d.has("mines") and not _ist_anzahl(d["mines"], MAX_MINEN):
|
||||
errors.append("V18: loadout.mines muss eine ganze Zahl von 0 bis %d sein." % MAX_MINEN)
|
||||
return errors
|
||||
|
||||
|
||||
static func _ist_anzahl(v: Variant, max_n: int) -> bool:
|
||||
if not (v is int or v is float):
|
||||
return false
|
||||
return float(v) == floorf(float(v)) and float(v) >= 0.0 and float(v) <= float(max_n)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ extends "res://src/ui/panels/editor_panel.gd"
|
|||
## 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: src/svmap/loadout_katalog.gd, V18). Der Server
|
||||
## setzt ein festes Loadout durch, der Client zeigt es an.
|
||||
## 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).
|
||||
|
|
@ -16,6 +17,10 @@ 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] = []
|
||||
|
|
@ -60,9 +65,31 @@ func build() -> void:
|
|||
attach_box.add_theme_constant_override("separation", UIScale.px(6))
|
||||
fixed_box.add_child(attach_box)
|
||||
|
||||
hint_label = _muted_label("Messer, Minen und Granaten bleiben unverändert.")
|
||||
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()))
|
||||
|
|
@ -71,6 +98,20 @@ func build() -> void:
|
|||
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:
|
||||
|
|
@ -97,15 +138,72 @@ func set_fixed(fixed: bool) -> void:
|
|||
if main.doc.is_empty():
|
||||
return
|
||||
if not fixed:
|
||||
_commit(null, "Loadout: Spieler-Loadout")
|
||||
_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
|
||||
_commit({"mode": LoadoutKatalog.MODE_FIXED, "primary": prim, "secondary": sec, "attachments": {}},
|
||||
"Loadout: festes Map-Loadout")
|
||||
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.
|
||||
|
|
@ -187,6 +285,12 @@ func refresh_values() -> void:
|
|||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,33 @@ func _test_panel() -> void:
|
|||
panel.set_fixed(false)
|
||||
_check(not main.doc.has("loadout"), "Spieler-Loadout entfernt den Block")
|
||||
|
||||
# Granaten + Minen: Undo, Spieler-Modus, Validierung
|
||||
_check(not panel.vorrat_check.button_pressed and not panel.vorrat_box.visible, "Vorrat aus: Server-Standard")
|
||||
panel.set_vorrat_aktiv(true)
|
||||
_check(main.doc["loadout"]["mode"] == "player" and main.doc["loadout"]["mines"] == 2, "Vorrat an: Block im Spieler-Modus, Standard 2 Minen")
|
||||
_check(panel.vorrat_box.visible and panel.minen_spin.value == 2, "Eingabefelder sichtbar")
|
||||
panel.set_granaten("he1", 3)
|
||||
panel.set_minen(1)
|
||||
_check(main.doc["loadout"]["grenades"]["he1"] == 3 and main.doc["loadout"]["mines"] == 1, "Anzahlen gespeichert")
|
||||
panel.set_granaten("smoke1", 0)
|
||||
_check(not main.doc["loadout"]["grenades"].has("smoke1"), "0 entfernt die Art")
|
||||
main.history.undo()
|
||||
main.call("_refresh_all")
|
||||
_check(main.doc["loadout"]["grenades"]["smoke1"] == 1 and panel.granaten_spins["smoke1"].value == 1, "Undo holt die Art zurück, UI folgt")
|
||||
panel.granaten_spins["blend1"].value = 4
|
||||
_check(main.doc["loadout"]["grenades"]["blend1"] == 4, "Eingabefeld schreibt den Wert")
|
||||
_check(LoadoutKatalog.validate(main.doc["loadout"]).is_empty(), "Vorrat gültig")
|
||||
panel.set_fixed(true)
|
||||
_check(panel.is_fixed() and main.doc["loadout"]["mines"] == 1, "Wechsel auf fest behält den Vorrat")
|
||||
panel.set_fixed(false)
|
||||
_check(main.doc["loadout"]["mode"] == "player" and main.doc["loadout"]["mines"] == 1, "Wechsel auf Spieler behält den Vorrat")
|
||||
_check(not LoadoutKatalog.validate({"mode": "player", "mines": 4}).is_empty(), "zu viele Minen ungültig")
|
||||
_check(not LoadoutKatalog.validate({"mode": "player", "grenades": {"he1": 6}}).is_empty(), "zu viele Granaten ungültig")
|
||||
_check(not LoadoutKatalog.validate({"mode": "player", "grenades": {"nix": 1}}).is_empty(), "unbekannte Granate ungültig")
|
||||
_check(not LoadoutKatalog.validate({"mode": "player", "grenades": 3}).is_empty(), "grenades ohne Objekt ungültig")
|
||||
panel.set_vorrat_aktiv(false)
|
||||
_check(not main.doc.has("loadout"), "Vorrat aus im Spieler-Modus entfernt den Block")
|
||||
|
||||
# Speichern/Laden
|
||||
panel.set_fixed(true)
|
||||
panel.set_weapons("sr2", "pistol2")
|
||||
|
|
@ -133,6 +160,13 @@ func _test_panel() -> void:
|
|||
_check(res["ok"], "Speichern ok: " + str(res["errors"]))
|
||||
var back := SvmapIO.load_from_file(path)
|
||||
_check(back["ok"] and back["data"]["loadout"]["primary"] == "sr2", "Laden: Loadout bleibt erhalten")
|
||||
panel.set_vorrat_aktiv(true)
|
||||
panel.set_minen(3)
|
||||
var doc3: Dictionary = main.doc.duplicate(true)
|
||||
SvmapIO.sign_map(doc3)
|
||||
res = SvmapIO.save_to_file(path, doc3)
|
||||
back = SvmapIO.load_from_file(path)
|
||||
_check(res["ok"] and back["ok"] and back["data"]["loadout"]["mines"] == 3, "Laden: Vorrat bleibt erhalten")
|
||||
|
||||
|
||||
func _signed(d: Dictionary) -> Dictionary:
|
||||
|
|
|
|||
Loading…
Reference in a new issue