9 junction events across 6 levels, auto-generated from real OSM intersections (tools/osm_junction.py): each crossing street's ways become lane-offset convoy streams, deterministic speeds/intervals/models -- learnable puzzles, not dice. Menu lists CRASH JCT entries; crash -> 9s settle with camera-relative aftertouch on the wreck and a one-shot crashbreaker shockwave (blast() on traffic, radial impulse on debris, orange boom). Damage tally vs bronze/silver/gold targets. Fixed zero-normal UV projection that striped roads. Smoke test drives a full junction run to tally (k, gold). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate crash-junction events from real OSM intersections.
|
|
|
|
Usage: osm_junction.py <area.json> <out junctions.json> <lat0> <lon0> "Name|Street A|Street B" ...
|
|
|
|
For each spec: junction = shared node of the two streets. Every same-named way
|
|
passing within RADIUS of it becomes a convoy stream (clipped, plus a reversed
|
|
copy lane-offset the other way). Spawn point sits ~120m up Street A aimed at
|
|
the junction. Coordinates are level-local Godot metres (x east, z south).
|
|
"""
|
|
import json
|
|
import math
|
|
import sys
|
|
|
|
RADIUS = 170.0
|
|
LANE = 2.7
|
|
TARGETS = [15000, 35000, 60000]
|
|
|
|
src, out = sys.argv[1], sys.argv[2]
|
|
LAT0, LON0 = float(sys.argv[3]), float(sys.argv[4])
|
|
M_LAT = 110574.0
|
|
M_LON = 111320.0 * math.cos(math.radians(LAT0))
|
|
|
|
def xz(lat, lon):
|
|
return ((lon - LON0) * M_LON, -(lat - LAT0) * M_LAT)
|
|
|
|
data = json.load(open(src))
|
|
ways = {}
|
|
for e in data["elements"]:
|
|
t = e.get("tags", {})
|
|
if e["type"] == "way" and t.get("highway") and t.get("name") and e.get("geometry"):
|
|
ways.setdefault(t["name"], []).append(e)
|
|
|
|
def offset(pts, d):
|
|
outp = []
|
|
for i, p in enumerate(pts):
|
|
a = pts[max(i - 1, 0)]
|
|
b = pts[min(i + 1, len(pts) - 1)]
|
|
dx, dz = b[0] - a[0], b[1] - a[1]
|
|
L = math.hypot(dx, dz) or 1.0
|
|
outp.append([p[0] + dz / L * d, p[1] - dx / L * d])
|
|
return outp
|
|
|
|
events = []
|
|
for spec in sys.argv[5:]:
|
|
name, a, b = spec.split("|")[:3]
|
|
na = {n for w in ways.get(a, []) for n in w["nodes"]}
|
|
nb = {n for w in ways.get(b, []) for n in w["nodes"]}
|
|
shared = na & nb
|
|
if not shared:
|
|
print("SKIP %s: %s never meets %s" % (name, a, b))
|
|
continue
|
|
jn = sorted(shared)[0]
|
|
jpt = None
|
|
for w in ways[a]:
|
|
if jn in w["nodes"]:
|
|
g = w["geometry"][w["nodes"].index(jn)]
|
|
jpt = xz(g["lat"], g["lon"])
|
|
streams = []
|
|
spawn = None
|
|
for street in (a, b):
|
|
for w in ways[street]:
|
|
pts = [xz(g["lat"], g["lon"]) for g in w["geometry"]]
|
|
near = [p for p in pts if math.hypot(p[0] - jpt[0], p[1] - jpt[1]) < RADIUS]
|
|
if len(near) < 2:
|
|
continue
|
|
length = sum(math.hypot(near[i + 1][0] - near[i][0], near[i + 1][1] - near[i][1])
|
|
for i in range(len(near) - 1))
|
|
if length < 60:
|
|
continue
|
|
streams.append({"pts": offset(near, LANE), "speed": 12.0, "interval": 2.2})
|
|
streams.append({"pts": offset(list(reversed(near)), LANE), "speed": 11.0, "interval": 2.6})
|
|
if street == a and spawn is None:
|
|
best = max(near, key=lambda p: math.hypot(p[0] - jpt[0], p[1] - jpt[1]))
|
|
dx, dz = jpt[0] - best[0], jpt[1] - best[1]
|
|
L = math.hypot(dx, dz) or 1.0
|
|
spawn = {"x": best[0], "z": best[1], "dx": dx / L, "dz": dz / L}
|
|
if not streams or spawn is None:
|
|
print("SKIP %s: no usable streams" % name)
|
|
continue
|
|
events.append({"name": name, "spawn": spawn, "streams": streams[:6], "targets": TARGETS})
|
|
print("OK %s: %d streams, spawn %.0fm out" % (name, min(len(streams), 6),
|
|
math.hypot(spawn["x"] - jpt[0], spawn["z"] - jpt[1])))
|
|
|
|
json.dump(events, open(out, "w"), indent=1)
|
|
print("wrote %s (%d events)" % (out, len(events)))
|