extends RefCounted class_name TestStart ## Test-Startpunkt (Karte t_96e3c4ee): der Punkt, an dem „▶ Karte testen“ (F5) ## den Spieler absetzt — KEIN Spiel-Spawnpunkt (taucht nicht in doc, ## Spawn-Checks/Analysen oder Validator auf, ändert keine Zonen). ## ## Bewusste Entscheidungen dieser Karte: ## - Der Punkt lebt NUR editor-seitig (Sidecar, test_start_store.gd) — das ## .svmap-Format (Validator V1–V16, Client/Server) bleibt UNBERÜHRT. ## - Gespeichert wird nur x/z. Die Höhe kommt IMMER automatisch aus dem ## Terrain an der Stelle (SvmapDocument.terrain_height_at) — Terrain ## formen lässt den Punkt nicht schweben und versinken. ## - Fallback-Reihenfolge F5 (je mit Klartext-Hinweis): gesetzter Test-Start ## → erster Team-Spawnpunkt (Team Blau, wie bisher) → Kartenmitte. ## - Trifft der Klick ein Objekt/Bauteil: landet der Punkt auf dem BODEN ## DARUNTER (nie im Objekt schwebend) — das Tool meldet es im Klartext. ## ## Alles pure Statics (kein UI, kein Node-Zugriff) → headless testbar in ## tests/test_playtest_spawn.gd. ## Kein Punkt gesetzt (Sentinel-Typ für Variant-Vergleiche in main.gd). ## Start-Punkt für den Testlauf auflösen (Fallback-Kette). test_point ist ## Vector3 (nur x/z zählen) oder null. Rückgabe: ## {pos: Vector3 (y = Terrainhöhe), quelle: "test"|"team"|"mitte", ## status: Klartext für die Statuszeile} static func resolve_start(doc: Dictionary, test_point: Variant) -> Dictionary: if test_point != null: var p: Vector3 = test_point var y := SvmapDocument.terrain_height_at(doc, p.x, p.z) return { "pos": Vector3(p.x, y, p.z), "quelle": "test", "status": "Start an deinem gesetzten Test-Startpunkt (%.0f, %.0f, Höhe automatisch)." % [p.x, p.z], } # Erster Team-Spawnpunkt (Team Blau = „0“) — exakt wie bisher (t_1cdc1a72). var zones: Dictionary = doc.get("spawn_zones", {}) var team0 = zones.get("0", {}) if team0 is Dictionary: var points: Array = (team0 as Dictionary).get("points", []) if not points.is_empty() and points[0] is Dictionary: var pos = (points[0] as Dictionary).get("pos", []) if pos is Array and (pos as Array).size() >= 3: return { "pos": Vector3(float(pos[0]), float(pos[1]), float(pos[2])), "quelle": "team", "status": "Kein Test-Start gesetzt — Start am ersten Team-Blau-Spawnpunkt.", } # Kartenmitte aus den Bounds. var bounds: Dictionary = doc.get("bounds", {}) var mn: Array = bounds.get("min", [-50.0, -10.0, -50.0]) var mx: Array = bounds.get("max", [50.0, 80.0, 50.0]) var cx := (float(mn[0]) + float(mx[0])) * 0.5 var cz := (float(mn[2]) + float(mx[2])) * 0.5 return { "pos": Vector3(cx, SvmapDocument.terrain_height_at(doc, cx, cz), cz), "quelle": "mitte", "status": "Weder Test-Start noch Team-Spawnpunkte — Start in der Kartenmitte.", } ## Strahl → erster Treffer auf einer AABB (Slab-Methode) oder -1.0. ## Für „Klick trifft Bauteil“ im Editor-Tool und „Fadenkreuz trifft Hindernis“ ## im Testlauf (kein Physik-Server im Editor/SubViewport). ## Rückgabe: Distanz t >= 0 (origin + dir * t) oder -1.0 = kein Treffer. static func ray_aabb(origin: Vector3, dir: Vector3, box: AABB) -> float: var d := dir if d.length_squared() < 0.000000001: return -1.0 d = d.normalized() var tmin := 0.0 var tmax := INF for axis in 3: var o := origin[axis] var dv := d[axis] var lo := box.position[axis] var hi := box.end[axis] if absf(dv) < 0.0000001: if o < lo or o > hi: return -1.0 continue var t1 := (lo - o) / dv var t2 := (hi - o) / dv if t1 > t2: var tmp := t1 t1 = t2 t2 = tmp tmin = maxf(tmin, t1) tmax = minf(tmax, t2) if tmin > tmax: return -1.0 return tmin