Editor: Blickrichtung für Spawn-Punkte und Conquest-Zonen (yaw_deg, optional): Pfeil im Bild, Spitze ziehen oder Zahl im Panel, Standard-Knopf, Undo, Symmetrie spiegelt mit, Playtest nutzt sie; Prüfung; Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff1c0e345e
commit
e8ab954876
10 changed files with 368 additions and 3 deletions
|
|
@ -87,6 +87,10 @@ func enter(doc: Dictionary, view_builder: Node3D, start_override: Variant = null
|
||||||
body.position = Vector3(start.x,
|
body.position = Vector3(start.x,
|
||||||
body.terrain_height(start.x, start.z) + PlaytestBody.HALF_H + 0.05,
|
body.terrain_height(start.x, start.z) + PlaytestBody.HALF_H + 0.05,
|
||||||
start.z)
|
start.z)
|
||||||
|
if start_override == null:
|
||||||
|
var first := _first_spawn(doc)
|
||||||
|
if first.has(SpawnOps.YAW_KEY):
|
||||||
|
body.yaw_deg = float(first[SpawnOps.YAW_KEY])
|
||||||
# Kollider 1:1 vom Quest-Client (Karte t_1da96275): Terrain-Trimesh +
|
# Kollider 1:1 vom Quest-Client (Karte t_1da96275): Terrain-Trimesh +
|
||||||
# Objekt-Kollider aus der Registry + Bauteil-Trimeshe. Alles als Kind
|
# Objekt-Kollider aus der Registry + Bauteil-Trimeshe. Alles als Kind
|
||||||
# dieses Modus — exit() räumt es mit ab.
|
# dieses Modus — exit() räumt es mit ab.
|
||||||
|
|
@ -165,6 +169,15 @@ func _set_ui_visible(vis: bool) -> void:
|
||||||
## Start-Punkt: erster Spawn-Punkt aus dem Dokument (spawn_zones-Team „0",
|
## Start-Punkt: erster Spawn-Punkt aus dem Dokument (spawn_zones-Team „0",
|
||||||
## erster Eintrag der points-Liste); sonst Kartenmitte aus den Bounds.
|
## erster Eintrag der points-Liste); sonst Kartenmitte aus den Bounds.
|
||||||
## doc-Format (svmap): {"spawn_zones": {"0": {"points": [{"id", "pos": [x,y,z]}]}, ...}}
|
## doc-Format (svmap): {"spawn_zones": {"0": {"points": [{"id", "pos": [x,y,z]}]}, ...}}
|
||||||
|
func _first_spawn(doc: Dictionary) -> Dictionary:
|
||||||
|
var team0 = (doc.get("spawn_zones", {}) as Dictionary).get("0", {})
|
||||||
|
if team0 is Dictionary:
|
||||||
|
var points: Array = (team0 as Dictionary).get("points", [])
|
||||||
|
if not points.is_empty() and points[0] is Dictionary:
|
||||||
|
return points[0]
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
func _spawn_start(doc: Dictionary) -> Vector3:
|
func _spawn_start(doc: Dictionary) -> Vector3:
|
||||||
var zones: Dictionary = doc.get("spawn_zones", {})
|
var zones: Dictionary = doc.get("spawn_zones", {})
|
||||||
var team0 = zones.get("0", {})
|
var team0 = zones.get("0", {})
|
||||||
|
|
@ -393,7 +406,8 @@ func _spawn_point_for(index: int) -> Dictionary:
|
||||||
if pos.size() < 3:
|
if pos.size() < 3:
|
||||||
return {}
|
return {}
|
||||||
return {"pos": Vector3(float(pos[0]), float(pos[1]), float(pos[2])),
|
return {"pos": Vector3(float(pos[0]), float(pos[1]), float(pos[2])),
|
||||||
"label": str((zone as Dictionary).get("label", "Zone %d" % index))}
|
"label": str((zone as Dictionary).get("label", "Zone %d" % index)),
|
||||||
|
"yaw": p.get(SpawnOps.YAW_KEY, null)}
|
||||||
|
|
||||||
|
|
||||||
## Sprung zu einem Spawnpunkt (Tasten 1–4): exakt auf den Punkt gesetzt
|
## Sprung zu einem Spawnpunkt (Tasten 1–4): exakt auf den Punkt gesetzt
|
||||||
|
|
@ -411,6 +425,8 @@ func jump_to_spawn(index: int) -> bool:
|
||||||
body.set("position", Vector3(target.x, y + PlaytestBody.HALF_H + 0.02, target.z))
|
body.set("position", Vector3(target.x, y + PlaytestBody.HALF_H + 0.02, target.z))
|
||||||
body.set("velocity", Vector3.ZERO)
|
body.set("velocity", Vector3.ZERO)
|
||||||
body.set("on_floor", true)
|
body.set("on_floor", true)
|
||||||
|
if sp["yaw"] != null:
|
||||||
|
body.set("yaw_deg", float(sp["yaw"]))
|
||||||
show_message("Spawnpunkt %d = %s: x=%.1f z=%.1f (Höhe %.1f) — Blickrichtung steht im HUD." % [
|
show_message("Spawnpunkt %d = %s: x=%.1f z=%.1f (Höhe %.1f) — Blickrichtung steht im HUD." % [
|
||||||
index, str(sp["label"]), target.x, target.z, y])
|
index, str(sp["label"]), target.x, target.z, y])
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const DEFAULT_ZONE_RADIUS := 6.0 # Meter — geräumig für einen Spieler-Sta
|
||||||
const MIN_ZONE_RADIUS := 2.0
|
const MIN_ZONE_RADIUS := 2.0
|
||||||
const MAX_ZONE_RADIUS := 30.0
|
const MAX_ZONE_RADIUS := 30.0
|
||||||
const RADIUS_KEY := "spawn_radius_m" # Unter design_notes (Loader-frei, siehe oben)
|
const RADIUS_KEY := "spawn_radius_m" # Unter design_notes (Loader-frei, siehe oben)
|
||||||
|
const YAW_KEY := "yaw_deg" # Am Punkt/an der Zone selbst, Server und Client lesen es
|
||||||
|
|
||||||
# TDM-Konvention (map_format.md): Team 0 = Blau = Prefix "A" (A1, A2 …),
|
# TDM-Konvention (map_format.md): Team 0 = Blau = Prefix "A" (A1, A2 …),
|
||||||
# Team 1 = Rot = Prefix "B" (B1, B2 …).
|
# Team 1 = Rot = Prefix "B" (B1, B2 …).
|
||||||
|
|
@ -152,6 +153,44 @@ static func snapshot_blocks(hist: ActionHistory, doc: Dictionary) -> Dictionary:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Blickrichtung beim Spawn (optionales Feld yaw_deg an Spawn-Punkten und Conquest-Zonen).
|
||||||
|
## Fehlt es, schaut der Spieler zur Kartenmitte — der Pfeil zeigt das grau an.
|
||||||
|
static func has_yaw(entry: Dictionary) -> bool:
|
||||||
|
return entry.has(YAW_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
static func yaw_towards(from: Vector3, to: Vector2) -> float:
|
||||||
|
var dx := to.x - from.x
|
||||||
|
var dz := to.y - from.z
|
||||||
|
if Vector2(dx, dz).length() < 0.5:
|
||||||
|
return 0.0
|
||||||
|
return fposmod(rad_to_deg(atan2(-dx, -dz)), 360.0)
|
||||||
|
|
||||||
|
|
||||||
|
## Die Richtung, die im Spiel gilt: gesetzte oder Standard (zur Kartenmitte).
|
||||||
|
static func effective_yaw(doc: Dictionary, entry: Dictionary) -> float:
|
||||||
|
if entry.has(YAW_KEY):
|
||||||
|
return fposmod(float(entry[YAW_KEY]), 360.0)
|
||||||
|
var pos: Array = entry.get("pos", [0.0, 0.0, 0.0])
|
||||||
|
return yaw_towards(Vector3(float(pos[0]), float(pos[1]), float(pos[2])), SymmetryCore.map_center(doc))
|
||||||
|
|
||||||
|
|
||||||
|
## yaw = Grad setzen, null = zurück auf Standard.
|
||||||
|
static func write_yaw(entry: Dictionary, yaw: Variant) -> void:
|
||||||
|
if yaw == null:
|
||||||
|
entry.erase(YAW_KEY)
|
||||||
|
else:
|
||||||
|
entry[YAW_KEY] = fposmod(roundf(float(yaw)), 360.0)
|
||||||
|
|
||||||
|
|
||||||
|
static func set_point_yaw(doc: Dictionary, team: int, pid: String, yaw: Variant) -> bool:
|
||||||
|
for p in points(doc, team):
|
||||||
|
if p is Dictionary and str(p.get("id", "")) == pid:
|
||||||
|
write_yaw(p, yaw)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
## Neue Spawn-Zone (Punkt) ans Team hängen. y = Terrain-Höhe (Punkt sitzt auf
|
## Neue Spawn-Zone (Punkt) ans Team hängen. y = Terrain-Höhe (Punkt sitzt auf
|
||||||
## dem Boden; der Loader prüft nur Grenzen, aber schwebende Spawns verwirren).
|
## dem Boden; der Loader prüft nur Grenzen, aber schwebende Spawns verwirren).
|
||||||
## Rückgabe: der neue Punkt (id + pos).
|
## Rückgabe: der neue Punkt (id + pos).
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ extends Node3D
|
||||||
## Bedienung:
|
## Bedienung:
|
||||||
## Linksklick = Zone setzen (Klick wie beim Objekt-Platzieren)
|
## Linksklick = Zone setzen (Klick wie beim Objekt-Platzieren)
|
||||||
## Linksklick auf Zone = Zone auswählen; ZIEHEN verschiebt sie (Terrain-Höhe)
|
## Linksklick auf Zone = Zone auswählen; ZIEHEN verschiebt sie (Terrain-Höhe)
|
||||||
|
## Pfeilspitze ziehen = Blickrichtung der Zone (auch Conquest-Zonen; grau = Standard)
|
||||||
## Regel rechts = Radius der neuen Zonen; mit Auswahl: Radius DER Zone
|
## Regel rechts = Radius der neuen Zonen; mit Auswahl: Radius DER Zone
|
||||||
## Entf = ausgewählte Zone löschen
|
## Entf = ausgewählte Zone löschen
|
||||||
## ESC = Auswahl weg / Werkzeug beendet (ESC in main.gd;
|
## ESC = Auswahl weg / Werkzeug beendet (ESC in main.gd;
|
||||||
|
|
@ -16,7 +17,7 @@ extends Node3D
|
||||||
## Feedback: halbtransparenter Kreis folgt dem Cursor auf dem Terrain
|
## Feedback: halbtransparenter Kreis folgt dem Cursor auf dem Terrain
|
||||||
## (Vorschau VOR dem Klick — wie der Objekt-Ghost), die gesetzte Zone leuchtet.
|
## (Vorschau VOR dem Klick — wie der Objekt-Ghost), die gesetzte Zone leuchtet.
|
||||||
|
|
||||||
const MODE_HELP := "Linksklick: Zone setzen · Klick+Ziehen: Zone verschieben · Entf: löschen ·\nRadius/Team rechts · Mittlere Maustaste: Kamera · Strg+Z: zurück"
|
const MODE_HELP := "Linksklick: Zone setzen · Klick+Ziehen: Zone verschieben · Pfeilspitze ziehen: Blickrichtung · Entf: löschen ·\nRadius/Team rechts · Mittlere Maustaste: Kamera · Strg+Z: zurück"
|
||||||
|
|
||||||
## Symmetrie (M4, Karte t_ffa207b1): Partner-Kopplung gespiegelter Spawn-Zonen.
|
## Symmetrie (M4, Karte t_ffa207b1): Partner-Kopplung gespiegelter Spawn-Zonen.
|
||||||
## Liegt in design_notes (Loader-frei, rundtrip-sicher — wie spawn_radius_m):
|
## Liegt in design_notes (Loader-frei, rundtrip-sicher — wie spawn_radius_m):
|
||||||
|
|
@ -41,6 +42,12 @@ var _drag_moved := false
|
||||||
var _press_pos := Vector2.ZERO
|
var _press_pos := Vector2.ZERO
|
||||||
var _grab_offset := Vector3.ZERO
|
var _grab_offset := Vector3.ZERO
|
||||||
|
|
||||||
|
# Pfeilspitze ziehen (Blickrichtung): Griff aus view_builder.yaw_handles(), {} = keiner
|
||||||
|
const ARROW_PICK_M := 2.5
|
||||||
|
const ARROW_SNAP_DEG := 5.0
|
||||||
|
var _arrow_grab: Dictionary = {}
|
||||||
|
var _arrow_yaw := 0.0
|
||||||
|
|
||||||
# Terrain-Cache für Cursor-Raycast (nur LESEN — dieses Werkzeug formt nichts)
|
# Terrain-Cache für Cursor-Raycast (nur LESEN — dieses Werkzeug formt nichts)
|
||||||
var _heights := PackedFloat32Array()
|
var _heights := PackedFloat32Array()
|
||||||
var _res := 0
|
var _res := 0
|
||||||
|
|
@ -156,6 +163,9 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event is InputEventMouseMotion:
|
if event is InputEventMouseMotion:
|
||||||
var mm := event as InputEventMouseMotion
|
var mm := event as InputEventMouseMotion
|
||||||
_update_cursor(mm.position)
|
_update_cursor(mm.position)
|
||||||
|
if not _arrow_grab.is_empty():
|
||||||
|
_drag_arrow(mm.position)
|
||||||
|
return
|
||||||
if _dragging and selected_team >= 0:
|
if _dragging and selected_team >= 0:
|
||||||
var hit: Variant = _ray_hit(mm.position)
|
var hit: Variant = _ray_hit(mm.position)
|
||||||
if hit == null:
|
if hit == null:
|
||||||
|
|
@ -231,6 +241,10 @@ func _terrain_half() -> Vector2:
|
||||||
# ----------------------------------------------------- Setzen/Auswählen -----
|
# ----------------------------------------------------- Setzen/Auswählen -----
|
||||||
|
|
||||||
func _on_left_press(viewport_pos: Vector2) -> void:
|
func _on_left_press(viewport_pos: Vector2) -> void:
|
||||||
|
_arrow_grab = _pick_arrow(viewport_pos)
|
||||||
|
if not _arrow_grab.is_empty():
|
||||||
|
_drag_arrow(viewport_pos)
|
||||||
|
return
|
||||||
var picked := _pick_spawn(viewport_pos)
|
var picked := _pick_spawn(viewport_pos)
|
||||||
if picked.is_empty():
|
if picked.is_empty():
|
||||||
_place_zone(viewport_pos)
|
_place_zone(viewport_pos)
|
||||||
|
|
@ -254,6 +268,9 @@ func _on_left_press(viewport_pos: Vector2) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_left_release() -> void:
|
func _on_left_release() -> void:
|
||||||
|
if not _arrow_grab.is_empty():
|
||||||
|
_commit_arrow()
|
||||||
|
return
|
||||||
if not _dragging:
|
if not _dragging:
|
||||||
return
|
return
|
||||||
_dragging = false
|
_dragging = false
|
||||||
|
|
@ -312,6 +329,80 @@ func _place_zone(viewport_pos: Vector2) -> void:
|
||||||
pid, SpawnOps.team_label(team), "" if mirror_text.is_empty() else " " + mirror_text])
|
pid, SpawnOps.team_label(team), "" if mirror_text.is_empty() else " " + mirror_text])
|
||||||
|
|
||||||
|
|
||||||
|
## Kursor-Strahl → nächste Pfeilspitze in Greifweite oder {}.
|
||||||
|
func _pick_arrow(viewport_pos: Vector2) -> Dictionary:
|
||||||
|
var hit: Variant = _ray_hit(viewport_pos)
|
||||||
|
if hit == null:
|
||||||
|
return {}
|
||||||
|
var p: Vector3 = hit as Vector3
|
||||||
|
var best := {}
|
||||||
|
var best_d := ARROW_PICK_M
|
||||||
|
for h in view_builder.yaw_handles():
|
||||||
|
var tip: Vector3 = h["tip"]
|
||||||
|
var d := Vector2(p.x - tip.x, p.z - tip.z).length()
|
||||||
|
if d <= best_d:
|
||||||
|
best = h
|
||||||
|
best_d = d
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
func _drag_arrow(viewport_pos: Vector2) -> void:
|
||||||
|
var hit: Variant = _ray_hit(viewport_pos)
|
||||||
|
var node: Node3D = _arrow_grab.get("node")
|
||||||
|
if hit == null or node == null or not is_instance_valid(node):
|
||||||
|
return
|
||||||
|
var c := node.global_position
|
||||||
|
var yaw := SpawnOps.yaw_towards(c, Vector2((hit as Vector3).x, (hit as Vector3).z))
|
||||||
|
_arrow_yaw = fposmod(snappedf(yaw, ARROW_SNAP_DEG), 360.0)
|
||||||
|
var arrow: MeshInstance3D = view_builder.yaw_arrow_of(node)
|
||||||
|
if arrow != null:
|
||||||
|
arrow.rotation.y = deg_to_rad(_arrow_yaw)
|
||||||
|
main.call("_set_status", "Blickrichtung von %s: %d°" % [str(_arrow_grab["id"]), int(_arrow_yaw)])
|
||||||
|
|
||||||
|
|
||||||
|
func _commit_arrow() -> void:
|
||||||
|
var grab := _arrow_grab
|
||||||
|
_arrow_grab = {}
|
||||||
|
if str(grab["kind"]) == "zone":
|
||||||
|
apply_zone_yaw(str(grab["id"]), _arrow_yaw)
|
||||||
|
return
|
||||||
|
selected_team = int(grab["team"])
|
||||||
|
selected_id = str(grab["id"])
|
||||||
|
set_selected_yaw(_arrow_yaw)
|
||||||
|
|
||||||
|
|
||||||
|
## Blickrichtung der ausgewählten Zone setzen (yaw = null: Standard). EIN Undo-Eintrag.
|
||||||
|
func set_selected_yaw(yaw: Variant) -> void:
|
||||||
|
if selected_team < 0 or selected_id == "":
|
||||||
|
return
|
||||||
|
var before: Dictionary = SpawnOps.snapshot_blocks(main.history, main.doc)
|
||||||
|
if not SpawnOps.set_point_yaw(main.doc, selected_team, selected_id, yaw):
|
||||||
|
return
|
||||||
|
spawn_mirror_yaw(main.doc, selected_id, yaw, _sym_mode())
|
||||||
|
main.history.commit("Blickrichtung von %s" % selected_id,
|
||||||
|
before, SpawnOps.snapshot_blocks(main.history, main.doc))
|
||||||
|
main.call("_after_structure_change")
|
||||||
|
view_builder.call("set_spawn_selected", selected_team, selected_id)
|
||||||
|
refresh_selection_panel()
|
||||||
|
main.call("_set_status", "Blickrichtung von %s: %s — Strg+Z macht das rückgängig." % [
|
||||||
|
selected_id, "Standard (zur Kartenmitte)" if yaw == null else "%d°" % int(yaw)])
|
||||||
|
|
||||||
|
|
||||||
|
## Dasselbe für eine Conquest-Zone (der Griff im Bild, das Panel geht über modus_panel).
|
||||||
|
func apply_zone_yaw(zid: String, yaw: Variant) -> void:
|
||||||
|
var zones: Variant = main.doc.get("conquest_zones", [])
|
||||||
|
if not (zones is Array):
|
||||||
|
return
|
||||||
|
var before := {"conquest_zones": (zones as Array).duplicate(true)}
|
||||||
|
for z in zones:
|
||||||
|
if z is Dictionary and str(z.get("id", "")) == zid:
|
||||||
|
SpawnOps.write_yaw(z, yaw)
|
||||||
|
main.history.commit("Blickrichtung von %s" % zid, before,
|
||||||
|
{"conquest_zones": (zones as Array).duplicate(true)})
|
||||||
|
main.call("_after_structure_change")
|
||||||
|
main.call("_set_status", "Blickrichtung von %s: %d° — Strg+Z macht das rückgängig." % [zid, int(yaw)])
|
||||||
|
|
||||||
|
|
||||||
## Kursor-Strahl → getroffene Zone ({team, id}) oder {}.
|
## Kursor-Strahl → getroffene Zone ({team, id}) oder {}.
|
||||||
func _pick_spawn(viewport_pos: Vector2) -> Dictionary:
|
func _pick_spawn(viewport_pos: Vector2) -> Dictionary:
|
||||||
var hit: Variant = _ray_hit(viewport_pos)
|
var hit: Variant = _ray_hit(viewport_pos)
|
||||||
|
|
@ -451,6 +542,8 @@ static func spawn_mirror_add(doc: Dictionary, team: int, pid: String, mode: int)
|
||||||
var twin_id := str(twin["id"])
|
var twin_id := str(twin["id"])
|
||||||
SpawnOps.set_radius(doc, twin_id, SpawnOps.zone_radius(doc, pid))
|
SpawnOps.set_radius(doc, twin_id, SpawnOps.zone_radius(doc, pid))
|
||||||
_mirror_map_set(doc, pid, twin_id)
|
_mirror_map_set(doc, pid, twin_id)
|
||||||
|
if SpawnOps.has_yaw(original):
|
||||||
|
SpawnOps.write_yaw(twin, SymmetryCore.mirror_facing(mode, float(original[SpawnOps.YAW_KEY])))
|
||||||
return {"ok": true, "status": "Gespiegelte Zone %s (%s) gesetzt." % [
|
return {"ok": true, "status": "Gespiegelte Zone %s (%s) gesetzt." % [
|
||||||
twin_id, SpawnOps.team_label(1 - team)]}
|
twin_id, SpawnOps.team_label(1 - team)]}
|
||||||
|
|
||||||
|
|
@ -480,6 +573,17 @@ static func spawn_mirror_move(doc: Dictionary, team: int, pid: String, mode: int
|
||||||
return "Gespiegelte Zone %s mitverschoben." % partner
|
return "Gespiegelte Zone %s mitverschoben." % partner
|
||||||
|
|
||||||
|
|
||||||
|
## Blickrichtung spiegeln: Zwilling schaut gespiegelt (yaw = null: auch er auf Standard).
|
||||||
|
static func spawn_mirror_yaw(doc: Dictionary, pid: String, yaw: Variant, mode: int) -> void:
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return
|
||||||
|
var partner := spawn_mirror_partner(doc, pid)
|
||||||
|
var partner_team := _team_of_point(doc, partner) if not partner.is_empty() else -1
|
||||||
|
if partner_team >= 0:
|
||||||
|
SpawnOps.set_point_yaw(doc, partner_team, partner,
|
||||||
|
null if yaw == null else SymmetryCore.mirror_facing(mode, float(yaw)))
|
||||||
|
|
||||||
|
|
||||||
## Löschung spiegeln (entfernen → beide weg, inkl. Radius-Eintrag und
|
## Löschung spiegeln (entfernen → beide weg, inkl. Radius-Eintrag und
|
||||||
## Partner-Map). Rückgabe: Klartext-Status ("" = nur das Original war weg).
|
## Partner-Map). Rückgabe: Klartext-Status ("" = nur das Original war weg).
|
||||||
static func spawn_mirror_delete(doc: Dictionary, pid: String, mode: int) -> String:
|
static func spawn_mirror_delete(doc: Dictionary, pid: String, mode: int) -> String:
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,20 @@ static func mirror_rot_y(mode: int, rot_y_deg: float) -> float:
|
||||||
return rot_y_deg
|
return rot_y_deg
|
||||||
|
|
||||||
|
|
||||||
|
## Spiegelt eine Blickrichtung (Grad, Blick = (-sin, 0, -cos)) exakt mit: anders als
|
||||||
|
## mirror_rot_y zeigt der Pfeil danach wirklich in die gespiegelte Richtung.
|
||||||
|
static func mirror_facing(mode: int, yaw_deg: float) -> float:
|
||||||
|
match mode:
|
||||||
|
Mode.POINT:
|
||||||
|
return fposmod(yaw_deg + 180.0, 360.0)
|
||||||
|
Mode.AXIS_X:
|
||||||
|
return fposmod(-yaw_deg, 360.0)
|
||||||
|
Mode.AXIS_Z:
|
||||||
|
return fposmod(180.0 - yaw_deg, 360.0)
|
||||||
|
_:
|
||||||
|
return yaw_deg
|
||||||
|
|
||||||
|
|
||||||
## true, wenn der Punkt unter diesem Modus auf sich selbst abgebildet wird
|
## true, wenn der Punkt unter diesem Modus auf sich selbst abgebildet wird
|
||||||
## (Zentrum bei Punktspiegelung, Spiegelachse bei Achsenspiegelung, ± Toleranz).
|
## (Zentrum bei Punktspiegelung, Spiegelachse bei Achsenspiegelung, ± Toleranz).
|
||||||
## Solche Elemente werden beim gespiegelten Bauen NICHT gedoppelt.
|
## Solche Elemente werden beim gespiegelten Bauen NICHT gedoppelt.
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,8 @@ static func _validate_zone_list(cps, code: String, what: String, bounds: Diction
|
||||||
var r = c.get("radius_m", DEFAULT_RADIUS)
|
var r = c.get("radius_m", DEFAULT_RADIUS)
|
||||||
if not _num(r) or float(r) < MIN_RADIUS or float(r) > MAX_RADIUS:
|
if not _num(r) or float(r) < MIN_RADIUS or float(r) > MAX_RADIUS:
|
||||||
errors.append("%s: %s '%s': radius_m muss 3–60 sein." % [code, what, str(cid)])
|
errors.append("%s: %s '%s': radius_m muss 3–60 sein." % [code, what, str(cid)])
|
||||||
|
if c.has("yaw_deg") and not _num(c["yaw_deg"]):
|
||||||
|
errors.append("%s: %s '%s': yaw_deg muss endlich sein." % [code, what, str(cid)])
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -244,6 +246,8 @@ static func _validate_mode_spawns(ms, bounds: Dictionary) -> PackedStringArray:
|
||||||
errors.append("V24: Spawn '%s': pos muss [x, y, z] mit endlichen Werten sein." % pid)
|
errors.append("V24: Spawn '%s': pos muss [x, y, z] mit endlichen Werten sein." % pid)
|
||||||
elif not _inside(pos, bounds):
|
elif not _inside(pos, bounds):
|
||||||
errors.append("V24: Spawn '%s' liegt außerhalb der Match-Grenzen." % pid)
|
errors.append("V24: Spawn '%s' liegt außerhalb der Match-Grenzen." % pid)
|
||||||
|
if p.has("yaw_deg") and not _num(p["yaw_deg"]):
|
||||||
|
errors.append("V24: Spawn '%s': yaw_deg muss endlich sein." % pid)
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -438,6 +438,8 @@ static func _validate_spawns(zones: Dictionary, bounds: Dictionary) -> PackedStr
|
||||||
errors.append("V9: Spawn '%s': pos muss [x, y, z] mit endlichen Werten sein." % str(pid))
|
errors.append("V9: Spawn '%s': pos muss [x, y, z] mit endlichen Werten sein." % str(pid))
|
||||||
elif not _inside_bounds(pos, bounds):
|
elif not _inside_bounds(pos, bounds):
|
||||||
errors.append("V9: Spawn '%s' liegt außerhalb der Match-Grenzen." % str(pid))
|
errors.append("V9: Spawn '%s' liegt außerhalb der Match-Grenzen." % str(pid))
|
||||||
|
if p.has("yaw_deg") and not _is_finite_num(p["yaw_deg"]):
|
||||||
|
errors.append("V9: Spawn '%s': yaw_deg muss endlich sein." % str(pid))
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ var spawn_radius_label: Label
|
||||||
var spawn_status: Label # Live-Validierung (IMMER sichtbar, Loader-Regeln)
|
var spawn_status: Label # Live-Validierung (IMMER sichtbar, Loader-Regeln)
|
||||||
var spawn_sel_label: Label # Welche Zone ist an?
|
var spawn_sel_label: Label # Welche Zone ist an?
|
||||||
var spawn_delete_btn: Button
|
var spawn_delete_btn: Button
|
||||||
|
var spawn_yaw_row: HBoxContainer
|
||||||
|
var spawn_yaw_spin: SpinBox
|
||||||
|
var spawn_yaw_reset: Button
|
||||||
|
|
||||||
|
|
||||||
static func spec() -> Dictionary:
|
static func spec() -> Dictionary:
|
||||||
|
|
@ -75,6 +78,23 @@ func build() -> void:
|
||||||
spawn_sel_label.text = "Keine Zone ausgewählt."
|
spawn_sel_label.text = "Keine Zone ausgewählt."
|
||||||
spawn_sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
spawn_sel_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
spawn_panel.add_child(spawn_sel_label)
|
spawn_panel.add_child(spawn_sel_label)
|
||||||
|
spawn_yaw_row = HBoxContainer.new()
|
||||||
|
spawn_yaw_row.add_theme_constant_override("separation", UIScale.px(8))
|
||||||
|
spawn_yaw_row.add_child(_field_label("Blickrichtung °"))
|
||||||
|
spawn_yaw_spin = SpinBox.new()
|
||||||
|
spawn_yaw_spin.min_value = 0.0
|
||||||
|
spawn_yaw_spin.max_value = 359.0
|
||||||
|
spawn_yaw_spin.step = 1.0
|
||||||
|
spawn_yaw_spin.tooltip_text = "Wohin Spieler und Bots hier beim Spawnen schauen. Geht auch im Bild: Pfeilspitze ziehen."
|
||||||
|
spawn_yaw_spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
spawn_yaw_spin.value_changed.connect(_on_spawn_yaw_changed)
|
||||||
|
spawn_yaw_row.add_child(spawn_yaw_spin)
|
||||||
|
spawn_yaw_reset = Button.new()
|
||||||
|
spawn_yaw_reset.text = "Standard"
|
||||||
|
spawn_yaw_reset.tooltip_text = "Keine feste Richtung: Spieler schauen zur Kartenmitte, Bots zum Gegner (grauer Pfeil)"
|
||||||
|
spawn_yaw_reset.pressed.connect(func() -> void: main.spawn_tool.call("set_selected_yaw", null))
|
||||||
|
spawn_yaw_row.add_child(spawn_yaw_reset)
|
||||||
|
spawn_panel.add_child(spawn_yaw_row)
|
||||||
spawn_delete_btn = Button.new()
|
spawn_delete_btn = Button.new()
|
||||||
spawn_delete_btn.text = "Zone löschen (Entf)"
|
spawn_delete_btn.text = "Zone löschen (Entf)"
|
||||||
spawn_delete_btn.custom_minimum_size = UIScale.v2(0, 40)
|
spawn_delete_btn.custom_minimum_size = UIScale.v2(0, 40)
|
||||||
|
|
@ -118,6 +138,11 @@ func _on_spawn_radius_changed(value: float) -> void:
|
||||||
main.spawn_tool.call("set_radius", value)
|
main.spawn_tool.call("set_radius", value)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_spawn_yaw_changed(value: float) -> void:
|
||||||
|
if str(main.get("tool_mode")) == "spawns":
|
||||||
|
main.spawn_tool.call("set_selected_yaw", value)
|
||||||
|
|
||||||
|
|
||||||
func _on_spawn_delete_pressed() -> void:
|
func _on_spawn_delete_pressed() -> void:
|
||||||
if str(main.get("tool_mode")) == "spawns":
|
if str(main.get("tool_mode")) == "spawns":
|
||||||
main.spawn_tool.call("delete_selected")
|
main.spawn_tool.call("delete_selected")
|
||||||
|
|
@ -132,7 +157,13 @@ func sync_spawn_selection() -> void:
|
||||||
if team < 0 or pid == "":
|
if team < 0 or pid == "":
|
||||||
spawn_sel_label.text = "Keine Zone ausgewählt.\nKlicke im Viewport auf einen Kreis."
|
spawn_sel_label.text = "Keine Zone ausgewählt.\nKlicke im Viewport auf einen Kreis."
|
||||||
spawn_delete_btn.disabled = true
|
spawn_delete_btn.disabled = true
|
||||||
|
spawn_yaw_row.visible = false
|
||||||
return
|
return
|
||||||
|
spawn_yaw_row.visible = true
|
||||||
|
for p in SpawnOps.points(main.doc, team):
|
||||||
|
if p is Dictionary and str(p.get("id", "")) == pid:
|
||||||
|
spawn_yaw_spin.set_value_no_signal(SpawnOps.effective_yaw(main.doc, p))
|
||||||
|
spawn_yaw_reset.disabled = not SpawnOps.has_yaw(p)
|
||||||
spawn_sel_label.text = "Ausgewählt: %s (%s)\nVerschieben: klicken+ziehen · Radius: Regler oben" % [
|
spawn_sel_label.text = "Ausgewählt: %s (%s)\nVerschieben: klicken+ziehen · Radius: Regler oben" % [
|
||||||
pid, SpawnOps.team_label(team)]
|
pid, SpawnOps.team_label(team)]
|
||||||
spawn_delete_btn.disabled = false
|
spawn_delete_btn.disabled = false
|
||||||
|
|
|
||||||
|
|
@ -195,8 +195,10 @@ func edit_zone(id: String, fields: Dictionary) -> void:
|
||||||
var nx := clampf(float(fields.get("x", p[0])), -half.x, half.x)
|
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)
|
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]
|
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
|
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") else "")
|
_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 ----
|
# ---- Spawn-Sätze ----
|
||||||
|
|
@ -378,6 +380,18 @@ func _row(cp: Dictionary, index := 0, count := 1, mode := "conquest") -> Control
|
||||||
nums.add_child(_spin("Radius", "radius_m", id, float(cp.get("radius_m", ModusKatalog.DEFAULT_RADIUS)),
|
nums.add_child(_spin("Radius", "radius_m", id, float(cp.get("radius_m", ModusKatalog.DEFAULT_RADIUS)),
|
||||||
ModusKatalog.MIN_RADIUS, ModusKatalog.MAX_RADIUS, 1.0))
|
ModusKatalog.MIN_RADIUS, ModusKatalog.MAX_RADIUS, 1.0))
|
||||||
box.add_child(nums)
|
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
|
return box
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -512,6 +512,10 @@ func _rebuild_spawn_markers(data: Dictionary) -> void:
|
||||||
var marker := _spawn_marker(
|
var marker := _spawn_marker(
|
||||||
Vector3(float(pos[0]), float(pos[1]), float(pos[2])),
|
Vector3(float(pos[0]), float(pos[1]), float(pos[2])),
|
||||||
team, pid, SpawnOps.zone_radius(data, pid))
|
team, pid, SpawnOps.zone_radius(data, pid))
|
||||||
|
var yaw_col: Color = TEAM_COLORS[team] if team < TEAM_COLORS.size() else Color.WHITE
|
||||||
|
marker.set_meta("yaw_color", yaw_col)
|
||||||
|
marker.add_child(_yaw_arrow(SpawnOps.zone_radius(data, pid),
|
||||||
|
SpawnOps.effective_yaw(data, p), SpawnOps.has_yaw(p), yaw_col))
|
||||||
spawn_markers.append(marker)
|
spawn_markers.append(marker)
|
||||||
add_child(marker)
|
add_child(marker)
|
||||||
|
|
||||||
|
|
@ -577,6 +581,10 @@ func rebuild_zone_markers(data: Dictionary) -> void:
|
||||||
label.font_size = 160
|
label.font_size = 160
|
||||||
label.pixel_size = 0.01
|
label.pixel_size = 0.01
|
||||||
node.add_child(label)
|
node.add_child(label)
|
||||||
|
if mode == ModusKatalog.MODE_CONQUEST: # Hills sind keine Spawns
|
||||||
|
node.set_meta("zone_id", str(cp.get("id", "")))
|
||||||
|
node.set_meta("yaw_radius", r)
|
||||||
|
node.add_child(_yaw_arrow(r, SpawnOps.effective_yaw(data, cp), SpawnOps.has_yaw(cp), col))
|
||||||
zone_root.add_child(node)
|
zone_root.add_child(node)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -602,6 +610,73 @@ func _spawn_marker(pos: Vector3, team: int, pid: String, radius: float) -> Node3
|
||||||
return marker
|
return marker
|
||||||
|
|
||||||
|
|
||||||
|
## Pfeil der Blickrichtung: flach am Boden, zeigt lokal nach -Z, rotation.y = Blickrichtung.
|
||||||
|
## Gesetzte Richtung kräftig, Standard (zur Kartenmitte) blass grau. Ohne Tiefentest, damit
|
||||||
|
## Hügel und Objekte ihn nicht verdecken.
|
||||||
|
func _yaw_arrow(radius: float, yaw_deg: float, is_set: bool, col: Color) -> MeshInstance3D:
|
||||||
|
var length := yaw_arrow_length(radius)
|
||||||
|
var half_w := maxf(0.25, length * 0.04)
|
||||||
|
var head_w := half_w * 3.0
|
||||||
|
var head_l := minf(length * 0.4, head_w * 2.5)
|
||||||
|
var neck := -(length - head_l)
|
||||||
|
var verts := PackedVector3Array([
|
||||||
|
Vector3(-half_w, 0, 0), Vector3(half_w, 0, 0), Vector3(half_w, 0, neck),
|
||||||
|
Vector3(-half_w, 0, 0), Vector3(half_w, 0, neck), Vector3(-half_w, 0, neck),
|
||||||
|
Vector3(-head_w, 0, neck), Vector3(head_w, 0, neck), Vector3(0, 0, -length),
|
||||||
|
])
|
||||||
|
var arrays := []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX)
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
|
mat.no_depth_test = true
|
||||||
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
mat.albedo_color = col.lightened(0.35) if is_set else Color(0.75, 0.75, 0.75, 0.55)
|
||||||
|
var arrow := MeshInstance3D.new()
|
||||||
|
arrow.name = "YawArrow"
|
||||||
|
arrow.mesh = mesh
|
||||||
|
arrow.material_override = mat
|
||||||
|
arrow.position.y = 0.4
|
||||||
|
arrow.rotation.y = deg_to_rad(yaw_deg)
|
||||||
|
arrow.set_meta("yaw_deg", yaw_deg)
|
||||||
|
arrow.set_meta("yaw_set", is_set)
|
||||||
|
arrow.set_meta("yaw_length", length)
|
||||||
|
return arrow
|
||||||
|
|
||||||
|
|
||||||
|
static func yaw_arrow_length(radius: float) -> float:
|
||||||
|
return maxf(radius, 3.0) + 2.0
|
||||||
|
|
||||||
|
|
||||||
|
func yaw_arrow_of(node: Node3D) -> MeshInstance3D:
|
||||||
|
return node.get_node_or_null("YawArrow") as MeshInstance3D
|
||||||
|
|
||||||
|
|
||||||
|
## Alle Pfeile zum Anfassen: [{kind:"spawn"|"zone", team, id, node, tip:Vector3}].
|
||||||
|
func yaw_handles() -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
var add := func(node: Node3D, entry: Dictionary) -> void:
|
||||||
|
var arrow := yaw_arrow_of(node)
|
||||||
|
if arrow == null:
|
||||||
|
return
|
||||||
|
var yaw := deg_to_rad(float(arrow.get_meta("yaw_deg", 0.0)))
|
||||||
|
var length := float(arrow.get_meta("yaw_length", 0.0))
|
||||||
|
entry["node"] = node
|
||||||
|
entry["tip"] = node.global_position + Vector3(-sin(yaw), 0.0, -cos(yaw)) * length
|
||||||
|
out.append(entry)
|
||||||
|
for marker in spawn_markers:
|
||||||
|
if marker != null and is_instance_valid(marker):
|
||||||
|
add.call(marker, {"kind": "spawn", "team": int(marker.get_meta("spawn_team", -1)), "id": str(marker.get_meta("spawn_id", ""))})
|
||||||
|
if zone_root != null and is_instance_valid(zone_root):
|
||||||
|
for node in zone_root.get_children():
|
||||||
|
if node.has_meta("zone_id"):
|
||||||
|
add.call(node, {"kind": "zone", "team": -1, "id": str(node.get_meta("zone_id"))})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Kreisfläche EINES Markers (neu) bauen — Werkzeuge rufen nach Radius-Änderung
|
## Kreisfläche EINES Markers (neu) bauen — Werkzeuge rufen nach Radius-Änderung
|
||||||
## style_spawn_marker auf. U6 (t_639135dd): die Fläche bleibt IMMER gleich
|
## style_spawn_marker auf. U6 (t_639135dd): die Fläche bleibt IMMER gleich
|
||||||
## dezent (Teamfarbe, halbtransparent) — die Auswahl zeigt sich als schmaler
|
## dezent (Teamfarbe, halbtransparent) — die Auswahl zeigt sich als schmaler
|
||||||
|
|
@ -668,6 +743,13 @@ func style_spawn_marker(marker: Node3D, radius: float, selected: bool) -> void:
|
||||||
var ring := _spawn_ring(radius)
|
var ring := _spawn_ring(radius)
|
||||||
marker.add_child(ring)
|
marker.add_child(ring)
|
||||||
marker.set_meta("spawn_ring", ring)
|
marker.set_meta("spawn_ring", ring)
|
||||||
|
var arrow := yaw_arrow_of(marker)
|
||||||
|
if arrow != null: # Länge folgt dem Radius
|
||||||
|
var yaw_deg := float(arrow.get_meta("yaw_deg", 0.0))
|
||||||
|
var yaw_set := bool(arrow.get_meta("yaw_set", false))
|
||||||
|
arrow.name = "YawArrowAlt"
|
||||||
|
arrow.queue_free()
|
||||||
|
marker.add_child(_yaw_arrow(radius, yaw_deg, yaw_set, marker.get_meta("yaw_color", Color.WHITE)))
|
||||||
|
|
||||||
|
|
||||||
## Auswahl-Highlight über ALLE Spawn-Marker (Werkzeug ruft bei Änderung).
|
## Auswahl-Highlight über ALLE Spawn-Marker (Werkzeug ruft bei Änderung).
|
||||||
|
|
|
||||||
59
tests/test_spawn_yaw.gd
Normal file
59
tests/test_spawn_yaw.gd
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Blickrichtung an Spawn-Punkten und Conquest-Zonen (optionales Feld yaw_deg).
|
||||||
|
## Ausführen: godot --headless --path . --script res://tests/test_spawn_yaw.gd
|
||||||
|
|
||||||
|
var failures := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _check(cond: bool, label: String) -> void:
|
||||||
|
if not cond:
|
||||||
|
failures += 1
|
||||||
|
print("FAIL: ", label)
|
||||||
|
|
||||||
|
|
||||||
|
func _fwd(deg: float) -> Vector2:
|
||||||
|
var r := deg_to_rad(deg)
|
||||||
|
return Vector2(-sin(r), -cos(r))
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
SpawnOps.edit_set = ModusKatalog.SPAWN_SET_BASE
|
||||||
|
var doc := SvmapDocument.create_new("Blick", 512.0, "mittag")
|
||||||
|
var p := SpawnOps.add_zone(doc, 0, -50.0, 0.0)
|
||||||
|
var pid := str(p["id"])
|
||||||
|
_check(not SpawnOps.has_yaw(p), "neuer Punkt ohne Feld (alte Karten bleiben gleich)")
|
||||||
|
# Standard: zur Kartenmitte (0,0) — von -50 aus also nach +X.
|
||||||
|
_check(_fwd(SpawnOps.effective_yaw(doc, p)).distance_to(Vector2(1, 0)) < 0.01, "Standard zeigt zur Mitte")
|
||||||
|
_check(SpawnOps.set_point_yaw(doc, 0, pid, 450.0), "setzen")
|
||||||
|
_check(is_equal_approx(float(p["yaw_deg"]), 90.0), "normalisiert auf 0–360")
|
||||||
|
SpawnOps.set_point_yaw(doc, 0, pid, null)
|
||||||
|
_check(not p.has("yaw_deg"), "null räumt das Feld weg")
|
||||||
|
_check(not SpawnOps.set_point_yaw(doc, 0, "gibtsnicht", 1.0), "unbekannte ID")
|
||||||
|
|
||||||
|
# Spiegeln: die Blickrichtung muss wirklich gespiegelt sein.
|
||||||
|
for yaw in [0.0, 30.0, 135.0, 290.0]:
|
||||||
|
var f := _fwd(yaw)
|
||||||
|
_check(_fwd(SymmetryCore.mirror_facing(SymmetryCore.Mode.AXIS_X, yaw)).distance_to(Vector2(-f.x, f.y)) < 0.01, "Achse X %d" % int(yaw))
|
||||||
|
_check(_fwd(SymmetryCore.mirror_facing(SymmetryCore.Mode.AXIS_Z, yaw)).distance_to(Vector2(f.x, -f.y)) < 0.01, "Achse Z %d" % int(yaw))
|
||||||
|
_check(_fwd(SymmetryCore.mirror_facing(SymmetryCore.Mode.POINT, yaw)).distance_to(-f) < 0.01, "Punkt %d" % int(yaw))
|
||||||
|
|
||||||
|
# Zwilling erbt und folgt.
|
||||||
|
var doc2 := SvmapDocument.create_new("Blick2", 512.0, "mittag")
|
||||||
|
var a := SpawnOps.add_zone(doc2, 0, -40.0, 10.0)
|
||||||
|
SpawnOps.write_yaw(a, 30.0)
|
||||||
|
var tool_script := load("res://src/editor/spawn_tool.gd")
|
||||||
|
tool_script.spawn_mirror_add(doc2, 0, str(a["id"]), SymmetryCore.Mode.POINT)
|
||||||
|
var twin_id: String = tool_script.spawn_mirror_partner(doc2, str(a["id"]))
|
||||||
|
var twin: Dictionary = tool_script._spawn_point_of(doc2, 1, twin_id)
|
||||||
|
_check(is_equal_approx(float(twin.get("yaw_deg", -1.0)), 210.0), "Zwilling schaut gespiegelt")
|
||||||
|
tool_script.spawn_mirror_yaw(doc2, str(a["id"]), null, SymmetryCore.Mode.POINT)
|
||||||
|
_check(not twin.has("yaw_deg"), "Standard gilt auch für den Zwilling")
|
||||||
|
|
||||||
|
# Prüfung: Unsinn wird abgelehnt, das Feld selbst nicht.
|
||||||
|
var bounds: Dictionary = doc["bounds"]
|
||||||
|
var zone := {"id": "Z1", "pos": [0.0, 0.0, 0.0], "radius_m": 12.0, "yaw_deg": 45.0}
|
||||||
|
_check(ModusKatalog._validate_zone_list([zone], "V22", "Zone", bounds).is_empty(), "Zone mit yaw_deg gültig")
|
||||||
|
zone["yaw_deg"] = "Norden"
|
||||||
|
_check(not ModusKatalog._validate_zone_list([zone], "V22", "Zone", bounds).is_empty(), "Zone mit Text-yaw abgelehnt")
|
||||||
|
print("ALL OK" if failures == 0 else "FAILED: %d" % failures)
|
||||||
|
quit(0 if failures == 0 else 1)
|
||||||
Loading…
Reference in a new issue