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

413 lines
15 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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