Spielmodi im Editor: Checkpoints → Conquest (Alt-Maps werden beim Laden umgestellt), King of the Hill mit Hill-Liste + Reihenfolge, Deathmatch, eigene Spawns je Modus (mode_spawns, Spawn-Satz-Auswahl, Undo), V21–V24
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f42e133bae
commit
92e2c688c1
10 changed files with 578 additions and 155 deletions
|
|
@ -25,6 +25,39 @@ const RADIUS_KEY := "spawn_radius_m" # Unter design_notes (Loader-frei, siehe ob
|
||||||
const TEAM_LABELS := ["Team Blau", "Team Rot"]
|
const TEAM_LABELS := ["Team Blau", "Team Rot"]
|
||||||
const TEAM_PREFIXES := ["A", "B"]
|
const TEAM_PREFIXES := ["A", "B"]
|
||||||
|
|
||||||
|
## Welcher Spawn-Satz gerade bearbeitet wird (reiner Editor-Zustand, nicht im Dokument):
|
||||||
|
## "base" = doc.spawn_zones (Standard für alle Modi), sonst ein Modus aus
|
||||||
|
## doc.mode_spawns (eigene Spawns dieses Modus, ModusKatalog). Deathmatch kennt nur
|
||||||
|
## den Schlüssel "all" — beide Team-Werkzeuge schreiben dorthin.
|
||||||
|
static var edit_set: String = ModusKatalog.SPAWN_SET_BASE
|
||||||
|
const SET_ID_PREFIX := {"tdm": "T", "conquest": "Q", "koth": "K", "dm": "D"}
|
||||||
|
|
||||||
|
|
||||||
|
static func _team_key(team: int) -> String:
|
||||||
|
return "all" if edit_set == ModusKatalog.MODE_DM else str(clampi(team, 0, 1))
|
||||||
|
|
||||||
|
|
||||||
|
## Zonen-Block des bearbeiteten Satzes: {"0":{points}, "1":{points}} bzw. {"all":{points}}.
|
||||||
|
## create=true legt fehlende Blöcke an (nur für Schreibzugriffe).
|
||||||
|
static func zones_of(doc: Dictionary, create := false) -> Dictionary:
|
||||||
|
if edit_set == ModusKatalog.SPAWN_SET_BASE:
|
||||||
|
var z = doc.get("spawn_zones", {})
|
||||||
|
if not (z is Dictionary):
|
||||||
|
z = {}
|
||||||
|
if create:
|
||||||
|
doc["spawn_zones"] = z
|
||||||
|
return z
|
||||||
|
var ms = doc.get("mode_spawns", {})
|
||||||
|
if not (ms is Dictionary):
|
||||||
|
ms = {}
|
||||||
|
var set_v = (ms as Dictionary).get(edit_set, {})
|
||||||
|
if not (set_v is Dictionary):
|
||||||
|
set_v = {}
|
||||||
|
if create:
|
||||||
|
doc["mode_spawns"] = ms
|
||||||
|
(ms as Dictionary)[edit_set] = set_v
|
||||||
|
return set_v
|
||||||
|
|
||||||
|
|
||||||
static func team_label(team: int) -> String:
|
static func team_label(team: int) -> String:
|
||||||
return TEAM_LABELS[clampi(team, 0, 1)]
|
return TEAM_LABELS[clampi(team, 0, 1)]
|
||||||
|
|
@ -36,9 +69,8 @@ static func team_prefix(team: int) -> String:
|
||||||
|
|
||||||
## Der Zone-Block eines Teams ("0"/"1") — immer ein Dictionary (leer, wenn fehlt).
|
## Der Zone-Block eines Teams ("0"/"1") — immer ein Dictionary (leer, wenn fehlt).
|
||||||
static func zone_dict(doc: Dictionary, team: int) -> Dictionary:
|
static func zone_dict(doc: Dictionary, team: int) -> Dictionary:
|
||||||
var zones = doc.get("spawn_zones", {})
|
var zones = zones_of(doc)
|
||||||
if zones is Dictionary:
|
var zone = zones.get(_team_key(team), {})
|
||||||
var zone = zones.get(str(team), {})
|
|
||||||
if zone is Dictionary:
|
if zone is Dictionary:
|
||||||
return zone
|
return zone
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -58,8 +90,13 @@ static func count_points(doc: Dictionary, team: int) -> int:
|
||||||
## (Team-Prefixe trennen die Teams, doppelt vergebene Nummern nie).
|
## (Team-Prefixe trennen die Teams, doppelt vergebene Nummern nie).
|
||||||
static func next_id(doc: Dictionary, team: int) -> String:
|
static func next_id(doc: Dictionary, team: int) -> String:
|
||||||
var used := {}
|
var used := {}
|
||||||
var zones = doc.get("spawn_zones", {})
|
var all_sets: Array = [doc.get("spawn_zones", {})]
|
||||||
if zones is Dictionary:
|
var ms = doc.get("mode_spawns", {})
|
||||||
|
if ms is Dictionary:
|
||||||
|
all_sets.append_array((ms as Dictionary).values())
|
||||||
|
for zones in all_sets:
|
||||||
|
if not (zones is Dictionary):
|
||||||
|
continue
|
||||||
for key in zones.keys():
|
for key in zones.keys():
|
||||||
var zone = zones[key]
|
var zone = zones[key]
|
||||||
if not (zone is Dictionary):
|
if not (zone is Dictionary):
|
||||||
|
|
@ -71,6 +108,8 @@ static func next_id(doc: Dictionary, team: int) -> String:
|
||||||
if p is Dictionary:
|
if p is Dictionary:
|
||||||
used[str(p.get("id", ""))] = true
|
used[str(p.get("id", ""))] = true
|
||||||
var prefix := team_prefix(team)
|
var prefix := team_prefix(team)
|
||||||
|
if edit_set != ModusKatalog.SPAWN_SET_BASE:
|
||||||
|
prefix = str(SET_ID_PREFIX.get(edit_set, "M")) + ("" if edit_set == ModusKatalog.MODE_DM else prefix)
|
||||||
var i := 1
|
var i := 1
|
||||||
while used.has("%s%d" % [prefix, i]):
|
while used.has("%s%d" % [prefix, i]):
|
||||||
i += 1
|
i += 1
|
||||||
|
|
@ -107,6 +146,8 @@ static func set_radius(doc: Dictionary, pid: String, m: float) -> void:
|
||||||
## Alt-Radien nach Undo herumgeistern.
|
## Alt-Radien nach Undo herumgeistern.
|
||||||
static func snapshot_blocks(hist: ActionHistory, doc: Dictionary) -> Dictionary:
|
static func snapshot_blocks(hist: ActionHistory, doc: Dictionary) -> Dictionary:
|
||||||
var out := hist.snapshot(["spawn_zones"])
|
var out := hist.snapshot(["spawn_zones"])
|
||||||
|
var ms: Variant = doc.get("mode_spawns", null) # fehlend = null → Undo löscht den Block wieder
|
||||||
|
out["mode_spawns"] = ms.duplicate(true) if ms is Dictionary else null
|
||||||
out["design_notes"] = (doc.get("design_notes", {}) as Dictionary).duplicate(true)
|
out["design_notes"] = (doc.get("design_notes", {}) as Dictionary).duplicate(true)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
@ -115,10 +156,8 @@ static func snapshot_blocks(hist: ActionHistory, doc: Dictionary) -> Dictionary:
|
||||||
## dem Boden; der Loader prüft nur Grenzen, aber schwebende Spawns verwirren).
|
## dem Boden; der Loader prüft nur Grenzen, aber schwebende Spawns verwirren).
|
||||||
## Rückgabe: der neue Punkt (id + pos).
|
## Rückgabe: der neue Punkt (id + pos).
|
||||||
static func add_zone(doc: Dictionary, team: int, x: float, z: float) -> Dictionary:
|
static func add_zone(doc: Dictionary, team: int, x: float, z: float) -> Dictionary:
|
||||||
var zones: Dictionary = doc.get("spawn_zones", {})
|
var zones: Dictionary = zones_of(doc, true)
|
||||||
if not (zones is Dictionary):
|
var team_key := _team_key(team)
|
||||||
zones = {}
|
|
||||||
var team_key := str(team)
|
|
||||||
var zone = zones.get(team_key, {})
|
var zone = zones.get(team_key, {})
|
||||||
if not (zone is Dictionary):
|
if not (zone is Dictionary):
|
||||||
zone = {}
|
zone = {}
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,13 @@ static func create_new(map_name: String, size_m: float, weather: String, game_mo
|
||||||
},
|
},
|
||||||
"weather": weather,
|
"weather": weather,
|
||||||
}
|
}
|
||||||
# Spielmodus (28.10.): nur ein abweichender Modus wird geschrieben; Checkpoint-
|
# Spielmodus (28.10.): nur ein abweichender Modus wird geschrieben; Conquest-
|
||||||
# Maps starten mit drei Zonen zwischen den Basen.
|
# und KotH-Maps starten mit drei Zonen/Hills zwischen den Basen.
|
||||||
if game_mode == ModusKatalog.MODE_CHECKPOINTS:
|
game_mode = ModusKatalog.normalize(game_mode)
|
||||||
|
if game_mode != ModusKatalog.MODE_TDM and ModusKatalog.MODES.has(game_mode):
|
||||||
data["game_mode"] = game_mode
|
data["game_mode"] = game_mode
|
||||||
data["checkpoints"] = ModusKatalog.default_checkpoints(data)
|
if ModusKatalog.has_zones(game_mode):
|
||||||
|
data[ModusKatalog.ZONE_KEYS[game_mode]] = ModusKatalog.default_zones(data, game_mode)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,249 @@
|
||||||
class_name ModusKatalog
|
class_name ModusKatalog
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Spielmodus + Checkpoints der Map (Karte Checkpoint-Modus, 28.10.).
|
## Spielmodus, Zonen/Hills und Modus-Spawns der Map (28.10.).
|
||||||
## doc["game_mode"] ("tdm" | "checkpoints", fehlt = tdm) und doc["checkpoints"]
|
## doc["game_mode"] ("tdm" | "conquest" | "koth" | "dm", fehlt = tdm),
|
||||||
## [{id, label, pos:[x,y,z], radius_m}] — Regeln 1:1 wie SvmapLoader im
|
## doc["conquest_zones"] und doc["hills"] [{id, label, pos:[x,y,z], radius_m}],
|
||||||
## Match-Server (V21/V22, docs/map_format.md, docs/checkpoints.md).
|
## doc["mode_spawns"] {modus: {"0"/"1"/"all": {points:[{id,pos}]}}} — Regeln 1:1
|
||||||
|
## wie SvmapLoader im Match-Server (V21–V24, docs/map_format.md).
|
||||||
|
## Alt-Namen: game_mode "checkpoints" und Feld "checkpoints" (= Conquest) werden
|
||||||
|
## beim Laden umbenannt (migrate).
|
||||||
|
|
||||||
const MODE_TDM := "tdm"
|
const MODE_TDM := "tdm"
|
||||||
const MODE_CHECKPOINTS := "checkpoints"
|
const MODE_CONQUEST := "conquest"
|
||||||
const MODES: Array[String] = [MODE_TDM, MODE_CHECKPOINTS]
|
const MODE_KOTH := "koth"
|
||||||
|
const MODE_DM := "dm"
|
||||||
|
const MODES: Array[String] = [MODE_TDM, MODE_CONQUEST, MODE_KOTH, MODE_DM]
|
||||||
const MODE_LABELS := {
|
const MODE_LABELS := {
|
||||||
MODE_TDM: "Team-Deathmatch",
|
MODE_TDM: "Team-Deathmatch",
|
||||||
MODE_CHECKPOINTS: "Checkpoints einnehmen",
|
MODE_CONQUEST: "Conquest (mehrere Zonen einnehmen)",
|
||||||
|
MODE_KOTH: "King of the Hill (ein wandernder Hill)",
|
||||||
|
MODE_DM: "Deathmatch (alle gegen alle)",
|
||||||
}
|
}
|
||||||
|
const MODE_SHORT := {
|
||||||
|
MODE_TDM: "Team-Deathmatch",
|
||||||
|
MODE_CONQUEST: "Conquest",
|
||||||
|
MODE_KOTH: "King of the Hill",
|
||||||
|
MODE_DM: "Deathmatch",
|
||||||
|
}
|
||||||
|
const MODE_ALIASES := {"checkpoints": MODE_CONQUEST}
|
||||||
|
## Feld mit den Zonen je Modus (nur Conquest und KotH haben welche).
|
||||||
|
const ZONE_KEYS := {MODE_CONQUEST: "conquest_zones", MODE_KOTH: "hills"}
|
||||||
|
const ZONE_NAMES := {MODE_CONQUEST: "Zone", MODE_KOTH: "Hill"}
|
||||||
|
const ZONE_PREFIX := {MODE_CONQUEST: "C", MODE_KOTH: "H"}
|
||||||
const DEFAULT_RADIUS := 15.0
|
const DEFAULT_RADIUS := 15.0
|
||||||
const MIN_RADIUS := 3.0
|
const MIN_RADIUS := 3.0
|
||||||
const MAX_RADIUS := 60.0
|
const MAX_RADIUS := 60.0
|
||||||
|
const SPAWN_SET_BASE := "base"
|
||||||
|
|
||||||
|
|
||||||
|
static func normalize(m: String) -> String:
|
||||||
|
return str(MODE_ALIASES.get(m, m))
|
||||||
|
|
||||||
|
|
||||||
static func mode_of(doc: Dictionary) -> String:
|
static func mode_of(doc: Dictionary) -> String:
|
||||||
var m := str(doc.get("game_mode", MODE_TDM))
|
var m := normalize(str(doc.get("game_mode", MODE_TDM)))
|
||||||
return m if MODES.has(m) else MODE_TDM
|
return m if MODES.has(m) else MODE_TDM
|
||||||
|
|
||||||
|
|
||||||
static func checkpoints(doc: Dictionary) -> Array:
|
static func has_zones(mode: String) -> bool:
|
||||||
var c: Variant = doc.get("checkpoints", [])
|
return ZONE_KEYS.has(mode)
|
||||||
|
|
||||||
|
|
||||||
|
## Zonen/Hills des Modus (leer für Modi ohne Zonen).
|
||||||
|
static func zones(doc: Dictionary, mode: String) -> Array:
|
||||||
|
if not ZONE_KEYS.has(mode):
|
||||||
|
return []
|
||||||
|
var c: Variant = doc.get(ZONE_KEYS[mode], [])
|
||||||
return c if c is Array else []
|
return c if c is Array else []
|
||||||
|
|
||||||
|
|
||||||
## Nächste freie ID "C1", "C2", …
|
## Nächste freie ID "C1", "C2", … (Conquest) bzw. "H1", … (Hills).
|
||||||
static func next_id(doc: Dictionary) -> String:
|
static func next_id(doc: Dictionary, mode: String) -> String:
|
||||||
var used := {}
|
var used := {}
|
||||||
for c in checkpoints(doc):
|
for c in zones(doc, mode):
|
||||||
if c is Dictionary:
|
if c is Dictionary:
|
||||||
used[str(c.get("id", ""))] = true
|
used[str(c.get("id", ""))] = true
|
||||||
|
var prefix: String = ZONE_PREFIX.get(mode, "Z")
|
||||||
var i := 1
|
var i := 1
|
||||||
while used.has("C%d" % i):
|
while used.has("%s%d" % [prefix, i]):
|
||||||
i += 1
|
i += 1
|
||||||
return "C%d" % i
|
return "%s%d" % [prefix, i]
|
||||||
|
|
||||||
|
|
||||||
## Neuer Checkpoint auf dem Boden (y = Terrain-Höhe).
|
## Neue Zone auf dem Boden (y = Terrain-Höhe).
|
||||||
static func make_checkpoint(doc: Dictionary, x: float, z: float, label := "") -> Dictionary:
|
static func make_zone(doc: Dictionary, mode: String, x: float, z: float, label := "") -> Dictionary:
|
||||||
var id := next_id(doc)
|
var id := next_id(doc, mode)
|
||||||
return {
|
return {
|
||||||
"id": id,
|
"id": id,
|
||||||
"label": label if label != "" else "Zone %s" % id.substr(1),
|
"label": label if label != "" else "%s %s" % [ZONE_NAMES.get(mode, "Zone"), id.substr(1)],
|
||||||
"pos": [x, SvmapDocument.terrain_height_at(doc, x, z), z],
|
"pos": [x, SvmapDocument.terrain_height_at(doc, x, z), z],
|
||||||
"radius_m": DEFAULT_RADIUS,
|
"radius_m": DEFAULT_RADIUS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
## Drei Start-Zonen zwischen den Basen (West–Ost-Linie), für neue Checkpoint-Maps.
|
## Drei Start-Zonen zwischen den Basen (West–Ost-Linie) für neue Maps des Modus.
|
||||||
static func default_checkpoints(doc: Dictionary) -> Array:
|
static func default_zones(doc: Dictionary, mode: String) -> Array:
|
||||||
var size := float(doc.get("terrain", {}).get("size_x_m", 1024.0))
|
var size := float(doc.get("terrain", {}).get("size_x_m", 1024.0))
|
||||||
var out: Array = []
|
var out: Array = []
|
||||||
var names := ["Westposten", "Mitte", "Ostposten"]
|
var names := ["Westposten", "Mitte", "Ostposten"] if mode == MODE_CONQUEST else ["Westhang", "Talmitte", "Osthang"]
|
||||||
var xs := [-0.25, 0.0, 0.25]
|
var xs := [-0.25, 0.0, 0.25]
|
||||||
for i in 3:
|
for i in 3:
|
||||||
var cp := make_checkpoint({"checkpoints": out, "terrain": doc.get("terrain", {})}, xs[i] * size, 0.0, names[i])
|
var probe := {ZONE_KEYS[mode]: out, "terrain": doc.get("terrain", {})}
|
||||||
out.append(cp)
|
out.append(make_zone(probe, mode, xs[i] * size, 0.0, names[i]))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Alte Namen auf die heutigen umstellen (nach erfolgreichem Laden).
|
||||||
|
static func migrate(doc: Dictionary) -> void:
|
||||||
|
if str(doc.get("game_mode", "")) == "checkpoints":
|
||||||
|
doc["game_mode"] = MODE_CONQUEST
|
||||||
|
if doc.has("checkpoints"):
|
||||||
|
if not doc.has("conquest_zones"):
|
||||||
|
doc["conquest_zones"] = doc["checkpoints"]
|
||||||
|
doc.erase("checkpoints")
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------- Modus-Spawns --
|
||||||
|
|
||||||
|
## Team-Schlüssel eines Spawn-Satzes: Deathmatch hat nur "all".
|
||||||
|
static func spawn_keys(mode: String) -> Array:
|
||||||
|
return ["all"] if mode == MODE_DM else ["0", "1"]
|
||||||
|
|
||||||
|
|
||||||
|
static func spawn_set(doc: Dictionary, mode: String) -> Dictionary:
|
||||||
|
var ms: Variant = doc.get("mode_spawns", {})
|
||||||
|
if ms is Dictionary and (ms as Dictionary).get(mode, null) is Dictionary:
|
||||||
|
return (ms as Dictionary)[mode]
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
static func has_own_spawns(doc: Dictionary, mode: String) -> bool:
|
||||||
|
return not spawn_set(doc, mode).is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
## Eigene Spawns für den Modus anlegen: Kopie der Basis-Spawns (Deathmatch: beide
|
||||||
|
## Teams in einem Topf). IDs bekommen den Modus-Präfix, damit sie map-weit eindeutig sind.
|
||||||
|
static func create_spawn_set(doc: Dictionary, mode: String) -> void:
|
||||||
|
var ms: Dictionary = doc.get("mode_spawns", {}) if doc.get("mode_spawns", {}) is Dictionary else {}
|
||||||
|
var base: Dictionary = doc.get("spawn_zones", {})
|
||||||
|
var out := {}
|
||||||
|
var tag := mode.to_upper().substr(0, 1) if mode != MODE_DM else "D"
|
||||||
|
var n := 0
|
||||||
|
for key in spawn_keys(mode):
|
||||||
|
var pts: Array = []
|
||||||
|
var src_teams: Array = ["0", "1"] if mode == MODE_DM else [key]
|
||||||
|
for t in src_teams:
|
||||||
|
var z: Variant = base.get(t, {})
|
||||||
|
if not (z is Dictionary):
|
||||||
|
continue
|
||||||
|
for p in (z as Dictionary).get("points", []):
|
||||||
|
if p is Dictionary:
|
||||||
|
n += 1
|
||||||
|
pts.append({"id": "%s%s%d" % [tag, "" if mode == MODE_DM else ("A" if t == "0" else "B"), n],
|
||||||
|
"pos": (p.get("pos", [0, 0, 0]) as Array).duplicate()})
|
||||||
|
out[key] = {"points": pts}
|
||||||
|
ms[mode] = out
|
||||||
|
doc["mode_spawns"] = ms
|
||||||
|
|
||||||
|
|
||||||
|
static func remove_spawn_set(doc: Dictionary, mode: String) -> void:
|
||||||
|
var ms: Variant = doc.get("mode_spawns", null)
|
||||||
|
if not (ms is Dictionary):
|
||||||
|
return
|
||||||
|
(ms as Dictionary).erase(mode)
|
||||||
|
if (ms as Dictionary).is_empty():
|
||||||
|
doc.erase("mode_spawns")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Validierung --
|
||||||
|
|
||||||
static func validate(data: Dictionary, bounds: Dictionary) -> PackedStringArray:
|
static func validate(data: Dictionary, bounds: Dictionary) -> PackedStringArray:
|
||||||
var errors := PackedStringArray()
|
var errors := PackedStringArray()
|
||||||
var mode = data.get("game_mode", "")
|
var raw = data.get("game_mode", "")
|
||||||
if mode != null and str(mode) != "" and not MODES.has(str(mode)):
|
var mode := normalize(str(raw)) if raw != null else ""
|
||||||
errors.append("V21: game_mode '%s' unbekannt (%s)." % [str(mode), ", ".join(MODES)])
|
if mode != "" and not MODES.has(mode):
|
||||||
var cps = data.get("checkpoints", [])
|
errors.append("V21: game_mode '%s' unbekannt (%s)." % [mode, ", ".join(MODES)])
|
||||||
if not (cps is Array):
|
var cz = data.get("conquest_zones", data.get("checkpoints", []))
|
||||||
errors.append("V22: checkpoints muss ein Array sein.")
|
errors.append_array(_validate_zone_list(cz, "V22", "Conquest-Zone", bounds))
|
||||||
|
if mode == MODE_CONQUEST and cz is Array and (cz as Array).is_empty():
|
||||||
|
errors.append("V21: game_mode 'conquest' braucht mindestens eine Zone.")
|
||||||
|
var hl = data.get("hills", [])
|
||||||
|
errors.append_array(_validate_zone_list(hl, "V23", "Hill", bounds))
|
||||||
|
if mode == MODE_KOTH and hl is Array and (hl as Array).is_empty():
|
||||||
|
errors.append("V21: game_mode 'koth' braucht mindestens einen Hill.")
|
||||||
|
errors.append_array(_validate_mode_spawns(data.get("mode_spawns", null), bounds))
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
static func _validate_zone_list(cps, code: String, what: String, bounds: Dictionary) -> PackedStringArray:
|
||||||
|
var errors := PackedStringArray()
|
||||||
|
if not (cps is Array):
|
||||||
|
errors.append("%s: %s-Liste muss ein Array sein." % [code, what])
|
||||||
return errors
|
return errors
|
||||||
if str(mode) == MODE_CHECKPOINTS and (cps as Array).is_empty():
|
|
||||||
errors.append("V21: game_mode 'checkpoints' braucht mindestens einen Checkpoint.")
|
|
||||||
var seen := {}
|
var seen := {}
|
||||||
for i in (cps as Array).size():
|
for i in (cps as Array).size():
|
||||||
var c = cps[i]
|
var c = cps[i]
|
||||||
if not (c is Dictionary):
|
if not (c is Dictionary):
|
||||||
errors.append("V22: Checkpoint %d ist kein Objekt." % i)
|
errors.append("%s: %s %d ist kein Objekt." % [code, what, i])
|
||||||
continue
|
continue
|
||||||
var cid = c.get("id", "")
|
var cid = c.get("id", "")
|
||||||
if not (cid is String) or (cid as String).strip_edges().is_empty():
|
if not (cid is String) or (cid as String).strip_edges().is_empty():
|
||||||
errors.append("V22: Checkpoint %d: id fehlt/leer." % i)
|
errors.append("%s: %s %d: id fehlt/leer." % [code, what, i])
|
||||||
elif seen.has(cid):
|
elif seen.has(cid):
|
||||||
errors.append("V22: Checkpoint-ID '%s' ist doppelt." % cid)
|
errors.append("%s: %s-ID '%s' ist doppelt." % [code, what, cid])
|
||||||
else:
|
else:
|
||||||
seen[cid] = true
|
seen[cid] = true
|
||||||
var pos = c.get("pos", null)
|
var pos = c.get("pos", null)
|
||||||
if not (pos is Array) or (pos as Array).size() != 3 \
|
if not (pos is Array) or (pos as Array).size() != 3 \
|
||||||
or not _num(pos[0]) or not _num(pos[1]) or not _num(pos[2]):
|
or not _num(pos[0]) or not _num(pos[1]) or not _num(pos[2]):
|
||||||
errors.append("V22: Checkpoint '%s': pos muss [x, y, z] mit endlichen Werten sein." % str(cid))
|
errors.append("%s: %s '%s': pos muss [x, y, z] mit endlichen Werten sein." % [code, what, str(cid)])
|
||||||
elif not _inside(pos, bounds):
|
elif not _inside(pos, bounds):
|
||||||
errors.append("V22: Checkpoint '%s' liegt außerhalb der Match-Grenzen." % str(cid))
|
errors.append("%s: %s '%s' liegt außerhalb der Match-Grenzen." % [code, what, str(cid)])
|
||||||
var r = c.get("radius_m", DEFAULT_RADIUS)
|
var r = c.get("radius_m", DEFAULT_RADIUS)
|
||||||
if not _num(r) or float(r) < MIN_RADIUS or float(r) > MAX_RADIUS:
|
if not _num(r) or float(r) < MIN_RADIUS or float(r) > MAX_RADIUS:
|
||||||
errors.append("V22: Checkpoint '%s': radius_m muss 3–60 sein." % str(cid))
|
errors.append("%s: %s '%s': radius_m muss 3–60 sein." % [code, what, str(cid)])
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
static func _validate_mode_spawns(ms, bounds: Dictionary) -> PackedStringArray:
|
||||||
|
var errors := PackedStringArray()
|
||||||
|
if ms == null:
|
||||||
|
return errors
|
||||||
|
if not (ms is Dictionary):
|
||||||
|
return PackedStringArray(["V24: mode_spawns muss ein Objekt sein."])
|
||||||
|
for mode in (ms as Dictionary).keys():
|
||||||
|
if not MODES.has(str(mode)):
|
||||||
|
errors.append("V24: mode_spawns: Modus '%s' unbekannt." % str(mode))
|
||||||
|
continue
|
||||||
|
var m = ms[mode]
|
||||||
|
if not (m is Dictionary):
|
||||||
|
errors.append("V24: mode_spawns.%s muss ein Objekt sein." % str(mode))
|
||||||
|
continue
|
||||||
|
var seen := {}
|
||||||
|
for key in (m as Dictionary).keys():
|
||||||
|
var allowed := spawn_keys(str(mode))
|
||||||
|
if not allowed.has(str(key)):
|
||||||
|
errors.append("V24: mode_spawns.%s: Schlüssel '%s' nicht erlaubt (%s)." % [str(mode), str(key), ", ".join(allowed)])
|
||||||
|
continue
|
||||||
|
var pts = m[key].get("points", []) if m[key] is Dictionary else null
|
||||||
|
if not (pts is Array) or (pts as Array).is_empty():
|
||||||
|
errors.append("V24: mode_spawns.%s.%s braucht mindestens einen Punkt." % [str(mode), str(key)])
|
||||||
|
continue
|
||||||
|
for i in (pts as Array).size():
|
||||||
|
var p = pts[i]
|
||||||
|
var pid = p.get("id", "") if p is Dictionary else ""
|
||||||
|
if not (pid is String) or (pid as String).strip_edges().is_empty():
|
||||||
|
errors.append("V24: mode_spawns.%s.%s Punkt %d: id fehlt/leer." % [str(mode), str(key), i])
|
||||||
|
continue
|
||||||
|
if seen.has(pid):
|
||||||
|
errors.append("V24: Spawn-ID '%s' ist doppelt (Modus %s)." % [pid, str(mode)])
|
||||||
|
seen[pid] = true
|
||||||
|
var pos = p.get("pos", null)
|
||||||
|
if not (pos is Array) or (pos as Array).size() != 3 \
|
||||||
|
or not _num(pos[0]) or not _num(pos[1]) or not _num(pos[2]):
|
||||||
|
errors.append("V24: Spawn '%s': pos muss [x, y, z] mit endlichen Werten sein." % pid)
|
||||||
|
elif not _inside(pos, bounds):
|
||||||
|
errors.append("V24: Spawn '%s' liegt außerhalb der Match-Grenzen." % pid)
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ static func parse_and_validate(text: String, manifest: Dictionary = {}) -> Dicti
|
||||||
if not errors.is_empty():
|
if not errors.is_empty():
|
||||||
result["errors"] = Array(errors)
|
result["errors"] = Array(errors)
|
||||||
return result
|
return result
|
||||||
|
ModusKatalog.migrate(data) # Alt-Namen (checkpoints → conquest) NACH der Prüfsumme umstellen
|
||||||
result["ok"] = true
|
result["ok"] = true
|
||||||
result["data"] = data
|
result["data"] = data
|
||||||
return result
|
return result
|
||||||
|
|
@ -222,7 +223,7 @@ static func _validate_core(data: Dictionary, manifest: Dictionary = {}) -> Packe
|
||||||
str(weather_sky), ", ".join(ids)])
|
str(weather_sky), ", ".join(ids)])
|
||||||
# V18: loadout (optional) — festes Map-Loadout, siehe LoadoutKatalog.
|
# V18: loadout (optional) — festes Map-Loadout, siehe LoadoutKatalog.
|
||||||
errors.append_array(LoadoutKatalog.validate(data.get("loadout", null)))
|
errors.append_array(LoadoutKatalog.validate(data.get("loadout", null)))
|
||||||
# V21/V22: Spielmodus + Checkpoints (optional), siehe ModusKatalog.
|
# V21–V24: Spielmodus, Zonen, Hills, Modus-Spawns (optional), siehe ModusKatalog.
|
||||||
errors.append_array(ModusKatalog.validate(data, bounds))
|
errors.append_array(ModusKatalog.validate(data, bounds))
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ func _init() -> void:
|
||||||
for m in ModusKatalog.MODES:
|
for m in ModusKatalog.MODES:
|
||||||
mode_option.add_item(ModusKatalog.MODE_LABELS[m])
|
mode_option.add_item(ModusKatalog.MODE_LABELS[m])
|
||||||
mode_option.custom_minimum_size = UIScale.v2(0, 40)
|
mode_option.custom_minimum_size = UIScale.v2(0, 40)
|
||||||
mode_option.tooltip_text = "Team-Deathmatch: Kills zählen. Checkpoints: Teams nehmen Zonen ein und sammeln Punkte. Lässt sich später im Map-Tab ändern."
|
mode_option.tooltip_text = "Team-Deathmatch: Kills zählen. Conquest: Teams nehmen mehrere Zonen ein. King of the Hill: ein Hill, der wandert. Deathmatch: alle gegen alle. Lässt sich später im Map-Tab ändern."
|
||||||
vbox.add_child(mode_option)
|
vbox.add_child(mode_option)
|
||||||
|
|
||||||
var hint := _make_label("Die Map startet leer mit 4 Spawn-Punkten pro Team\n(Blau im Westen, Rot im Osten) — beides lässt sich später ändern.")
|
var hint := _make_label("Die Map startet leer mit 4 Spawn-Punkten pro Team\n(Blau im Westen, Rot im Osten) — beides lässt sich später ändern.")
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
extends "res://src/ui/panels/editor_panel.gd"
|
extends "res://src/ui/panels/editor_panel.gd"
|
||||||
## Spielmodus-Bereich im Map-Tab (Karte Checkpoint-Modus, 28.10.): Modus der Map
|
## Spielmodus-Bereich im Map-Tab (28.10.): Modus der Map wählen (Team-Deathmatch /
|
||||||
## wählen (Team-Deathmatch / Checkpoints) und Checkpoint-Zonen setzen. Die Zonen
|
## Conquest / King of the Hill / Deathmatch), Zonen bzw. Hills setzen und eigene
|
||||||
## erscheinen als farbige Kreise im 3D-Bild. Gespeichert als optionale Felder
|
## Spawns je Modus anlegen. Zonen erscheinen als farbige Kreise im 3D-Bild.
|
||||||
## doc["game_mode"] und doc["checkpoints"] (Regeln: src/svmap/modus_katalog.gd).
|
## Gespeichert als optionale Felder doc["game_mode"], doc["conquest_zones"],
|
||||||
|
## doc["hills"], doc["mode_spawns"] (Regeln: src/svmap/modus_katalog.gd).
|
||||||
##
|
##
|
||||||
## Jede Änderung ist ein Undo-Schritt (History-Blöcke game_mode + checkpoints;
|
## Jede Änderung ist ein Undo-Schritt (History-Blöcke; fehlt ein Block, gilt der
|
||||||
## fehlt der Block, gilt TDM bzw. keine Zonen).
|
## Standard: TDM, keine Zonen, keine eigenen Spawns).
|
||||||
|
|
||||||
var mode_option: OptionButton
|
var mode_option: OptionButton
|
||||||
var cp_box: VBoxContainer
|
var cp_box: VBoxContainer # Zonen-/Hill-Bereich (nur Conquest und KotH)
|
||||||
|
var zones_title: Label
|
||||||
var rows_box: VBoxContainer
|
var rows_box: VBoxContainer
|
||||||
var add_button: Button
|
var add_button: Button
|
||||||
var hint_label: Label
|
var hint_label: Label
|
||||||
|
var spawn_option: OptionButton # Spawn-Satz, der mit dem Spawn-Werkzeug bearbeitet wird
|
||||||
|
var spawn_create: Button
|
||||||
|
var spawn_remove: Button
|
||||||
|
var spawn_hint: Label
|
||||||
|
|
||||||
var _loading := false
|
var _loading := false
|
||||||
|
|
||||||
|
|
@ -30,7 +36,7 @@ func build() -> void:
|
||||||
|
|
||||||
mode_option = OptionButton.new()
|
mode_option = OptionButton.new()
|
||||||
mode_option.name = "ModusAuswahl"
|
mode_option.name = "ModusAuswahl"
|
||||||
mode_option.tooltip_text = "Team-Deathmatch: Kills zählen. Checkpoints: Die Teams nehmen Zonen ein und sammeln Punkte, solange sie sie halten."
|
mode_option.tooltip_text = "Team-Deathmatch: Kills zählen. Conquest: Teams nehmen mehrere Zonen ein. King of the Hill: ein Hill, der wandert. Deathmatch: alle gegen alle."
|
||||||
for m in ModusKatalog.MODES:
|
for m in ModusKatalog.MODES:
|
||||||
mode_option.add_item(ModusKatalog.MODE_LABELS[m])
|
mode_option.add_item(ModusKatalog.MODE_LABELS[m])
|
||||||
add_child(mode_option)
|
add_child(mode_option)
|
||||||
|
|
@ -39,24 +45,48 @@ func build() -> void:
|
||||||
cp_box.add_theme_constant_override("separation", UIScale.px(8))
|
cp_box.add_theme_constant_override("separation", UIScale.px(8))
|
||||||
add_child(cp_box)
|
add_child(cp_box)
|
||||||
|
|
||||||
cp_box.add_child(_field_label("Checkpoints"))
|
zones_title = _field_label("Zonen")
|
||||||
|
cp_box.add_child(zones_title)
|
||||||
rows_box = VBoxContainer.new()
|
rows_box = VBoxContainer.new()
|
||||||
rows_box.add_theme_constant_override("separation", UIScale.px(6))
|
rows_box.add_theme_constant_override("separation", UIScale.px(6))
|
||||||
cp_box.add_child(rows_box)
|
cp_box.add_child(rows_box)
|
||||||
|
|
||||||
add_button = Button.new()
|
add_button = Button.new()
|
||||||
add_button.name = "CheckpointNeu"
|
add_button.name = "ZoneNeu"
|
||||||
add_button.text = "+ Checkpoint in Bildmitte setzen"
|
add_button.text = "+ Zone in Bildmitte setzen"
|
||||||
add_button.tooltip_text = "Setzt eine neue Zone dorthin, wo die Kamera hinschaut. Position und Größe lassen sich darunter ändern."
|
add_button.tooltip_text = "Setzt eine neue Zone dorthin, wo die Kamera hinschaut. Position und Größe lassen sich darunter ändern."
|
||||||
cp_box.add_child(add_button)
|
cp_box.add_child(add_button)
|
||||||
|
|
||||||
hint_label = _muted_label("Mindestens ein Checkpoint nötig. Spieler spawnen zusätzlich an Zonen ihres Teams.")
|
hint_label = _muted_label("Mindestens eine Zone nötig.")
|
||||||
cp_box.add_child(hint_label)
|
cp_box.add_child(hint_label)
|
||||||
|
|
||||||
|
# Spawns je Modus
|
||||||
|
add_child(_field_label("Spawns bearbeiten für"))
|
||||||
|
spawn_option = OptionButton.new()
|
||||||
|
spawn_option.name = "SpawnSatz"
|
||||||
|
spawn_option.tooltip_text = "Welche Spawn-Punkte das Spawn-Werkzeug gerade zeigt und ändert. Ohne eigene Spawns nutzt ein Modus die Basis-Spawns."
|
||||||
|
add_child(spawn_option)
|
||||||
|
spawn_create = Button.new()
|
||||||
|
spawn_create.name = "SpawnSatzNeu"
|
||||||
|
spawn_create.tooltip_text = "Legt eigene Spawn-Punkte für diesen Modus an (Kopie der Basis-Spawns) — danach mit dem Spawn-Werkzeug anpassen."
|
||||||
|
add_child(spawn_create)
|
||||||
|
spawn_remove = Button.new()
|
||||||
|
spawn_remove.name = "SpawnSatzWeg"
|
||||||
|
spawn_remove.text = "Eigene Spawns löschen"
|
||||||
|
spawn_remove.tooltip_text = "Entfernt die eigenen Spawns dieses Modus — er nutzt danach wieder die Basis-Spawns."
|
||||||
|
add_child(spawn_remove)
|
||||||
|
spawn_hint = _muted_label("")
|
||||||
|
add_child(spawn_hint)
|
||||||
|
|
||||||
mode_option.item_selected.connect(func(i: int) -> void:
|
mode_option.item_selected.connect(func(i: int) -> void:
|
||||||
if not _loading:
|
if not _loading:
|
||||||
set_mode(ModusKatalog.MODES[i]))
|
set_mode(ModusKatalog.MODES[i]))
|
||||||
add_button.pressed.connect(func() -> void: add_checkpoint_at_view())
|
add_button.pressed.connect(func() -> void: add_zone_at_view())
|
||||||
|
spawn_option.item_selected.connect(func(i: int) -> void:
|
||||||
|
if not _loading:
|
||||||
|
select_spawn_set(_spawn_set_ids()[i]))
|
||||||
|
spawn_create.pressed.connect(func() -> void: create_spawns(SpawnOps.edit_set))
|
||||||
|
spawn_remove.pressed.connect(func() -> void: remove_spawns(SpawnOps.edit_set))
|
||||||
refresh_values()
|
refresh_values()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -70,50 +100,88 @@ func set_mode(mode: String) -> void:
|
||||||
main.doc.erase("game_mode")
|
main.doc.erase("game_mode")
|
||||||
else:
|
else:
|
||||||
main.doc["game_mode"] = mode
|
main.doc["game_mode"] = mode
|
||||||
if ModusKatalog.checkpoints(main.doc).is_empty():
|
if ModusKatalog.has_zones(mode) and ModusKatalog.zones(main.doc, mode).is_empty():
|
||||||
main.doc["checkpoints"] = ModusKatalog.default_checkpoints(main.doc)
|
main.doc[ModusKatalog.ZONE_KEYS[mode]] = ModusKatalog.default_zones(main.doc, mode)
|
||||||
_commit(before, "Spielmodus: %s" % ModusKatalog.MODE_LABELS[mode])
|
_commit(before, "Spielmodus: %s" % ModusKatalog.MODE_SHORT[mode])
|
||||||
|
|
||||||
|
|
||||||
func add_checkpoint_at_view() -> void:
|
func _zone_key() -> String:
|
||||||
if main.doc.is_empty():
|
return str(ModusKatalog.ZONE_KEYS.get(ModusKatalog.mode_of(main.doc), ""))
|
||||||
|
|
||||||
|
|
||||||
|
func add_zone_at_view() -> void:
|
||||||
|
if main.doc.is_empty() or _zone_key() == "":
|
||||||
return
|
return
|
||||||
var at := Vector3.ZERO
|
var at := Vector3.ZERO
|
||||||
if main.camera != null and "target" in main.camera:
|
if main.camera != null and "target" in main.camera:
|
||||||
at = main.camera.target
|
at = main.camera.target
|
||||||
var half := _half()
|
var half := _half()
|
||||||
add_checkpoint(clampf(at.x, -half.x, half.x), clampf(at.z, -half.y, half.y))
|
add_zone(clampf(at.x, -half.x, half.x), clampf(at.z, -half.y, half.y))
|
||||||
|
|
||||||
|
|
||||||
func add_checkpoint(x: float, z: float) -> String:
|
func add_zone(x: float, z: float) -> String:
|
||||||
|
var mode := ModusKatalog.mode_of(main.doc)
|
||||||
|
var key := _zone_key()
|
||||||
|
if key == "":
|
||||||
|
return ""
|
||||||
var before := _snap()
|
var before := _snap()
|
||||||
var cp := ModusKatalog.make_checkpoint(main.doc, x, z)
|
var zone := ModusKatalog.make_zone(main.doc, mode, x, z)
|
||||||
var list: Array = ModusKatalog.checkpoints(main.doc).duplicate(true)
|
var list: Array = ModusKatalog.zones(main.doc, mode).duplicate(true)
|
||||||
list.append(cp)
|
list.append(zone)
|
||||||
main.doc["checkpoints"] = list
|
main.doc[key] = list
|
||||||
_commit(before, "Checkpoint %s gesetzt" % cp["id"])
|
_commit(before, "%s %s gesetzt" % [ModusKatalog.ZONE_NAMES[mode], zone["id"]])
|
||||||
return str(cp["id"])
|
return str(zone["id"])
|
||||||
|
|
||||||
|
|
||||||
func remove_checkpoint(id: String) -> void:
|
func remove_zone(id: String) -> void:
|
||||||
|
var mode := ModusKatalog.mode_of(main.doc)
|
||||||
|
var key := _zone_key()
|
||||||
|
if key == "":
|
||||||
|
return
|
||||||
var before := _snap()
|
var before := _snap()
|
||||||
var list: Array = []
|
var list: Array = []
|
||||||
for c in ModusKatalog.checkpoints(main.doc):
|
for c in ModusKatalog.zones(main.doc, mode):
|
||||||
if str(c.get("id", "")) != id:
|
if str(c.get("id", "")) != id:
|
||||||
list.append((c as Dictionary).duplicate(true))
|
list.append((c as Dictionary).duplicate(true))
|
||||||
if list.size() == ModusKatalog.checkpoints(main.doc).size():
|
if list.size() == ModusKatalog.zones(main.doc, mode).size():
|
||||||
return
|
return
|
||||||
if list.is_empty():
|
if list.is_empty():
|
||||||
main.doc.erase("checkpoints")
|
main.doc.erase(key)
|
||||||
else:
|
else:
|
||||||
main.doc["checkpoints"] = list
|
main.doc[key] = list
|
||||||
_commit(before, "Checkpoint %s gelöscht" % id)
|
_commit(before, "%s %s gelöscht" % [ModusKatalog.ZONE_NAMES[mode], id])
|
||||||
|
|
||||||
|
|
||||||
## Name / Position / Radius eines Checkpoints ändern (nur übergebene Felder).
|
## King of the Hill: Reihenfolge der Hills ändern (dir = -1 hoch, +1 runter).
|
||||||
func edit_checkpoint(id: String, fields: Dictionary) -> void:
|
func move_zone(id: String, dir: int) -> void:
|
||||||
|
var mode := ModusKatalog.mode_of(main.doc)
|
||||||
|
var key := _zone_key()
|
||||||
|
if key == "":
|
||||||
|
return
|
||||||
|
var list: Array = ModusKatalog.zones(main.doc, mode).duplicate(true)
|
||||||
|
var idx := -1
|
||||||
|
for i in list.size():
|
||||||
|
if str(list[i].get("id", "")) == id:
|
||||||
|
idx = i
|
||||||
|
var to := idx + dir
|
||||||
|
if idx < 0 or to < 0 or to >= list.size():
|
||||||
|
return
|
||||||
var before := _snap()
|
var before := _snap()
|
||||||
var list: Array = ModusKatalog.checkpoints(main.doc).duplicate(true)
|
var tmp: Variant = list[idx]
|
||||||
|
list[idx] = list[to]
|
||||||
|
list[to] = tmp
|
||||||
|
main.doc[key] = list
|
||||||
|
_commit(before, "%s %s verschoben (Reihenfolge)" % [ModusKatalog.ZONE_NAMES[mode], id])
|
||||||
|
|
||||||
|
|
||||||
|
## Name / Position / Radius einer Zone ändern (nur übergebene Felder).
|
||||||
|
func edit_zone(id: String, fields: Dictionary) -> void:
|
||||||
|
var mode := ModusKatalog.mode_of(main.doc)
|
||||||
|
var key := _zone_key()
|
||||||
|
if key == "":
|
||||||
|
return
|
||||||
|
var before := _snap()
|
||||||
|
var list: Array = ModusKatalog.zones(main.doc, mode).duplicate(true)
|
||||||
for c in list:
|
for c in list:
|
||||||
if str(c.get("id", "")) != id:
|
if str(c.get("id", "")) != id:
|
||||||
continue
|
continue
|
||||||
|
|
@ -127,8 +195,57 @@ func edit_checkpoint(id: String, fields: Dictionary) -> void:
|
||||||
var nx := clampf(float(fields.get("x", p[0])), -half.x, half.x)
|
var nx := clampf(float(fields.get("x", p[0])), -half.x, half.x)
|
||||||
var nz := clampf(float(fields.get("z", p[2])), -half.y, half.y)
|
var nz := clampf(float(fields.get("z", p[2])), -half.y, half.y)
|
||||||
c["pos"] = [nx, SvmapDocument.terrain_height_at(main.doc, nx, nz), nz]
|
c["pos"] = [nx, SvmapDocument.terrain_height_at(main.doc, nx, nz), nz]
|
||||||
main.doc["checkpoints"] = list
|
main.doc[key] = list
|
||||||
_commit(before, "Checkpoint %s geändert" % id, "cp_%s" % id if fields.has("x") or fields.has("z") or fields.has("radius_m") else "")
|
_commit(before, "%s %s geändert" % [ModusKatalog.ZONE_NAMES[mode], id], "cp_%s" % id if fields.has("x") or fields.has("z") or fields.has("radius_m") else "")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Spawn-Sätze ----
|
||||||
|
|
||||||
|
func _spawn_set_ids() -> Array:
|
||||||
|
var out: Array = [ModusKatalog.SPAWN_SET_BASE]
|
||||||
|
out.append_array(ModusKatalog.MODES)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func select_spawn_set(set_id: String) -> void:
|
||||||
|
if not _spawn_set_ids().has(set_id):
|
||||||
|
return
|
||||||
|
SpawnOps.edit_set = set_id
|
||||||
|
_refresh_spawn_view()
|
||||||
|
refresh_values()
|
||||||
|
main.call("_set_status", "Spawn-Satz: %s." % _set_label(set_id))
|
||||||
|
|
||||||
|
|
||||||
|
func create_spawns(mode: String) -> void:
|
||||||
|
if main.doc.is_empty() or mode == ModusKatalog.SPAWN_SET_BASE or ModusKatalog.has_own_spawns(main.doc, mode):
|
||||||
|
return
|
||||||
|
var before := _snap()
|
||||||
|
ModusKatalog.create_spawn_set(main.doc, mode)
|
||||||
|
_commit(before, "Eigene Spawns für %s angelegt" % ModusKatalog.MODE_SHORT[mode])
|
||||||
|
_refresh_spawn_view()
|
||||||
|
|
||||||
|
|
||||||
|
func remove_spawns(mode: String) -> void:
|
||||||
|
if main.doc.is_empty() or mode == ModusKatalog.SPAWN_SET_BASE or not ModusKatalog.has_own_spawns(main.doc, mode):
|
||||||
|
return
|
||||||
|
var before := _snap()
|
||||||
|
ModusKatalog.remove_spawn_set(main.doc, mode)
|
||||||
|
SpawnOps.edit_set = ModusKatalog.SPAWN_SET_BASE
|
||||||
|
_commit(before, "Eigene Spawns für %s gelöscht" % ModusKatalog.MODE_SHORT[mode])
|
||||||
|
_refresh_spawn_view()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_spawn_view() -> void:
|
||||||
|
if main.view_builder != null:
|
||||||
|
main.view_builder.call("_rebuild_spawn_markers", main.doc)
|
||||||
|
if "spawn_tool" in main and main.spawn_tool != null:
|
||||||
|
main.spawn_tool.call("refresh_from_doc")
|
||||||
|
|
||||||
|
|
||||||
|
func _set_label(set_id: String) -> String:
|
||||||
|
if set_id == ModusKatalog.SPAWN_SET_BASE:
|
||||||
|
return "Basis (für alle Modi ohne eigene Spawns)"
|
||||||
|
return "%s%s" % [ModusKatalog.MODE_SHORT[set_id], " ✔ eigene" if ModusKatalog.has_own_spawns(main.doc, set_id) else ""]
|
||||||
|
|
||||||
|
|
||||||
func _half() -> Vector2:
|
func _half() -> Vector2:
|
||||||
|
|
@ -138,7 +255,7 @@ func _half() -> Vector2:
|
||||||
|
|
||||||
func _snap() -> Dictionary:
|
func _snap() -> Dictionary:
|
||||||
var out := {}
|
var out := {}
|
||||||
for key in ["game_mode", "checkpoints"]:
|
for key in ["game_mode", "conquest_zones", "hills", "mode_spawns"]:
|
||||||
var v: Variant = main.doc.get(key, null)
|
var v: Variant = main.doc.get(key, null)
|
||||||
out[key] = v.duplicate(true) if v is Array or v is Dictionary else v
|
out[key] = v.duplicate(true) if v is Array or v is Dictionary else v
|
||||||
return out
|
return out
|
||||||
|
|
@ -153,7 +270,7 @@ func _commit(before: Dictionary, text: String, merge_tag := "") -> void:
|
||||||
main.call("_refresh_file_label")
|
main.call("_refresh_file_label")
|
||||||
main.call("_refresh_history_buttons")
|
main.call("_refresh_history_buttons")
|
||||||
if main.view_builder != null:
|
if main.view_builder != null:
|
||||||
main.view_builder.call("rebuild_checkpoint_markers", main.doc)
|
main.view_builder.call("rebuild_zone_markers", main.doc)
|
||||||
refresh_values()
|
refresh_values()
|
||||||
main.call("_set_status", "%s." % text)
|
main.call("_set_status", "%s." % text)
|
||||||
|
|
||||||
|
|
@ -165,29 +282,58 @@ func refresh_values() -> void:
|
||||||
if mode_option == null or main == null or main.doc.is_empty():
|
if mode_option == null or main == null or main.doc.is_empty():
|
||||||
return
|
return
|
||||||
_loading = true
|
_loading = true
|
||||||
var checkpoints_mode := ModusKatalog.mode_of(main.doc) == ModusKatalog.MODE_CHECKPOINTS
|
var mode := ModusKatalog.mode_of(main.doc)
|
||||||
mode_option.select(ModusKatalog.MODES.find(ModusKatalog.mode_of(main.doc)))
|
mode_option.select(ModusKatalog.MODES.find(mode))
|
||||||
cp_box.visible = checkpoints_mode
|
cp_box.visible = ModusKatalog.has_zones(mode)
|
||||||
|
if cp_box.visible:
|
||||||
|
var nm: String = ModusKatalog.ZONE_NAMES[mode]
|
||||||
|
zones_title.text = "Hills (Reihenfolge = Wanderreihenfolge)" if mode == ModusKatalog.MODE_KOTH else "Zonen"
|
||||||
|
add_button.text = "+ %s in Bildmitte setzen" % nm
|
||||||
|
hint_label.text = "Mindestens %s nötig. %s" % [
|
||||||
|
"einen Hill" if mode == ModusKatalog.MODE_KOTH else "eine Zone",
|
||||||
|
"Der Server wechselt nach der eingestellten Zeit zum nächsten Hill (oder zufällig)." if mode == ModusKatalog.MODE_KOTH
|
||||||
|
else "Spieler spawnen zusätzlich an Zonen ihres Teams."]
|
||||||
for c in rows_box.get_children():
|
for c in rows_box.get_children():
|
||||||
rows_box.remove_child(c)
|
rows_box.remove_child(c)
|
||||||
c.queue_free()
|
c.queue_free()
|
||||||
for cp in ModusKatalog.checkpoints(main.doc):
|
var zl := ModusKatalog.zones(main.doc, mode)
|
||||||
if cp is Dictionary:
|
for i in zl.size():
|
||||||
rows_box.add_child(_row(cp))
|
if zl[i] is Dictionary:
|
||||||
hint_label.visible = ModusKatalog.checkpoints(main.doc).is_empty()
|
rows_box.add_child(_row(zl[i], i, zl.size(), mode))
|
||||||
|
hint_label.visible = zl.is_empty()
|
||||||
|
# Spawn-Sätze
|
||||||
|
if not _spawn_set_ids().has(SpawnOps.edit_set):
|
||||||
|
SpawnOps.edit_set = ModusKatalog.SPAWN_SET_BASE
|
||||||
|
spawn_option.clear()
|
||||||
|
for sid in _spawn_set_ids():
|
||||||
|
spawn_option.add_item(_set_label(sid))
|
||||||
|
spawn_option.select(_spawn_set_ids().find(SpawnOps.edit_set))
|
||||||
|
var base := SpawnOps.edit_set == ModusKatalog.SPAWN_SET_BASE
|
||||||
|
var own := not base and ModusKatalog.has_own_spawns(main.doc, SpawnOps.edit_set)
|
||||||
|
spawn_create.visible = not base and not own
|
||||||
|
spawn_create.text = "Eigene Spawns für %s anlegen" % (ModusKatalog.MODE_SHORT[SpawnOps.edit_set] if not base else "")
|
||||||
|
spawn_remove.visible = own
|
||||||
|
if base:
|
||||||
|
spawn_hint.text = "Basis-Spawns gelten für jeden Modus ohne eigene Spawns."
|
||||||
|
elif own:
|
||||||
|
spawn_hint.text = "Das Spawn-Werkzeug ändert jetzt die eigenen Spawns dieses Modus."
|
||||||
|
else:
|
||||||
|
spawn_hint.text = "%s nutzt noch die Basis-Spawns. Mit dem Knopf legst du eigene an." % ModusKatalog.MODE_SHORT[SpawnOps.edit_set]
|
||||||
|
if SpawnOps.edit_set == ModusKatalog.MODE_DM and own:
|
||||||
|
spawn_hint.text += " Deathmatch hat keine Teams — alle Punkte gelten für alle."
|
||||||
_loading = false
|
_loading = false
|
||||||
|
|
||||||
|
|
||||||
func _row(cp: Dictionary) -> Control:
|
func _row(cp: Dictionary, index := 0, count := 1, mode := "conquest") -> Control:
|
||||||
var id := str(cp.get("id", ""))
|
var id := str(cp.get("id", ""))
|
||||||
var pos: Array = cp.get("pos", [0, 0, 0])
|
var pos: Array = cp.get("pos", [0, 0, 0])
|
||||||
var half := _half()
|
var half := _half()
|
||||||
var box := VBoxContainer.new()
|
var box := VBoxContainer.new()
|
||||||
box.name = "CP_%s" % id
|
box.name = "Z_%s" % id
|
||||||
box.add_theme_constant_override("separation", UIScale.px(4))
|
box.add_theme_constant_override("separation", UIScale.px(4))
|
||||||
|
|
||||||
var top := HBoxContainer.new()
|
var top := HBoxContainer.new()
|
||||||
var idl := _field_label(id)
|
var idl := _field_label("%d·%s" % [index + 1, id] if mode == ModusKatalog.MODE_KOTH else id)
|
||||||
idl.custom_minimum_size = UIScale.v2(32, 0)
|
idl.custom_minimum_size = UIScale.v2(32, 0)
|
||||||
top.add_child(idl)
|
top.add_child(idl)
|
||||||
var name_edit := LineEdit.new()
|
var name_edit := LineEdit.new()
|
||||||
|
|
@ -197,16 +343,31 @@ func _row(cp: Dictionary) -> Control:
|
||||||
name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
name_edit.text_submitted.connect(func(t: String) -> void:
|
name_edit.text_submitted.connect(func(t: String) -> void:
|
||||||
if not _loading:
|
if not _loading:
|
||||||
edit_checkpoint(id, {"label": t}))
|
edit_zone(id, {"label": t}))
|
||||||
name_edit.focus_exited.connect(func() -> void:
|
name_edit.focus_exited.connect(func() -> void:
|
||||||
if not _loading and name_edit.text != str(cp.get("label", id)):
|
if not _loading and name_edit.text != str(cp.get("label", id)):
|
||||||
edit_checkpoint(id, {"label": name_edit.text}))
|
edit_zone(id, {"label": name_edit.text}))
|
||||||
top.add_child(name_edit)
|
top.add_child(name_edit)
|
||||||
|
if mode == ModusKatalog.MODE_KOTH:
|
||||||
|
var up := Button.new()
|
||||||
|
up.name = "Hoch"
|
||||||
|
up.text = "▲"
|
||||||
|
up.tooltip_text = "Früher drankommen"
|
||||||
|
up.disabled = index == 0
|
||||||
|
up.pressed.connect(func() -> void: move_zone(id, -1))
|
||||||
|
top.add_child(up)
|
||||||
|
var dn := Button.new()
|
||||||
|
dn.name = "Runter"
|
||||||
|
dn.text = "▼"
|
||||||
|
dn.tooltip_text = "Später drankommen"
|
||||||
|
dn.disabled = index >= count - 1
|
||||||
|
dn.pressed.connect(func() -> void: move_zone(id, 1))
|
||||||
|
top.add_child(dn)
|
||||||
var del := Button.new()
|
var del := Button.new()
|
||||||
del.name = "Loeschen"
|
del.name = "Loeschen"
|
||||||
del.text = "✕"
|
del.text = "✕"
|
||||||
del.tooltip_text = "Checkpoint löschen"
|
del.tooltip_text = "Löschen"
|
||||||
del.pressed.connect(func() -> void: remove_checkpoint(id))
|
del.pressed.connect(func() -> void: remove_zone(id))
|
||||||
top.add_child(del)
|
top.add_child(del)
|
||||||
box.add_child(top)
|
box.add_child(top)
|
||||||
|
|
||||||
|
|
@ -233,6 +394,6 @@ func _spin(label: String, field: String, id: String, value: float, mn: float, mx
|
||||||
sb.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
sb.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
sb.value_changed.connect(func(v: float) -> void:
|
sb.value_changed.connect(func(v: float) -> void:
|
||||||
if not _loading:
|
if not _loading:
|
||||||
edit_checkpoint(id, {field: v}))
|
edit_zone(id, {field: v}))
|
||||||
h.add_child(sb)
|
h.add_child(sb)
|
||||||
return h
|
return h
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ var selected_indices: Array = [] # komplette Auswahl (Karte t_f6f29d86 — Mehrf
|
||||||
var hover_index := -1 # Hover-Objekt (schwächste Kontur; U6-Abstufung)
|
var hover_index := -1 # Hover-Objekt (schwächste Kontur; U6-Abstufung)
|
||||||
var object_markers: Array = [] # Node3D je Objekt (gleiche Reihenfolge wie doc.objects)
|
var object_markers: Array = [] # Node3D je Objekt (gleiche Reihenfolge wie doc.objects)
|
||||||
var fehlende_glbs: Array = [] # OIDs ohne echte GLB (Platzhalter-Fallback — Statuszeilen-Hinweis)
|
var fehlende_glbs: Array = [] # OIDs ohne echte GLB (Platzhalter-Fallback — Statuszeilen-Hinweis)
|
||||||
var checkpoint_root: Node3D = null # Checkpoint-Zonen (Spielmodus), Kinder je Zone
|
var zone_root: Node3D = null # Conquest-Zonen bzw. Hills (Spielmodus), Kinder je Zone
|
||||||
var spawn_markers: Array = [] # Node3D je Spawn-Point (Meta: spawn_team/spawn_id/spawn_radius)
|
var spawn_markers: Array = [] # Node3D je Spawn-Point (Meta: spawn_team/spawn_id/spawn_radius)
|
||||||
var terrain_view # TerrainMesh-Ansicht (script-erweitert, Variant)
|
var terrain_view # TerrainMesh-Ansicht (script-erweitert, Variant)
|
||||||
var grid_node: MeshInstance3D # Maßstabsgitter (Toggle „⊞ Gitter“, Karte t_7ac70014)
|
var grid_node: MeshInstance3D # Maßstabsgitter (Toggle „⊞ Gitter“, Karte t_7ac70014)
|
||||||
|
|
@ -116,7 +116,7 @@ func rebuild(data: Dictionary, manifest: Dictionary) -> void:
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
object_markers = []
|
object_markers = []
|
||||||
spawn_markers = []
|
spawn_markers = []
|
||||||
checkpoint_root = null
|
zone_root = null
|
||||||
grid_node = null
|
grid_node = null
|
||||||
_doc_ref = data
|
_doc_ref = data
|
||||||
_base_mat = StandardMaterial3D.new()
|
_base_mat = StandardMaterial3D.new()
|
||||||
|
|
@ -152,7 +152,7 @@ func rebuild(data: Dictionary, manifest: Dictionary) -> void:
|
||||||
grid_node.visible = terrain.is_empty()
|
grid_node.visible = terrain.is_empty()
|
||||||
_build_objects(data.get("objects", []), manifest)
|
_build_objects(data.get("objects", []), manifest)
|
||||||
_rebuild_spawn_markers(data)
|
_rebuild_spawn_markers(data)
|
||||||
rebuild_checkpoint_markers(data)
|
rebuild_zone_markers(data)
|
||||||
|
|
||||||
|
|
||||||
## Terrain als echtes Höhenfeld-Mesh statt flacher Ebene (Karte t_9aba04f1):
|
## Terrain als echtes Höhenfeld-Mesh statt flacher Ebene (Karte t_9aba04f1):
|
||||||
|
|
@ -493,13 +493,13 @@ func _rebuild_spawn_markers(data: Dictionary) -> void:
|
||||||
if marker != null and is_instance_valid(marker):
|
if marker != null and is_instance_valid(marker):
|
||||||
marker.queue_free()
|
marker.queue_free()
|
||||||
spawn_markers = []
|
spawn_markers = []
|
||||||
var zones = data.get("spawn_zones", {})
|
var zones = SpawnOps.zones_of(data) # bearbeiteter Spawn-Satz (Basis oder Modus)
|
||||||
if not (zones is Dictionary):
|
if not (zones is Dictionary):
|
||||||
return
|
return
|
||||||
for team_key in zones.keys():
|
for team_key in zones.keys():
|
||||||
if not (zones[team_key] is Dictionary):
|
if not (zones[team_key] is Dictionary):
|
||||||
continue
|
continue
|
||||||
var team := int(team_key)
|
var team := 0 if str(team_key) == "all" else int(team_key)
|
||||||
var zone: Dictionary = zones[team_key]
|
var zone: Dictionary = zones[team_key]
|
||||||
var points = zone.get("points", [])
|
var points = zone.get("points", [])
|
||||||
if not (points is Array):
|
if not (points is Array):
|
||||||
|
|
@ -516,19 +516,22 @@ func _rebuild_spawn_markers(data: Dictionary) -> void:
|
||||||
add_child(marker)
|
add_child(marker)
|
||||||
|
|
||||||
|
|
||||||
## Checkpoint-Zonen (Spielmodus): gelber Boden-Kreis + durchscheinender Zylinder
|
## Zonen des Spielmodus: Conquest = gelb, King of the Hill = orange (mit Nummer der
|
||||||
## + Name. Wird nur im Modus "checkpoints" gezeigt.
|
## Wanderreihenfolge). Boden-Kreis + durchscheinender Zylinder + Name.
|
||||||
func rebuild_checkpoint_markers(data: Dictionary) -> void:
|
func rebuild_zone_markers(data: Dictionary) -> void:
|
||||||
if checkpoint_root != null and is_instance_valid(checkpoint_root):
|
if zone_root != null and is_instance_valid(zone_root):
|
||||||
checkpoint_root.queue_free()
|
zone_root.queue_free()
|
||||||
checkpoint_root = null
|
zone_root = null
|
||||||
if ModusKatalog.mode_of(data) != ModusKatalog.MODE_CHECKPOINTS:
|
var mode := ModusKatalog.mode_of(data)
|
||||||
|
if not ModusKatalog.has_zones(mode):
|
||||||
return
|
return
|
||||||
checkpoint_root = Node3D.new()
|
zone_root = Node3D.new()
|
||||||
checkpoint_root.name = "Checkpoints"
|
zone_root.name = "Zonen"
|
||||||
add_child(checkpoint_root)
|
add_child(zone_root)
|
||||||
var col := Color(1.0, 0.8, 0.2)
|
var col := Color(1.0, 0.8, 0.2) if mode == ModusKatalog.MODE_CONQUEST else Color(1.0, 0.45, 0.15)
|
||||||
for cp in ModusKatalog.checkpoints(data):
|
var zlist := ModusKatalog.zones(data, mode)
|
||||||
|
for zi in zlist.size():
|
||||||
|
var cp = zlist[zi]
|
||||||
if not (cp is Dictionary):
|
if not (cp is Dictionary):
|
||||||
continue
|
continue
|
||||||
var pos: Array = cp.get("pos", [0, 0, 0])
|
var pos: Array = cp.get("pos", [0, 0, 0])
|
||||||
|
|
@ -566,7 +569,7 @@ func rebuild_checkpoint_markers(data: Dictionary) -> void:
|
||||||
ring.position.y = 0.15
|
ring.position.y = 0.15
|
||||||
node.add_child(ring)
|
node.add_child(ring)
|
||||||
var label := Label3D.new()
|
var label := Label3D.new()
|
||||||
label.text = "⚑ %s" % str(cp.get("label", cp.get("id", "")))
|
label.text = ("⚑ %s" if mode == ModusKatalog.MODE_CONQUEST else "♛ %d · %%s" % (zi + 1)) % str(cp.get("label", cp.get("id", "")))
|
||||||
label.position.y = 10.0
|
label.position.y = 10.0
|
||||||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
label.modulate = col
|
label.modulate = col
|
||||||
|
|
@ -574,7 +577,7 @@ func rebuild_checkpoint_markers(data: Dictionary) -> void:
|
||||||
label.font_size = 160
|
label.font_size = 160
|
||||||
label.pixel_size = 0.01
|
label.pixel_size = 0.01
|
||||||
node.add_child(label)
|
node.add_child(label)
|
||||||
checkpoint_root.add_child(node)
|
zone_root.add_child(node)
|
||||||
|
|
||||||
|
|
||||||
## Ein Spawn-Marker bauen (flacher Kreis + Label). radius = angezeigter Zonen-Radius.
|
## Ein Spawn-Marker bauen (flacher Kreis + Label). radius = angezeigter Zonen-Radius.
|
||||||
|
|
|
||||||
1
tests/probe_drag_echt.gd.uid
Normal file
1
tests/probe_drag_echt.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cnxoexcx8rjkc
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
extends SceneTree
|
extends SceneTree
|
||||||
## Editor-Test (headless): Spielmodus + Checkpoints (V21/V22) — Neu-Dialog,
|
## Editor-Test (headless): Spielmodus, Conquest-Zonen, Hills, Modus-Spawns (V21–V24) — Neu-Dialog,
|
||||||
## Katalog, Panel mit Undo/Redo, Speichern/Laden. Ausführen:
|
## Katalog, Panel mit Undo/Redo, Speichern/Laden. Ausführen:
|
||||||
## godot --headless --path . --script res://tests/test_modus.gd
|
## godot --headless --path . --script res://tests/test_modus.gd
|
||||||
|
|
||||||
|
|
@ -30,19 +30,36 @@ func _signed(d: Dictionary) -> Dictionary:
|
||||||
func _run_all() -> void:
|
func _run_all() -> void:
|
||||||
print("Testgruppe: Katalog + Neu-Map")
|
print("Testgruppe: Katalog + Neu-Map")
|
||||||
var tdm := SvmapDocument.create_new("T", 512.0, "mittag")
|
var tdm := SvmapDocument.create_new("T", 512.0, "mittag")
|
||||||
_check(not tdm.has("game_mode") and not tdm.has("checkpoints"), "TDM-Map ohne Zusatzfelder (altes Format)")
|
_check(not tdm.has("game_mode") and not tdm.has("conquest_zones") and not tdm.has("hills") and not tdm.has("mode_spawns"), "TDM-Map ohne Zusatzfelder (altes Format)")
|
||||||
_check(SvmapIO.validate_map(_signed(tdm)).is_empty(), "TDM-Map gültig")
|
_check(SvmapIO.validate_map(_signed(tdm)).is_empty(), "TDM-Map gültig")
|
||||||
var cp := SvmapDocument.create_new("C", 512.0, "mittag", "checkpoints")
|
var cp := SvmapDocument.create_new("C", 512.0, "mittag", "conquest")
|
||||||
_check(cp["game_mode"] == "checkpoints" and (cp["checkpoints"] as Array).size() == 3, "Checkpoint-Map mit 3 Start-Zonen")
|
_check(cp["game_mode"] == "conquest" and (cp["conquest_zones"] as Array).size() == 3, "Conquest-Map mit 3 Start-Zonen")
|
||||||
_check(SvmapIO.validate_map(_signed(cp)).is_empty(), "Checkpoint-Map gültig: " + str(SvmapIO.validate_map(_signed(cp))))
|
_check(SvmapIO.validate_map(_signed(cp)).is_empty(), "Conquest-Map gültig: " + str(SvmapIO.validate_map(_signed(cp))))
|
||||||
|
var kh := SvmapDocument.create_new("K", 512.0, "mittag", "koth")
|
||||||
|
_check(kh["game_mode"] == "koth" and (kh["hills"] as Array).size() == 3 and not kh.has("conquest_zones"), "KotH-Map mit 3 Hills")
|
||||||
|
_check(SvmapIO.validate_map(_signed(kh)).is_empty(), "KotH-Map gültig: " + str(SvmapIO.validate_map(_signed(kh))))
|
||||||
|
var dmd := SvmapDocument.create_new("D", 512.0, "mittag", "dm")
|
||||||
|
_check(dmd["game_mode"] == "dm" and SvmapIO.validate_map(_signed(dmd)).is_empty(), "Deathmatch-Map gültig")
|
||||||
|
# Alt-Map (vor dem Umbau): "checkpoints" lädt als Conquest, speichert neu
|
||||||
|
var alt := cp.duplicate(true)
|
||||||
|
alt["game_mode"] = "checkpoints"
|
||||||
|
alt["checkpoints"] = alt["conquest_zones"]
|
||||||
|
alt.erase("conquest_zones")
|
||||||
|
var alt_signed := _signed(alt)
|
||||||
|
_check(SvmapIO.validate_map(alt_signed).is_empty(), "Alt-Map (checkpoints) ist gültig")
|
||||||
|
var alt_res := SvmapIO.parse_and_validate(JSON.stringify(alt_signed))
|
||||||
|
_check(alt_res["ok"] and alt_res["data"]["game_mode"] == "conquest" and alt_res["data"].has("conquest_zones") and not alt_res["data"].has("checkpoints"), "Alt-Map wird beim Laden zu Conquest")
|
||||||
var bad := cp.duplicate(true)
|
var bad := cp.duplicate(true)
|
||||||
bad["checkpoints"] = []
|
bad["conquest_zones"] = []
|
||||||
_check(not ModusKatalog.validate(bad, bad["bounds"]).is_empty(), "Modus ohne Zonen ungültig (V21)")
|
_check(not ModusKatalog.validate(bad, bad["bounds"]).is_empty(), "Modus ohne Zonen ungültig (V21)")
|
||||||
|
bad = kh.duplicate(true)
|
||||||
|
bad["hills"] = []
|
||||||
|
_check(not ModusKatalog.validate(bad, bad["bounds"]).is_empty(), "KotH ohne Hill ungültig (V21)")
|
||||||
bad = cp.duplicate(true)
|
bad = cp.duplicate(true)
|
||||||
bad["checkpoints"][0]["pos"] = [9999.0, 0.0, 0.0]
|
bad["conquest_zones"][0]["pos"] = [9999.0, 0.0, 0.0]
|
||||||
_check(not ModusKatalog.validate(bad, bad["bounds"]).is_empty(), "Zone außerhalb ungültig (V22)")
|
_check(not ModusKatalog.validate(bad, bad["bounds"]).is_empty(), "Zone außerhalb ungültig (V22)")
|
||||||
bad = cp.duplicate(true)
|
bad = cp.duplicate(true)
|
||||||
bad["checkpoints"][1]["id"] = "C1"
|
bad["conquest_zones"][1]["id"] = "C1"
|
||||||
_check(not ModusKatalog.validate(bad, bad["bounds"]).is_empty(), "doppelte ID ungültig (V22)")
|
_check(not ModusKatalog.validate(bad, bad["bounds"]).is_empty(), "doppelte ID ungültig (V22)")
|
||||||
bad = cp.duplicate(true)
|
bad = cp.duplicate(true)
|
||||||
bad["game_mode"] = "ctf"
|
bad["game_mode"] = "ctf"
|
||||||
|
|
@ -53,7 +70,11 @@ func _run_all() -> void:
|
||||||
root.add_child(dlg)
|
root.add_child(dlg)
|
||||||
_check(dlg.get_game_mode() == "tdm", "Dialog-Default TDM")
|
_check(dlg.get_game_mode() == "tdm", "Dialog-Default TDM")
|
||||||
dlg.mode_option.select(1)
|
dlg.mode_option.select(1)
|
||||||
_check(dlg.get_game_mode() == "checkpoints", "Dialog liefert Checkpoints")
|
_check(dlg.get_game_mode() == "conquest", "Dialog liefert Conquest")
|
||||||
|
dlg.mode_option.select(2)
|
||||||
|
_check(dlg.get_game_mode() == "koth", "Dialog liefert King of the Hill")
|
||||||
|
dlg.mode_option.select(3)
|
||||||
|
_check(dlg.get_game_mode() == "dm", "Dialog liefert Deathmatch")
|
||||||
|
|
||||||
print("Testgruppe: Panel")
|
print("Testgruppe: Panel")
|
||||||
var scene: PackedScene = load("res://scenes/main.tscn")
|
var scene: PackedScene = load("res://scenes/main.tscn")
|
||||||
|
|
@ -69,45 +90,90 @@ func _run_all() -> void:
|
||||||
if panel == null:
|
if panel == null:
|
||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
_check(not panel.cp_box.visible, "TDM: Checkpoint-Bereich verborgen")
|
_check(not panel.cp_box.visible, "TDM: Zonen-Bereich verborgen")
|
||||||
panel.set_mode("checkpoints")
|
panel.set_mode("conquest")
|
||||||
_check(main.doc["game_mode"] == "checkpoints" and main.doc["checkpoints"].size() == 3, "Wechsel auf Checkpoints setzt 3 Zonen")
|
_check(main.doc["game_mode"] == "conquest" and main.doc["conquest_zones"].size() == 3, "Wechsel auf Conquest setzt 3 Zonen")
|
||||||
_check(panel.cp_box.visible and panel.rows_box.get_child_count() == 3, "3 Zeilen im Panel")
|
_check(panel.cp_box.visible and panel.rows_box.get_child_count() == 3, "3 Zeilen im Panel")
|
||||||
_check(main.view_builder.checkpoint_root != null and main.view_builder.checkpoint_root.get_child_count() == 3, "3 Zonen im 3D-Bild")
|
_check(main.view_builder.zone_root != null and main.view_builder.zone_root.get_child_count() == 3, "3 Zonen im 3D-Bild")
|
||||||
var id: String = panel.add_checkpoint(50.0, -30.0)
|
var id: String = panel.add_zone(50.0, -30.0)
|
||||||
_check(id == "C4" and main.doc["checkpoints"].size() == 4, "neue Zone C4")
|
_check(id == "C4" and main.doc["conquest_zones"].size() == 4, "neue Zone C4")
|
||||||
panel.edit_checkpoint("C4", {"radius_m": 25.0})
|
panel.edit_zone("C4", {"radius_m": 25.0})
|
||||||
panel.edit_checkpoint("C4", {"x": 80.0, "z": 10.0, "label": "Turm"})
|
panel.edit_zone("C4", {"x": 80.0, "z": 10.0, "label": "Turm"})
|
||||||
var c4: Dictionary = main.doc["checkpoints"][3]
|
var c4: Dictionary = main.doc["conquest_zones"][3]
|
||||||
_check(c4["radius_m"] == 25.0 and c4["pos"][0] == 80.0 and c4["pos"][2] == 10.0, "Radius + Position geändert")
|
_check(c4["radius_m"] == 25.0 and c4["pos"][0] == 80.0 and c4["pos"][2] == 10.0, "Radius + Position geändert")
|
||||||
panel.edit_checkpoint("C4", {"radius_m": 500.0})
|
panel.edit_zone("C4", {"radius_m": 500.0})
|
||||||
_check(main.doc["checkpoints"][3]["radius_m"] == ModusKatalog.MAX_RADIUS, "Radius wird begrenzt")
|
_check(main.doc["conquest_zones"][3]["radius_m"] == ModusKatalog.MAX_RADIUS, "Radius wird begrenzt")
|
||||||
_check(SvmapIO.validate_map(_signed(main.doc)).is_empty(), "Map gültig nach Änderungen")
|
_check(SvmapIO.validate_map(_signed(main.doc)).is_empty(), "Map gültig nach Änderungen")
|
||||||
panel.remove_checkpoint("C4")
|
panel.remove_zone("C4")
|
||||||
_check(main.doc["checkpoints"].size() == 3, "Zone gelöscht")
|
_check(main.doc["conquest_zones"].size() == 3, "Zone gelöscht")
|
||||||
|
|
||||||
main.history.undo()
|
main.history.undo()
|
||||||
main.call("_refresh_all")
|
main.call("_refresh_all")
|
||||||
_check(main.doc["checkpoints"].size() == 4, "Undo: Zone zurück")
|
_check(main.doc["conquest_zones"].size() == 4, "Undo: Zone zurück")
|
||||||
while main.history.can_undo():
|
while main.history.can_undo():
|
||||||
main.history.undo()
|
main.history.undo()
|
||||||
main.call("_refresh_all")
|
main.call("_refresh_all")
|
||||||
_check(not main.doc.has("game_mode") and not main.doc.has("checkpoints") and not panel.cp_box.visible, "Undo bis Start: Felder weg, UI nachgezogen")
|
_check(not main.doc.has("game_mode") and not main.doc.has("conquest_zones") and not panel.cp_box.visible, "Undo bis Start: Felder weg, UI nachgezogen")
|
||||||
main.history.redo()
|
main.history.redo()
|
||||||
main.call("_refresh_all")
|
main.call("_refresh_all")
|
||||||
_check(main.doc.get("game_mode", "") == "checkpoints", "Redo: Modus wieder da")
|
_check(main.doc.get("game_mode", "") == "conquest", "Redo: Modus wieder da")
|
||||||
|
|
||||||
panel.set_mode("tdm")
|
panel.set_mode("tdm")
|
||||||
_check(not main.doc.has("game_mode"), "Zurück auf TDM entfernt game_mode")
|
_check(not main.doc.has("game_mode"), "Zurück auf TDM entfernt game_mode")
|
||||||
_check(main.view_builder.checkpoint_root == null, "TDM: keine Zonen im Bild")
|
_check(main.view_builder.zone_root == null, "TDM: keine Zonen im Bild")
|
||||||
|
|
||||||
panel.set_mode("checkpoints")
|
print("Testgruppe: King of the Hill")
|
||||||
|
panel.set_mode("koth")
|
||||||
|
_check(main.doc["game_mode"] == "koth" and main.doc["hills"].size() == 3, "KotH setzt 3 Hills")
|
||||||
|
_check(panel.rows_box.get_child_count() == 3 and main.view_builder.zone_root.get_child_count() == 3, "3 Hill-Zeilen und 3 Marker")
|
||||||
|
var first_id: String = main.doc["hills"][0]["id"]
|
||||||
|
panel.move_zone(first_id, 1)
|
||||||
|
_check(main.doc["hills"][1]["id"] == first_id, "Hill nach hinten geschoben (Reihenfolge)")
|
||||||
|
panel.move_zone(first_id, 5)
|
||||||
|
_check(main.doc["hills"][1]["id"] == first_id, "Verschieben über den Rand wirkt nicht")
|
||||||
|
var hid: String = panel.add_zone(10.0, 10.0)
|
||||||
|
_check(hid == "H4" and main.doc["hills"].size() == 4, "neuer Hill H4")
|
||||||
|
main.history.undo()
|
||||||
|
main.history.undo()
|
||||||
|
main.call("_refresh_all")
|
||||||
|
_check(main.doc["hills"][0]["id"] == first_id and main.doc["hills"].size() == 3, "Undo stellt Reihenfolge und Liste wieder her")
|
||||||
|
_check(SvmapIO.validate_map(_signed(main.doc)).is_empty(), "KotH-Map gültig")
|
||||||
|
|
||||||
|
print("Testgruppe: Modus-Spawns")
|
||||||
|
_check(not main.doc.has("mode_spawns"), "ohne Anlegen kein mode_spawns-Block")
|
||||||
|
panel.select_spawn_set("koth")
|
||||||
|
_check(SpawnOps.edit_set == "koth" and SpawnOps.count_points(main.doc, 0) == 0, "KotH-Satz leer, solange nichts angelegt")
|
||||||
|
panel.create_spawns("koth")
|
||||||
|
_check(main.doc.has("mode_spawns") and main.doc["mode_spawns"]["koth"]["0"]["points"].size() == 4, "Anlegen kopiert die 4 Basis-Spawns je Team")
|
||||||
|
_check(SpawnOps.count_points(main.doc, 0) == 4 and SpawnOps.count_points(main.doc, 1) == 4, "Spawn-Werkzeug sieht jetzt den KotH-Satz")
|
||||||
|
var kid := str(main.doc["mode_spawns"]["koth"]["0"]["points"][0]["id"])
|
||||||
|
_check(kid.begins_with("KA") and SpawnOps.next_id(main.doc, 0).begins_with("KA"), "IDs mit Modus-Präfix (%s)" % kid)
|
||||||
|
var added := SpawnOps.add_zone(main.doc, 0, 30.0, 30.0)
|
||||||
|
_check(main.doc["mode_spawns"]["koth"]["0"]["points"].size() == 5 and main.doc["spawn_zones"]["0"]["points"].size() == 4, "neuer Punkt landet nur im KotH-Satz")
|
||||||
|
_check(SvmapIO.validate_map(_signed(main.doc)).is_empty(), "Map mit eigenen Spawns gültig: " + str(SvmapIO.validate_map(_signed(main.doc))))
|
||||||
|
main.history.undo()
|
||||||
|
main.call("_refresh_all")
|
||||||
|
_check(not main.doc.has("mode_spawns"), "Undo entfernt den Spawn-Satz wieder")
|
||||||
|
panel.select_spawn_set("dm")
|
||||||
|
panel.create_spawns("dm")
|
||||||
|
_check(main.doc["mode_spawns"]["dm"].has("all") and main.doc["mode_spawns"]["dm"]["all"]["points"].size() == 8, "Deathmatch: beide Teams in einem Topf (8 Punkte)")
|
||||||
|
SpawnOps.add_zone(main.doc, 1, 40.0, 40.0)
|
||||||
|
_check(main.doc["mode_spawns"]["dm"]["all"]["points"].size() == 9 and not main.doc["mode_spawns"]["dm"].has("1"), "Deathmatch: Team-Werkzeug schreibt nach 'all'")
|
||||||
|
_check(SvmapIO.validate_map(_signed(main.doc)).is_empty(), "DM-Spawns gültig")
|
||||||
|
var bad_ms: Dictionary = main.doc.duplicate(true)
|
||||||
|
bad_ms["mode_spawns"]["dm"]["0"] = {"points": [{"id": "X", "pos": [0.0, 0.0, 0.0]}]}
|
||||||
|
_check(not ModusKatalog.validate(bad_ms, bad_ms["bounds"]).is_empty(), "V24: Team-Schlüssel im DM-Satz ungültig")
|
||||||
|
panel.remove_spawns("dm")
|
||||||
|
_check(not main.doc.has("mode_spawns") and SpawnOps.edit_set == "base", "Löschen entfernt den Satz und schaltet auf Basis zurück")
|
||||||
|
SpawnOps.edit_set = "base"
|
||||||
|
|
||||||
|
panel.set_mode("conquest")
|
||||||
var path := "user://modus_test.svmap"
|
var path := "user://modus_test.svmap"
|
||||||
var doc2 := _signed(main.doc)
|
var doc2 := _signed(main.doc)
|
||||||
var res := SvmapIO.save_to_file(path, doc2)
|
var res := SvmapIO.save_to_file(path, doc2)
|
||||||
_check(res["ok"], "Speichern ok: " + str(res["errors"]))
|
_check(res["ok"], "Speichern ok: " + str(res["errors"]))
|
||||||
var back := SvmapIO.load_from_file(path)
|
var back := SvmapIO.load_from_file(path)
|
||||||
_check(back["ok"] and back["data"]["game_mode"] == "checkpoints" and back["data"]["checkpoints"].size() >= 3, "Laden: Modus + Zonen erhalten")
|
_check(back["ok"] and back["data"]["game_mode"] == "conquest" and back["data"]["conquest_zones"].size() >= 3, "Laden: Modus + Zonen erhalten")
|
||||||
|
|
||||||
if failures == 0:
|
if failures == 0:
|
||||||
print("ALL OK: %d checks" % checks)
|
print("ALL OK: %d checks" % checks)
|
||||||
|
|
|
||||||
1
tests/test_modus.gd.uid
Normal file
1
tests/test_modus.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://55ke874wv33d
|
||||||
Loading…
Reference in a new issue