From fabbbcce20c61db1ab5f70b04efb48219073b4cb Mon Sep 17 00:00:00 2001 From: "Hermes (scopedvr-dev)" Date: Tue, 20 Oct 2026 20:11:11 +0000 Subject: [PATCH] =?UTF-8?q?Playtest-Kollision:=20echter=20Registry-Fu?= =?UTF-8?q?=C3=9Fabdruck=20statt=20Pick-Radius-Kugel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sly-Befund 20.10.: Im Editor-Testmodus hing man an unsichtbaren Wänden weit VOR den Gebäuden. Ursache: _collect_obstacles nutzte die Viewport-Metas pick_radius (Kugel-Näherung = volle Raumdiagonale + Zuschlag) — bei der Kaserne (17×35 m) blockte das ~40×40 m. Die Trimesh-Fixes aus t_9558038d wirken nur im Spiel-Client, der Editor-Playtest baut seine Blockierung selbst (keine Physik-Welt, nur AABBs). Fix: Hindernis-AABB aus der Registry (collision.size × scale, um rot_y_deg konservativ gedreht, Y vom Fußpunkt bis Fußpunkt + Höhe — gleiche Konvention wie der Client-Kollider). Fallback: Kugel-Näherung bleibt für Objekte ohne Registry-Eintrag. Regression in test_playtest.gd: gedreht/ungedreht, Scale, Fallback (20 checks, alle grün). Suiten: playtest_comfort 51, playtest_spawn 56, resize_dialog 44, terrain 153 — ALL OK. --- src/editor/playtest/playtest_mode.gd | 59 +++++++++++++++++++-- tests/test_playtest.gd | 78 ++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/src/editor/playtest/playtest_mode.gd b/src/editor/playtest/playtest_mode.gd index 3279a4f..b89e93c 100644 --- a/src/editor/playtest/playtest_mode.gd +++ b/src/editor/playtest/playtest_mode.gd @@ -145,16 +145,25 @@ func _spawn_start(doc: Dictionary) -> Vector3: ## Hindernis-AABBs sammeln (nur LESEND, keine neue Physik-Welt): -## - Objekt-Marker: die VORHANDENEN Viewport-Metas — pick_radius (x/z- -## Halbausdehnung, Kugel-Approximation konservativ) + half_height -## (Center-Höhe über dem Fußpunkt → Objekt-Top = 2× half_height). +## - Objekt-Marker: der ECHTE Fußabdruck aus der Registry (collision.size × +## scale, um rot_y_deg konservativ gedreht — Sly-Befund 20.10.: die alte +## pick_radius-Kugel-Näherung blockte ~40×40 m um eine 17×35 m-Kaserne = +## „unsichtbare Wand“ weit vor dem Modell). ## - Bau-Teile: exakt über BuildOps.piece_aabb (Dokument-Teile → AABB). +## - Fallback: Objekte ohne Registry-Eintrag behalten die alte Kugel-Näherung. func _collect_obstacles(doc: Dictionary, view_builder: Node3D) -> Array: var out: Array = [] + var lib := _lade_objekt_registry() if view_builder != null: for child in view_builder.get_children(): if child is Node3D and (child as Node3D).has_meta("obj_index"): var m := child as Node3D + var idx := int(m.get_meta("obj_index", -1)) + var aabb_v: Variant = _objekt_aabb_aus_registry(doc, idx, lib, m) + if aabb_v != null: + out.append(aabb_v) + continue + # Fallback: alte Kugel-Näherung (Objekt fehlt in der Registry). var r := float(m.get_meta("pick_radius", 1.0)) var ch := float(m.get_meta("half_height", 0.5)) var p := m.position @@ -170,6 +179,50 @@ func _collect_obstacles(doc: Dictionary, view_builder: Node3D) -> Array: return out +## objects.json einmal je Sammel-Lauf laden (klein genug, Playtest startet +## selten) — Key „objects“ → Dictionary oid → Entry. +func _lade_objekt_registry() -> Dictionary: + var parsed: Variant = JSON.parse_string( + FileAccess.get_file_as_string(SvmapIO.MANIFEST_PATH)) + if parsed is Dictionary and (parsed as Dictionary).has("objects"): + return (parsed as Dictionary)["objects"] + return {} + + +## Rotierter Fußabdruck-AABB eines Objekts aus der Registry — null wenn keine +## belastbaren Daten da sind (Caller fällt auf die Kugel-Näherung zurück). +## Y läuft vom Fußpunkt (Marker-Position) bis Fußpunkt + size.y × scale — +## gleiche Konvention wie der Client-Kollider. +func _objekt_aabb_aus_registry(doc: Dictionary, idx: int, lib: Dictionary, + marker: Node3D) -> Variant: + var objects: Array = doc.get("objects", []) + if idx < 0 or idx >= objects.size() or not (objects[idx] is Dictionary): + return null + var obj: Dictionary = objects[idx] + var entry: Dictionary = lib.get(str(obj.get("id", "")), {}) + if entry.is_empty(): + return null + var col: Dictionary = entry.get("collision", {}) + var size_v: Variant = col.get("size", null) + if not (size_v is Array) or (size_v as Array).size() != 3: + return null + var skala := float(obj.get("scale", 1.0)) + var sy := float(size_v[1]) * skala + var hx := float(size_v[0]) * skala * 0.5 + var hz := float(size_v[2]) * skala * 0.5 + if hx <= 0.0 or hz <= 0.0: + return null + var p := marker.position + var rot := deg_to_rad(float(obj.get("rot_y_deg", 0.0))) + var c := cos(rot) + var s := sin(rot) + # 4 Grundflächen-Ecken rotieren → konservative xz-Ausdehnung (symmetrisch + # um die Position, da der Fußabdruck zentriert ist). + var ex := maxf(absf(hx * c + hz * s), absf(hx * c - hz * s)) + var ez := maxf(absf(hx * s + hz * c), absf(hx * s - hz * c)) + return AABB(Vector3(p.x - ex, p.y, p.z - ez), Vector3(ex * 2.0, sy, ez * 2.0)) + + # ------------------------------------------------------------------- HUD ---- func _build_hud() -> void: diff --git a/tests/test_playtest.gd b/tests/test_playtest.gd index e9f59eb..2fa8ffb 100644 --- a/tests/test_playtest.gd +++ b/tests/test_playtest.gd @@ -57,6 +57,7 @@ func _run_all() -> void: _test_terrain_height_matches_document() _test_obstacle_blocks_player() _test_run_and_jump() + _test_obstacles_use_registry_footprint() _test_exit_restores_editor() _finish() @@ -210,6 +211,83 @@ func _test_obstacle_blocks_player() -> void: body.get("obstacles").erase(wall) +## --- Test 3b: Hindernisse = echter Registry-Fußabdruck (Sly 20.10.) --------- +## Die alte pick_radius-Kugel-Näherung blockte ~40×40 m um eine 17×35 m-Kaserne +## (= „unsichtbare Wand“ weit vor dem Modell). Jetzt: collision.size × scale, +## um rot_y_deg gedreht; Fallback Kugel-Näherung nur ohne Registry-Eintrag. +func _test_obstacles_use_registry_footprint() -> void: + print("Testgruppe: Hindernis-AABBs aus Registry-Fußabdruck") + var pt := _pt() + if pt == null: + _check(false, "PlaytestMode noch aktiv (Vorbedingung)") + return + + # Faken Viewport-Baukasten: 1 Marker mit obj_index + absurder Pick-Kugel. + var builder := Node3D.new() + var marker := Node3D.new() + marker.set_meta("obj_index", 0) + marker.set_meta("pick_radius", 20.3) + marker.set_meta("half_height", 3.1) + marker.position = Vector3(0.0, 0.0, -5.0) + builder.add_child(marker) + root.add_child(builder) + + # Kaserne um 90° gedreht: lange Achse (35.5 m) liegt dann auf X. + var doc: Dictionary = {"objects": [{ + "id": "barracks_001", "scale": 1.0, "rot_y_deg": 90.0, "pos": [0.0, 0.0, -5.0], + }]} + var obs: Array = pt.call("_collect_obstacles", doc, builder) + _check(obs.size() == 1, "Genau 1 Hindernis gesammelt (ist %d)" % obs.size()) + if obs.size() == 1: + var ab: AABB = obs[0] + _check(ab.size.x < 38.0, + "X-Ausdehnung unter Pick-Kugel-Blob (ist %.1f < 38)" % ab.size.x) + _check(absf(ab.size.x - 35.456) < 1.2, + "Gedrehte Länge auf X ≈ 35.5 m (ist %.2f)" % ab.size.x) + _check(absf(ab.size.z - 16.99) < 1.2, + "Gedrehte Breite auf Z ≈ 17.0 m (ist %.2f)" % ab.size.z) + _check(absf(ab.size.y - 6.247) < 0.5, + "Höhe ≈ 6.2 m (ist %.2f)" % ab.size.y) + _check(absf(ab.position.y - 0.0) < 0.01, "Fußpunkt an Marker-Y") + + # Rotation 0: Länge wieder auf Z. + var doc0: Dictionary = {"objects": [{ + "id": "barracks_001", "scale": 1.0, "rot_y_deg": 0.0, "pos": [0.0, 0.0, -5.0], + }]} + var obs0: Array = pt.call("_collect_obstacles", doc0, builder) + if obs0.size() == 1: + var ab0: AABB = obs0[0] + _check(absf(ab0.size.z - 35.456) < 1.2, + "Rotation 0: Länge auf Z ≈ 35.5 m (ist %.2f)" % ab0.size.z) + else: + _check(false, "Rotation-0-Fall liefert 1 Hindernis") + + # Scale wird mitgenommen (2× → doppelte Grundfläche). + var doc2: Dictionary = {"objects": [{ + "id": "barracks_001", "scale": 2.0, "rot_y_deg": 0.0, "pos": [0.0, 0.0, -5.0], + }]} + var obs2: Array = pt.call("_collect_obstacles", doc2, builder) + if obs2.size() == 1: + _check(absf((obs2[0] as AABB).size.z - 70.9) < 2.4, + "Scale 2× verdoppelt die Länge (ist %.1f)" % (obs2[0] as AABB).size.z) + else: + _check(false, "Scale-Fall liefert 1 Hindernis") + + # Fallback: unbekannte OID → alte Kugel-Näherung bleibt. + var docx: Dictionary = {"objects": [{ + "id": "gibts_nicht_999", "scale": 1.0, "rot_y_deg": 0.0, "pos": [0.0, 0.0, -5.0], + }]} + var obsx: Array = pt.call("_collect_obstacles", docx, builder) + if obsx.size() == 1: + var abx: AABB = obsx[0] + _check(absf(abx.size.x - 40.6) < 0.2, + "Fallback: Kugel-Näherung 2×pick_radius (ist %.1f)" % abx.size.x) + else: + _check(false, "Fallback-Fall liefert 1 Hindernis") + + builder.queue_free() + + ## --- Test 4: Rennen/Springen ------------------------------------------------ func _test_run_and_jump() -> void: print("Testgruppe: Rennen/Springen wirkt korrekt")