413 lines
15 KiB
GDScript
413 lines
15 KiB
GDScript
extends "res://core/ui/panels/editor_panel.gd"
|
||
## Spielmodus-Bereich im Map-Tab (28.10.): Modus der Map wählen (Team-Deathmatch /
|
||
## Conquest / King of the Hill / Deathmatch), Zonen bzw. Hills setzen und eigene
|
||
## Spawns je Modus anlegen. Zonen erscheinen als farbige Kreise im 3D-Bild.
|
||
## Gespeichert als optionale Felder doc["game_mode"], doc["conquest_zones"],
|
||
## doc["hills"], doc["mode_spawns"] (Regeln: core/svmap/modus_katalog.gd).
|
||
##
|
||
## Jede Änderung ist ein Undo-Schritt (History-Blöcke; fehlt ein Block, gilt der
|
||
## Standard: TDM, keine Zonen, keine eigenen Spawns).
|
||
|
||
var mode_option: OptionButton
|
||
var cp_box: VBoxContainer # Zonen-/Hill-Bereich (nur Conquest und KotH)
|
||
var zones_title: Label
|
||
var rows_box: VBoxContainer
|
||
var add_button: Button
|
||
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
|
||
|
||
|
||
static func spec() -> Dictionary:
|
||
return {"titel": "Spielmodus", "gruppe": "map", "order": 22}
|
||
|
||
|
||
func panel_title() -> String:
|
||
return "Spielmodus"
|
||
|
||
|
||
func build() -> void:
|
||
add_child(_header_label("Spielmodus"))
|
||
add_child(_muted_label("Legt fest, wie auf dieser Karte gewonnen wird."))
|
||
|
||
mode_option = OptionButton.new()
|
||
mode_option.name = "ModusAuswahl"
|
||
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:
|
||
mode_option.add_item(ModusKatalog.MODE_LABELS[m])
|
||
add_child(mode_option)
|
||
|
||
cp_box = VBoxContainer.new()
|
||
cp_box.add_theme_constant_override("separation", UIScale.px(8))
|
||
add_child(cp_box)
|
||
|
||
zones_title = _field_label("Zonen")
|
||
cp_box.add_child(zones_title)
|
||
rows_box = VBoxContainer.new()
|
||
rows_box.add_theme_constant_override("separation", UIScale.px(6))
|
||
cp_box.add_child(rows_box)
|
||
|
||
add_button = Button.new()
|
||
add_button.name = "ZoneNeu"
|
||
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."
|
||
cp_box.add_child(add_button)
|
||
|
||
hint_label = _muted_label("Mindestens eine Zone nötig.")
|
||
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:
|
||
if not _loading:
|
||
set_mode(ModusKatalog.MODES[i]))
|
||
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()
|
||
|
||
|
||
# ------------------------------------------------------------- Ändern ------
|
||
|
||
func set_mode(mode: String) -> void:
|
||
if main.doc.is_empty() or not ModusKatalog.MODES.has(mode) or ModusKatalog.mode_of(main.doc) == mode:
|
||
return
|
||
var before := _snap()
|
||
if mode == ModusKatalog.MODE_TDM:
|
||
main.doc.erase("game_mode")
|
||
else:
|
||
main.doc["game_mode"] = mode
|
||
if ModusKatalog.has_zones(mode) and ModusKatalog.zones(main.doc, mode).is_empty():
|
||
main.doc[ModusKatalog.ZONE_KEYS[mode]] = ModusKatalog.default_zones(main.doc, mode)
|
||
_commit(before, "Spielmodus: %s" % ModusKatalog.MODE_SHORT[mode])
|
||
|
||
|
||
func _zone_key() -> String:
|
||
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
|
||
var at := Vector3.ZERO
|
||
if main.camera != null and "target" in main.camera:
|
||
at = main.camera.target
|
||
var half := _half()
|
||
add_zone(clampf(at.x, -half.x, half.x), clampf(at.z, -half.y, half.y))
|
||
|
||
|
||
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 zone := ModusKatalog.make_zone(main.doc, mode, x, z)
|
||
var list: Array = ModusKatalog.zones(main.doc, mode).duplicate(true)
|
||
list.append(zone)
|
||
main.doc[key] = list
|
||
_commit(before, "%s %s gesetzt" % [ModusKatalog.ZONE_NAMES[mode], zone["id"]])
|
||
return str(zone["id"])
|
||
|
||
|
||
func remove_zone(id: String) -> void:
|
||
var mode := ModusKatalog.mode_of(main.doc)
|
||
var key := _zone_key()
|
||
if key == "":
|
||
return
|
||
var before := _snap()
|
||
var list: Array = []
|
||
for c in ModusKatalog.zones(main.doc, mode):
|
||
if str(c.get("id", "")) != id:
|
||
list.append((c as Dictionary).duplicate(true))
|
||
if list.size() == ModusKatalog.zones(main.doc, mode).size():
|
||
return
|
||
if list.is_empty():
|
||
main.doc.erase(key)
|
||
else:
|
||
main.doc[key] = list
|
||
_commit(before, "%s %s gelöscht" % [ModusKatalog.ZONE_NAMES[mode], id])
|
||
|
||
|
||
## King of the Hill: Reihenfolge der Hills ändern (dir = -1 hoch, +1 runter).
|
||
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 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:
|
||
if str(c.get("id", "")) != id:
|
||
continue
|
||
if fields.has("label"):
|
||
c["label"] = str(fields["label"]).strip_edges().substr(0, 32)
|
||
if fields.has("radius_m"):
|
||
c["radius_m"] = clampf(float(fields["radius_m"]), ModusKatalog.MIN_RADIUS, ModusKatalog.MAX_RADIUS)
|
||
if fields.has("x") or fields.has("z"):
|
||
var half := _half()
|
||
var p: Array = c["pos"]
|
||
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)
|
||
c["pos"] = [nx, SvmapDocument.terrain_height_at(main.doc, nx, nz), nz]
|
||
if fields.has(SpawnOps.YAW_KEY):
|
||
SpawnOps.write_yaw(c, fields[SpawnOps.YAW_KEY])
|
||
main.doc[key] = list
|
||
_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") or fields.has(SpawnOps.YAW_KEY) 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:
|
||
var t: Dictionary = main.doc.get("terrain", {})
|
||
return Vector2(float(t.get("size_x_m", 1024.0)) * 0.5, float(t.get("size_z_m", 1024.0)) * 0.5)
|
||
|
||
|
||
func _snap() -> Dictionary:
|
||
var out := {}
|
||
for key in ["game_mode", "conquest_zones", "hills", "mode_spawns"]:
|
||
var v: Variant = main.doc.get(key, null)
|
||
out[key] = v.duplicate(true) if v is Array or v is Dictionary else v
|
||
return out
|
||
|
||
|
||
func _commit(before: Dictionary, text: String, merge_tag := "") -> void:
|
||
var after := _snap()
|
||
if before == after:
|
||
return
|
||
main.history.commit(text, before, after, merge_tag)
|
||
main.dirty = true
|
||
main.call("_refresh_file_label")
|
||
main.call("_refresh_history_buttons")
|
||
if main.view_builder != null:
|
||
main.view_builder.call("rebuild_zone_markers", main.doc)
|
||
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 or main.doc.is_empty():
|
||
return
|
||
_loading = true
|
||
var mode := ModusKatalog.mode_of(main.doc)
|
||
mode_option.select(ModusKatalog.MODES.find(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():
|
||
rows_box.remove_child(c)
|
||
c.queue_free()
|
||
var zl := ModusKatalog.zones(main.doc, mode)
|
||
for i in zl.size():
|
||
if zl[i] is Dictionary:
|
||
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
|
||
|
||
|
||
func _row(cp: Dictionary, index := 0, count := 1, mode := "conquest") -> Control:
|
||
var id := str(cp.get("id", ""))
|
||
var pos: Array = cp.get("pos", [0, 0, 0])
|
||
var half := _half()
|
||
var box := VBoxContainer.new()
|
||
box.name = "Z_%s" % id
|
||
box.add_theme_constant_override("separation", UIScale.px(4))
|
||
|
||
var top := HBoxContainer.new()
|
||
var idl := _field_label("%d·%s" % [index + 1, id] if mode == ModusKatalog.MODE_KOTH else id)
|
||
idl.custom_minimum_size = UIScale.v2(32, 0)
|
||
top.add_child(idl)
|
||
var name_edit := LineEdit.new()
|
||
name_edit.name = "Name"
|
||
name_edit.text = str(cp.get("label", id))
|
||
name_edit.max_length = 32
|
||
name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
name_edit.text_submitted.connect(func(t: String) -> void:
|
||
if not _loading:
|
||
edit_zone(id, {"label": t}))
|
||
name_edit.focus_exited.connect(func() -> void:
|
||
if not _loading and name_edit.text != str(cp.get("label", id)):
|
||
edit_zone(id, {"label": name_edit.text}))
|
||
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()
|
||
del.name = "Loeschen"
|
||
del.text = "✕"
|
||
del.tooltip_text = "Löschen"
|
||
del.pressed.connect(func() -> void: remove_zone(id))
|
||
top.add_child(del)
|
||
box.add_child(top)
|
||
|
||
var nums := HBoxContainer.new()
|
||
nums.add_theme_constant_override("separation", UIScale.px(4))
|
||
nums.add_child(_spin("X", "x", id, float(pos[0]), -half.x, half.x, 1.0))
|
||
nums.add_child(_spin("Z", "z", id, float(pos[2]), -half.y, half.y, 1.0))
|
||
nums.add_child(_spin("Radius", "radius_m", id, float(cp.get("radius_m", ModusKatalog.DEFAULT_RADIUS)),
|
||
ModusKatalog.MIN_RADIUS, ModusKatalog.MAX_RADIUS, 1.0))
|
||
box.add_child(nums)
|
||
if mode == ModusKatalog.MODE_CONQUEST:
|
||
var facing := HBoxContainer.new()
|
||
facing.add_theme_constant_override("separation", UIScale.px(4))
|
||
facing.add_child(_spin("Blickrichtung °", SpawnOps.YAW_KEY, id, SpawnOps.effective_yaw(main.doc, cp), 0.0, 359.0, 1.0))
|
||
var reset := Button.new()
|
||
reset.name = "BlickStandard"
|
||
reset.text = "Standard"
|
||
reset.tooltip_text = "Keine feste Richtung: Spieler schauen zur Kartenmitte, Bots zum Gegner. Im Spawn-Werkzeug geht's auch per Pfeilspitze."
|
||
reset.disabled = not SpawnOps.has_yaw(cp)
|
||
reset.pressed.connect(func() -> void: edit_zone(id, {SpawnOps.YAW_KEY: null}))
|
||
facing.add_child(reset)
|
||
box.add_child(facing)
|
||
return box
|
||
|
||
|
||
func _spin(label: String, field: String, id: String, value: float, mn: float, mx: float, step: float) -> Control:
|
||
var h := HBoxContainer.new()
|
||
h.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
h.add_child(_field_label(label))
|
||
var sb := SpinBox.new()
|
||
sb.name = field
|
||
sb.min_value = mn
|
||
sb.max_value = mx
|
||
sb.step = step
|
||
sb.set_value_no_signal(value)
|
||
sb.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
sb.value_changed.connect(func(v: float) -> void:
|
||
if not _loading:
|
||
edit_zone(id, {field: v}))
|
||
h.add_child(sb)
|
||
return h
|