- BauKatalog: 12 Teile (Fundament, Boden, Wände mit Tür/Fenster/Scharte, Brüstung, Pfeiler, Giebel, Dächer, Treppe) auf Basis der CC0-Geometrie des Modular Builder 0.2.0, Wandhöhe 3 m - BuildSnap: Wände auf Zellkanten, Böden in Zellmitte, Höhe aus Ebenen statt Klickhöhe, Ersetzen (Tür in Wand) - Gebäude kopieren/auswählen/löschen im Bauen-Tab, Kopien bekommen eigene Gruppe - Kollision serverkonsistent über Trimesh-Dateien (Türen/Fenster echte Löcher), tools/gen_bauteile.gd erzeugt GLB, Kollision, Registry-Fragment - Alte gebaeude-Sektion wird beim Laden migriert, altes Bauen-Werkzeug entfernt, Resize behält Gruppennamen - Tests: test_bauen (258 Checks), Playtest läuft Treppe/Tür Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
219 lines
8.3 KiB
GDScript
219 lines
8.3 KiB
GDScript
extends RefCounted
|
||
class_name SymmetryCore
|
||
## Symmetrie-Modus im Map Builder (Karte t_f5935023, Spec
|
||
## /opt/data/scopedvr/KARTEN_MIRROR_vc27.md, Entscheidung Sly 20.10.):
|
||
## gespiegeltes Bauen — Änderungen an einer Kartenseite landen automatisch
|
||
## auf der anderen. DIESE Datei ist M1 (Fundament): reine Mathe + Format-
|
||
## Helfer, KEIN Spiegeln von Aktionen — das kommt mit M2 (Terrain),
|
||
## M3 (Objekte) und M4 (Bau-Teile + Spawns).
|
||
##
|
||
## Spiegel-Mathematik (Spec vc27):
|
||
## Zentrum = Kartenmitte. Editor-Maps liegen zentriert (bounds
|
||
## −size/2 … +size/2), die Mitte ist also in Weltkoordinaten (0, 0) —
|
||
## map_center() rechnet sie trotzdem generell aus den bounds aus
|
||
## ((min + max) / 2), damit die Helfer auch bei asymmetrischen bounds
|
||
## korrekt bleiben.
|
||
## | Modus | Position | rot_y |
|
||
## |-------------|-----------------------------|--------------|
|
||
## | Punkt (180°)| x' = 2cx − x, z' = 2cz − z | rot + 180° |
|
||
## | Achse X | x' = 2cx − x, z' = z | −rot |
|
||
## | Achse Z | x' = x, z' = 2cz − z | −rot |
|
||
## y bleibt in ALLEN Modi unverändert (Höhe ist ein Skalar).
|
||
## Standard = Punktspiegelung (nur sie macht beide Hälften wirklich
|
||
## deckungsgleich — Achsenspiegelung dreht den Zwilling um 180°, siehe Spec).
|
||
##
|
||
## Terrain-Zell-Abbildung (Zelle (i,j), i läuft entlang x, j entlang z —
|
||
## siehe SvmapDocument.terrain_height_at): Punkt (res−1−i, res−1−j),
|
||
## Achse X (res−1−i, j), Achse Z (i, res−1−j).
|
||
##
|
||
## Partner-Link: ein Paar trägt denselben `sym`-Schlüssel (optionales Feld
|
||
## an Objekten UND Bau-Teilen). Validator-Regel (svmap_io.gd, V16):
|
||
## String, nicht leer, ≤ 64 Zeichen, höchstens 2 Objekte pro Kennung.
|
||
## Alte Karten ohne `sym` laden unverändert (Rückwärtskompatibilität).
|
||
##
|
||
## Alles static + reine Funktionen, ohne Node-Abhängigkeit → headless testbar.
|
||
|
||
enum Mode {
|
||
OFF = 0, # Symmetrie aus
|
||
POINT = 1, # Punktspiegelung 180° um die Kartenmitte (Standard)
|
||
AXIS_X = 2, # Achsenspiegelung X (links ↔ rechts)
|
||
AXIS_Z = 3, # Achsenspiegelung Z (vorn ↔ hinten)
|
||
}
|
||
|
||
## Toolbar-/Statuszeilen-Texte, Index = Mode-Wert.
|
||
const MODE_LABELS: Array[String] = ["Aus", "Punkt", "Achse X", "Achse Z"]
|
||
|
||
## Toleranz in Metern für „liegt auf der Achse/im Zentrum“ — solche Elemente
|
||
## werden beim gespiegelten Bauen NICHT gedoppelt (Spec vc27).
|
||
const ON_AXIS_TOLERANCE_M := 0.05
|
||
|
||
|
||
# ------------------------------------------------------------ Modus/Helfer --
|
||
|
||
static func mode_label(mode: int) -> String:
|
||
return MODE_LABELS[clampi(mode, 0, MODE_LABELS.size() - 1)]
|
||
|
||
|
||
## Kartenmitte aus den bounds ((min + max) / 2 in X und Z). Für zentrierte
|
||
## Editor-Maps = (0, 0), unabhängig von der Kartengröße — die Größe steckt
|
||
## in den bounds. Fehlen die bounds (defektes doc) → (0, 0).
|
||
static func map_center(doc: Dictionary) -> Vector2:
|
||
var bounds: Dictionary = doc.get("bounds", {})
|
||
var mn: Array = bounds.get("min", [])
|
||
var mx: Array = bounds.get("max", [])
|
||
if (mn as Array).size() == 3 and (mx as Array).size() == 3:
|
||
return Vector2(
|
||
(float(mn[0]) + float(mx[0])) * 0.5,
|
||
(float(mn[2]) + float(mx[2])) * 0.5)
|
||
return Vector2.ZERO
|
||
|
||
|
||
# ----------------------------------------------------- Transform-Helfer -----
|
||
|
||
## Spiegelt die XZ-Position eines Objekts/Bau-Teils; Y (Höhe) bleibt gleich.
|
||
static func mirror_point(mode: int, p: Vector3, center: Vector2) -> Vector3:
|
||
match mode:
|
||
Mode.POINT:
|
||
return Vector3(2.0 * center.x - p.x, p.y, 2.0 * center.y - p.z)
|
||
Mode.AXIS_X:
|
||
return Vector3(2.0 * center.x - p.x, p.y, p.z)
|
||
Mode.AXIS_Z:
|
||
return Vector3(p.x, p.y, 2.0 * center.y - p.z)
|
||
_:
|
||
return p
|
||
|
||
|
||
## Spiegelt die Y-Drehung in Grad. Normalisiert auf [0, 360): zwei Anwendungen
|
||
## ergeben wieder die Ausgangsdrehung (modulo 360 — gleiche Orientierung).
|
||
## Punkt: +180°, Achsen: −180°-Spiegel = Vorzeichenwechsel.
|
||
static func mirror_rot_y(mode: int, rot_y_deg: float) -> float:
|
||
match mode:
|
||
Mode.POINT:
|
||
return fposmod(rot_y_deg + 180.0, 360.0)
|
||
Mode.AXIS_X, Mode.AXIS_Z:
|
||
return fposmod(-rot_y_deg, 360.0)
|
||
_:
|
||
return rot_y_deg
|
||
|
||
|
||
## true, wenn der Punkt unter diesem Modus auf sich selbst abgebildet wird
|
||
## (Zentrum bei Punktspiegelung, Spiegelachse bei Achsenspiegelung, ± Toleranz).
|
||
## Solche Elemente werden beim gespiegelten Bauen NICHT gedoppelt.
|
||
static func maps_to_self(mode: int, p: Vector3, center: Vector2,
|
||
threshold_m: float = ON_AXIS_TOLERANCE_M) -> bool:
|
||
match mode:
|
||
Mode.POINT:
|
||
return Vector2(p.x - center.x, p.z - center.y).length() < threshold_m
|
||
Mode.AXIS_X:
|
||
return absf(p.x - center.x) < threshold_m
|
||
Mode.AXIS_Z:
|
||
return absf(p.z - center.y) < threshold_m
|
||
_:
|
||
return true
|
||
|
||
|
||
# ------------------------------------------------ Zell-Abbildung (Terrain) --
|
||
|
||
## Rasterzelle (i, j) → gespiegelte Zelle. res = terrain.resolution.
|
||
## i läuft entlang x, j entlang z (wie terrain_height_at). OFF → unverändert.
|
||
static func mirror_cell(mode: int, i: int, j: int, res: int) -> Vector2i:
|
||
if res < 2:
|
||
return Vector2i(i, j)
|
||
var last := res - 1
|
||
match mode:
|
||
Mode.POINT:
|
||
return Vector2i(last - i, last - j)
|
||
Mode.AXIS_X:
|
||
return Vector2i(last - i, j)
|
||
Mode.AXIS_Z:
|
||
return Vector2i(i, last - j)
|
||
_:
|
||
return Vector2i(i, j)
|
||
|
||
|
||
# ------------------------------------------------------------ Partner-Link --
|
||
|
||
## sym-Kennung eines Elements lesen (leer = kein Partner). Typsicher: nur
|
||
## nicht-leere Strings gelten; defekte Felder werden still als „kein Partner“
|
||
## behandelt (Rückwärtskompatibilität mit alten Karten).
|
||
static func sym_of(obj: Dictionary) -> String:
|
||
var s = obj.get("sym", "")
|
||
return s if s is String else ""
|
||
|
||
|
||
## sym-Kennung setzen (mutiert das Element und gibt es zurück zum Verketten).
|
||
static func set_sym(obj: Dictionary, key: String) -> Dictionary:
|
||
obj["sym"] = key
|
||
return obj
|
||
|
||
|
||
## sym-Kennung entfernen (Feld komplett löschen — fehlt es, wie in alten Karten).
|
||
static func clear_sym(obj: Dictionary) -> Dictionary:
|
||
obj.erase("sym")
|
||
return obj
|
||
|
||
|
||
## Index des Partner-Elements in einer Liste (objects ODER gebaeude):
|
||
## das Element mit derselben sym-Kennung (index selbst ausgeschlossen).
|
||
## −1 = kein Partner gefunden.
|
||
static func find_partner_index(list: Array, key: String, exclude_index: int = -1) -> int:
|
||
if key.is_empty():
|
||
return -1
|
||
for i in list.size():
|
||
if i == exclude_index or not (list[i] is Dictionary):
|
||
continue
|
||
if sym_of(list[i] as Dictionary) == key:
|
||
return i
|
||
return -1
|
||
|
||
|
||
## Nächste freie sym-Kennung im Dokument ("sym_1", "sym_2", …): scannt
|
||
## Objekte (inkl. Bauteile), deterministic (kein Zufall, keine Uhr).
|
||
static func next_sym_key(doc: Dictionary) -> String:
|
||
var used: Dictionary = {}
|
||
var block: Array = doc.get("objects", [])
|
||
for item in block:
|
||
if not (item is Dictionary):
|
||
continue
|
||
var s := sym_of(item as Dictionary)
|
||
if s.begins_with("sym_"):
|
||
var tail := s.substr(4)
|
||
if tail.is_valid_int():
|
||
used[int(tail)] = true
|
||
var n := 1
|
||
while used.has(n):
|
||
n += 1
|
||
return "sym_%d" % n
|
||
|
||
|
||
# ---------------------------------- Spiegel-Mathe 3 Achsen (t_e166e656) ------
|
||
|
||
## Spiegelt eine VOLLE Euler-Orientierung (Karte t_e166e656, Platzierungs-
|
||
## freiheit). Definition (dokumentiert in docs/map_format.md §objects.rot):
|
||
## - Achsenspiegelung (AXIS_X / AXIS_Z) = Konjugation der Basis mit der
|
||
## Spiegelmatrix: B' = M·B·M — für reines Y-Drehen exakt die bisherige
|
||
## Regel mirror_rot_y (−rot).
|
||
## - Punktspiegelung (POINT) = 180°-Drehung um Y: B' = Ry(180°)·B — für
|
||
## reines Y-Drehen exakt mirror_rot_y (+180°).
|
||
## Godot-Euler-Reihenfolge YXZ (Node3D-Rotation), Rückgabe in Grad.
|
||
static func mirror_rot_euler(mode: int, rot_deg: Vector3) -> Vector3:
|
||
var b := Basis.from_euler(Vector3(
|
||
deg_to_rad(rot_deg.x), deg_to_rad(rot_deg.y), deg_to_rad(rot_deg.z)))
|
||
var gespiegelt: Basis
|
||
match mode:
|
||
Mode.POINT:
|
||
gespiegelt = Basis.from_euler(Vector3(0.0, deg_to_rad(180.0), 0.0)) * b
|
||
Mode.AXIS_X:
|
||
var m := Basis.from_scale(Vector3(-1.0, 1.0, 1.0))
|
||
gespiegelt = m * b * m
|
||
Mode.AXIS_Z:
|
||
var m := Basis.from_scale(Vector3(1.0, 1.0, -1.0))
|
||
gespiegelt = m * b * m
|
||
_:
|
||
return rot_deg
|
||
# Normalisierung [0, 360) je Achse — gleiche Darstellung wie rot_y_deg
|
||
# bisher (tests/test_object_mirror.gd erwartet z. B. 330 statt −30).
|
||
return Vector3(
|
||
fposmod(gespiegelt.get_euler().x * (180.0 / PI), 360.0),
|
||
fposmod(gespiegelt.get_euler().y * (180.0 / PI), 360.0),
|
||
fposmod(gespiegelt.get_euler().z * (180.0 / PI), 360.0))
|