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