ShitboxInfinity/tools/osm_junction.py
type-two 565caa6f85 Crash Mode junctions: scripted convoys, Aftertouch, Crashbreaker, medals
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>
2026-07-28 00:14:33 +10:00

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)))