194 lines
9.6 KiB
GDScript
194 lines
9.6 KiB
GDScript
extends SceneTree
|
||
|
||
## Platzierungsfreiheit (Karte t_e166e656) — EDITOR-Seite, pure Funktionen.
|
||
## Run: godot4 --headless --path . -s res://tests/test_platzierung.gd
|
||
## Exit 0 = alle Tests grün.
|
||
##
|
||
## Abdeckung:
|
||
## - SvmapIO.objekt_rotation: Fallback rot_y_deg (Legacy), rot überschreibt
|
||
## - SvmapIO.rot_setzen: rot-Feld nur bei echtem Kippen (Legacy-sauber)
|
||
## - SvmapIO-Validierung: V17 (kaputtes rot abgelehnt), gute Map mit rot lädt
|
||
## - KolliderBau.hindernis_aabb: 8 gedrehte Ecken (45°-Handrechnung)
|
||
## - ObjectOps.lift_object: Versenken unter Terrainhöhe erlaubt (Stein-Testfall)
|
||
## - ObjectOps.mirror_object / SymmetryCore.mirror_rot_euler: reines Y bleibt
|
||
## exakt das alte Verhalten (330/210), Kippen wird mitgespiegelt
|
||
|
||
var _failures: Array[String] = []
|
||
var _checks: int = 0
|
||
|
||
|
||
func _initialize() -> void:
|
||
_run_all()
|
||
print("--------------------------------------------------")
|
||
print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()])
|
||
for f in _failures:
|
||
print("FAIL: " + f)
|
||
quit(0 if _failures.is_empty() else 1)
|
||
|
||
|
||
func _check(cond: bool, label: String) -> void:
|
||
_checks += 1
|
||
if cond:
|
||
print(" ok - " + label)
|
||
else:
|
||
_failures.append(label)
|
||
print(" FAIL - " + label)
|
||
|
||
|
||
func _map_mit(obj: Dictionary) -> Dictionary:
|
||
var res := 17
|
||
var samples := PackedByteArray()
|
||
samples.resize(res * res * 2)
|
||
for i in res * res:
|
||
samples.encode_u16(i * 2, 32768) # flaches Terrain auf halber Höhe (5.0 m)
|
||
return {
|
||
"format_version": 2,
|
||
"info": {"name": "Platzierung", "author": "t", "description": "",
|
||
"version": 1, "created_utc": "2026-10-20T12:00:00Z", "target_game_version": "0.1.0"},
|
||
"terrain": {"type": "heightmap", "size_x_m": 200.0, "size_z_m": 200.0,
|
||
"min_height_m": 0.0, "max_height_m": 10.0, "resolution": res,
|
||
"heightmap_r16_b64": Marshalls.raw_to_base64(samples)},
|
||
"objects": [obj],
|
||
"spawn_zones": {
|
||
"0": {"points": [{"id": "A1", "pos": [-80.0, 5.0, -80.0]},
|
||
{"id": "A2", "pos": [-84.0, 5.0, -80.0]}, {"id": "A3", "pos": [-80.0, 5.0, -84.0]},
|
||
{"id": "A4", "pos": [-84.0, 5.0, -84.0]}]},
|
||
"1": {"points": [{"id": "B1", "pos": [80.0, 5.0, 80.0]},
|
||
{"id": "B2", "pos": [84.0, 5.0, 80.0]}, {"id": "B3", "pos": [80.0, 5.0, 84.0]},
|
||
{"id": "B4", "pos": [84.0, 5.0, 84.0]}]},
|
||
},
|
||
"bounds": {"min": [-100.0, -20.0, -100.0], "max": [100.0, 60.0, 100.0]},
|
||
"weather": "mittag",
|
||
}
|
||
|
||
|
||
func _run_all() -> void:
|
||
print("== 1) SvmapIO.objekt_rotation (Fallback + Override) ==")
|
||
var legacy := {"id": "x", "pos": [0.0, 0.0, 0.0], "rot_y_deg": 25.0, "scale": 1.0}
|
||
var e := SvmapIO.objekt_rotation(legacy)
|
||
_check(absf(e.y - 25.0) < 0.0001 and absf(e.x) < 0.0001 and absf(e.z) < 0.0001,
|
||
"Fallback: rot_y_deg = 25 → Euler (0, 25, 0)")
|
||
var gekippt := {"id": "x", "pos": [0.0, 0.0, 0.0], "rot_y_deg": 25.0, "scale": 1.0,
|
||
"rot": [20.0, 90.0, 10.0]}
|
||
var e2 := SvmapIO.objekt_rotation(gekippt)
|
||
_check(absf(e2.x - 20.0) < 0.0001 and absf(e2.y - 90.0) < 0.0001 and absf(e2.z - 10.0) < 0.0001,
|
||
"rot present → Euler (20, 90, 10), rot_y_deg ignoriert")
|
||
|
||
print("== 2) SvmapIO.rot_setzen (Legacy-sauber) ==")
|
||
var obj := {"id": "x", "pos": [0.0, 0.0, 0.0], "rot_y_deg": 45.0, "scale": 1.0}
|
||
SvmapIO.rot_setzen(obj, Vector3(0.0, 30.0, 0.0))
|
||
_check(absf(float(obj["rot_y_deg"]) - 30.0) < 0.0001 and not obj.has("rot"),
|
||
"reines Y: rot_y_deg aktualisiert, KEIN rot-Feld")
|
||
SvmapIO.rot_setzen(obj, Vector3(20.0, 30.0, -5.0))
|
||
_check((obj["rot"] as Array) == [20.0, 30.0, -5.0]
|
||
and absf(float(obj["rot_y_deg"]) - 30.0) < 0.0001,
|
||
"Kippen: rot=[20,30,-5] geschrieben, rot_y_deg = Y-Komponente")
|
||
SvmapIO.rot_setzen(obj, Vector3(0.0, 10.0, 0.0))
|
||
_check(not obj.has("rot") and absf(float(obj["rot_y_deg"]) - 10.0) < 0.0001,
|
||
"zurück auf 0-Kippen: rot-Feld wieder ENTFERNT (legacy-sauber)")
|
||
|
||
print("== 3) Validierung V17 + Roundtrip ==")
|
||
var gut := _map_mit({"id": "crate_wood", "pos": [0.0, 5.0, 0.0], "rot_y_deg": 0.0,
|
||
"scale": 1.0, "rot": [20.0, 0.0, 10.0]})
|
||
SvmapIO.sign_map(gut)
|
||
_check(SvmapIO.validate_map(gut).is_empty(), "Map mit rot=[20,0,10] validiert")
|
||
for bad in [[0.0, 0.0], ["a", "b", "c"]]:
|
||
var schlecht := _map_mit({"id": "crate_wood", "pos": [0.0, 5.0, 0.0], "rot_y_deg": 0.0,
|
||
"scale": 1.0, "rot": bad})
|
||
var errs := SvmapIO.validate_map(schlecht)
|
||
var hat := false
|
||
for f in errs:
|
||
if str(f).contains("V17"):
|
||
hat = true
|
||
_check(hat, "V17: rot=%s abgelehnt" % str(bad))
|
||
var pfad := "user://test_platzierung_rot.svmap"
|
||
var save := SvmapIO.save_to_file(pfad, gut)
|
||
_check(bool(save.get("ok", false)), "save_to_file mit rot ok")
|
||
var load := SvmapIO.load_from_file(pfad)
|
||
_check(bool(load.get("ok", false)), "Map mit rot lädt zurück")
|
||
if bool(load.get("ok", false)):
|
||
var objs: Array = (load["data"] as Dictionary).get("objects", [])
|
||
_check((objs[0] as Dictionary).get("rot", []) == [20.0, 0.0, 10.0],
|
||
"rot überlebt Roundtrip unverändert")
|
||
# Alte Karte (mit scale ≠ 1) bleibt ladbar — Format-Kompatibilität.
|
||
var alt := _map_mit({"id": "crate_wood", "pos": [0.0, 5.0, 0.0], "rot_y_deg": 15.0, "scale": 2.5})
|
||
SvmapIO.sign_map(alt)
|
||
_check(SvmapIO.validate_map(alt).is_empty(),
|
||
"alte Karte mit scale=2.5 lädt unverändert (kein Datenverlust)")
|
||
|
||
print("== 4) KolliderBau.hindernis_aabb (8 gedrehte Ecken) ==")
|
||
var col := {"shape": "box", "size": [2.0, 2.0, 2.0]}
|
||
var obj45 := {"id": "x", "pos": [0.0, 5.0, 0.0], "rot": [0.0, 45.0, 0.0], "scale": 1.0}
|
||
var aabb45: AABB = KolliderBau.hindernis_aabb(obj45, col, 1.0)
|
||
var erwartet := 2.0 * sqrt(2.0)
|
||
_check(absf(aabb45.size.x - erwartet) < 0.01 and absf(aabb45.size.z - erwartet) < 0.01
|
||
and absf(aabb45.size.y - 2.0) < 0.01, "45°-Box: AABB 2·√2 × 2 × 2·√2")
|
||
_check(absf(aabb45.get_center().y - 6.0) < 0.01, "Fußpunkt-Konvention: Zentrum halbe Höhe über pos")
|
||
|
||
print("== 5) Versenken (Stein im Boden) ==")
|
||
var doc := _map_mit({"id": "rock_large", "pos": [10.0, 6.0, 10.0], "rot_y_deg": 0.0, "scale": 1.0})
|
||
var boden_hoehe: float = SvmapDocument.terrain_height_at(doc, 10.0, 10.0)
|
||
_check(absf(boden_hoehe - 5.0) < 0.01, "Terrain dekodiert (5.0 m am Probe-Punkt)")
|
||
var angewandt: float = ObjectOps.lift_object(doc, 0, -2.0)
|
||
var versunken: float = float(((doc["objects"] as Array)[0] as Dictionary)["pos"][1])
|
||
_check(absf(angewandt + 2.0) < 0.001, "Absenken wirkt ÜBER die Terrainhöhe HINAUS (Versenken)")
|
||
_check(versunken < boden_hoehe - 0.9, "Objekt steht 1 m unter der Terrainhöhe (versenkt)")
|
||
var offset: float = ObjectOps.lift_offset(doc, 0)
|
||
_check(offset < -0.9, "lift_offset meldet negative Tiefe (%.2f m versenkt)" % -offset)
|
||
# Untergrenze = bounds.min.y (−20): weiter absenken klemmt dort.
|
||
var grenze: float = ObjectOps.lift_object(doc, 0, -100.0)
|
||
var y_ende: float = float(((doc["objects"] as Array)[0] as Dictionary)["pos"][1])
|
||
_check(absf(y_ende - (-20.0)) < 0.001 and grenze < -0.1,
|
||
"Untergrenze = bounds.min.y (−20 m), nicht tiefer")
|
||
|
||
print("== 6) Spiegel: Legacy-Verhalten rein-Y + Kippen ==")
|
||
# Pure-Y-Objekte: exakt die alten Zahlenwerte (tests/test_object_mirror-Kontrakt).
|
||
var nur_y := {"id": "x", "pos": [0.0, 0.0, 0.0], "rot_y_deg": 30.0, "scale": 1.0}
|
||
SvmapIO.rot_setzen(nur_y, Vector3(0.0, 30.0, 0.0)) # ohne rot-Feld
|
||
ObjectOps.mirror_object(nur_y, "x")
|
||
_check(absf(float(nur_y["rot_y_deg"]) - 330.0) < 0.0001 and not nur_y.has("rot"),
|
||
"Spiegel X, pure-Y: 30 → 330 (legacy-identisch), kein rot-Feld")
|
||
ObjectOps.mirror_object(nur_y, "z")
|
||
_check(absf(float(nur_y["rot_y_deg"]) - 210.0) < 0.0001,
|
||
"Spiegel Z, pure-Y: 330 → 210 (180 − 330, legacy-identisch)")
|
||
# Kippen: X-Spiegel dreht Y- UND Z-Komponente, X-Kippen bleibt.
|
||
var gekippt2 := {"id": "x", "pos": [0.0, 0.0, 0.0], "scale": 1.0, "rot": [20.0, 30.0, 10.0]}
|
||
ObjectOps.mirror_object(gekippt2, "x")
|
||
var mx := SvmapIO.objekt_rotation(gekippt2)
|
||
_check(absf(mx.x - 20.0) < 0.0001 and absf(mx.y - 330.0) < 0.0001 and absf(mx.z - 350.0) < 0.0001,
|
||
"Spiegel X mit Kippen: (20, 30, 10) → (20, 330, 350) — X-Kippen bleibt, Y/Z drehen Vorzeichen")
|
||
# Symmetrie-Kern: pure-Y-Kompatibilität.
|
||
var e30 := SymmetryCore.mirror_rot_euler(SymmetryCore.Mode.AXIS_X, Vector3(0.0, 30.0, 0.0))
|
||
_check(absf(e30.y - 330.0) < 0.0001 and absf(e30.x) < 0.0001 and absf(e30.z) < 0.0001,
|
||
"mirror_rot_euler AXIS_X, pure-Y: 30 → 330 (identisch zu mirror_rot_y)")
|
||
var e30p := SymmetryCore.mirror_rot_euler(SymmetryCore.Mode.POINT, Vector3(0.0, 30.0, 0.0))
|
||
_check(absf(e30p.y - 210.0) < 0.0001, "mirror_rot_euler POINT, pure-Y: 30 → 210")
|
||
# Achsenspiegelung ist selbstinvers: zweimal spiegeln = Ausgangslage.
|
||
var doppelt := SymmetryCore.mirror_rot_euler(SymmetryCore.Mode.AXIS_X,
|
||
SymmetryCore.mirror_rot_euler(SymmetryCore.Mode.AXIS_X, Vector3(20.0, 30.0, 10.0)))
|
||
_check(_euler_gleich(doppelt, Vector3(20.0, 30.0, 10.0)),
|
||
"AXIS_X-Spiegel ist selbstinvers (zweimal = Ausgangslage)")
|
||
|
||
print("== 7) Zwilling erbt gespiegeltes Kippen ==")
|
||
var doc2 := _map_mit({"id": "rock_large", "pos": [10.0, 5.0, 10.0], "scale": 1.0,
|
||
"rot": [20.0, 30.0, 10.0]})
|
||
var res: Dictionary = ObjectOps.symmetry_place_twin(doc2, 0, SymmetryCore.Mode.AXIS_X)
|
||
var twin_i := int(res.get("twin", -1))
|
||
_check(twin_i >= 0, "Zwilling angelegt")
|
||
if twin_i >= 0:
|
||
var twin: Dictionary = (doc2["objects"] as Array)[twin_i]
|
||
var tpos: Array = twin.get("pos", [])
|
||
_check(absf(float(tpos[0]) + 10.0) < 0.01, "Zwilling-Position gespiegelt (x → −x)")
|
||
var te := SvmapIO.objekt_rotation(twin)
|
||
var erwartet2 := SymmetryCore.mirror_rot_euler(SymmetryCore.Mode.AXIS_X, Vector3(20.0, 30.0, 10.0))
|
||
_check(_euler_gleich(te, erwartet2), "Zwilling-Kippen = gespiegelte Euler-Orientierung")
|
||
|
||
|
||
func _euler_gleich(a: Vector3, b: Vector3, tol := 0.01) -> bool:
|
||
var d := a - b
|
||
# Komponenten modulo 360 vergleichen (kürzester Weg).
|
||
for k in 3:
|
||
var w := fposmod(d[k] + 180.0, 360.0) - 180.0
|
||
if absf(w) > tol:
|
||
return false
|
||
return true
|