map: signature crossings + fix skyline setback clipping the road
buildBridges() adds the two landmark structures the generated road was
missing, driven off the existing per-sample tunnel/bridge flags in
Circuit.luau (200 tunnel / 474 bridge samples, recorded at trace time):
* NihonbashiViaduct - box girder carried on piers over the historic
stone bridge, the C1's most recognisable stretch.
* Shiodome cable-stayed span - twin H-towers with fanned stays.
Also fixes buildSkyline() placing geometry inside the racing surface:
* the setback test compared the building CENTRE against 220 studs, so a
220-wide tower still reached 110 studs inward - through the tunnel
bore walls. Now tests the outer face (near - w/2) against
ROAD_WIDTH/2 + 60.
* it strided samples by 3, skipping the closest approach on tight
corners. Now tests every sample.
* NeonSign hangs off the +X face and was never clearance-checked at
all; it now gets its own test.
Verified in Studio against a full rebuild (823 samples -> 2468 road
parts): 400 tunnel walls / 200 ceilings for the 200 flagged samples,
621 buildings placed, and raycasts over every road part report 0
headroom and 0 lateral obstructions. Minimum building clearance 47.8
studs.
This commit is contained in:
+297
-6
@@ -562,21 +562,28 @@ local function buildSkyline(map: Folder)
|
|||||||
local x = math.sin(a) * r - 477
|
local x = math.sin(a) * r - 477
|
||||||
local z = math.cos(a) * r - 473
|
local z = math.cos(a) * r - 473
|
||||||
|
|
||||||
-- keep buildings off the racing surface
|
local h = 120 + det(i * 5.1) ^ 2.2 * 900
|
||||||
|
local w = 90 + det(i * 8.3) * 130
|
||||||
|
|
||||||
|
-- keep buildings off the racing surface.
|
||||||
|
-- The test must use the building's OUTER FACE, not its centre: a 220-stud
|
||||||
|
-- centre setback still lets a 220-wide tower reach 110 studs inward, which
|
||||||
|
-- put it through the tunnel bore (walls sit at ROAD_WIDTH/2 + 1.5 + 3).
|
||||||
|
-- Sample every point too -- striding by 3 skipped the closest approach on
|
||||||
|
-- tight corners.
|
||||||
|
local CLEAR = ROAD_WIDTH / 2 + 60
|
||||||
local near = math.huge
|
local near = math.huge
|
||||||
for k = 1, n, 3 do
|
for k = 1, n do
|
||||||
local s = samples[k].pos
|
local s = samples[k].pos
|
||||||
local d = (Vector3.new(s.X, 0, s.Z) - Vector3.new(x, 0, z)).Magnitude
|
local d = (Vector3.new(s.X, 0, s.Z) - Vector3.new(x, 0, z)).Magnitude
|
||||||
if d < near then
|
if d < near then
|
||||||
near = d
|
near = d
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if near < 220 then
|
if near - w / 2 < CLEAR then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
local h = 120 + det(i * 5.1) ^ 2.2 * 900
|
|
||||||
local w = 90 + det(i * 8.3) * 130
|
|
||||||
local b = part({
|
local b = part({
|
||||||
Name = "Building",
|
Name = "Building",
|
||||||
Size = Vector3.new(w, h, w),
|
Size = Vector3.new(w, h, w),
|
||||||
@@ -588,16 +595,28 @@ local function buildSkyline(map: Folder)
|
|||||||
b.Parent = folder
|
b.Parent = folder
|
||||||
placed += 1
|
placed += 1
|
||||||
|
|
||||||
|
-- signs hang off the +X face, so they need their own clearance check
|
||||||
if det(i * 13.7) > 0.72 then
|
if det(i * 13.7) > 0.72 then
|
||||||
|
local sx = x + w / 2 + 6
|
||||||
|
local snear = math.huge
|
||||||
|
for k = 1, n do
|
||||||
|
local s = samples[k].pos
|
||||||
|
local d = (Vector3.new(s.X, 0, s.Z) - Vector3.new(sx, 0, z)).Magnitude
|
||||||
|
if d < snear then
|
||||||
|
snear = d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if snear > CLEAR then
|
||||||
local sign = part({
|
local sign = part({
|
||||||
Name = "NeonSign",
|
Name = "NeonSign",
|
||||||
Size = Vector3.new(12, h * 0.42, 5),
|
Size = Vector3.new(12, h * 0.42, 5),
|
||||||
CFrame = CFrame.new(x + w / 2, h * 0.62, z),
|
CFrame = CFrame.new(sx, h * 0.62, z),
|
||||||
Color = ({ NEON_PINK, NEON_BLUE, NEON_TEAL, NEON_AMBER })[(i % 4) + 1],
|
Color = ({ NEON_PINK, NEON_BLUE, NEON_TEAL, NEON_AMBER })[(i % 4) + 1],
|
||||||
Material = Enum.Material.Neon,
|
Material = Enum.Material.Neon,
|
||||||
})
|
})
|
||||||
sign.Parent = folder
|
sign.Parent = folder
|
||||||
end
|
end
|
||||||
|
end
|
||||||
if placed > 620 then
|
if placed > 620 then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -614,6 +633,277 @@ local function buildSkyline(map: Folder)
|
|||||||
ground.Parent = folder
|
ground.Parent = folder
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Signature crossings.
|
||||||
|
|
||||||
|
Two hand-placed structures on top of the generated road:
|
||||||
|
|
||||||
|
* NihonbashiViaduct -- the C1's most famous stretch. The expressway runs
|
||||||
|
on a box girder directly OVER the historic Nihonbashi stone bridge and
|
||||||
|
the Nihonbashi-gawa. The stone bridge underneath is the actual landmark,
|
||||||
|
so it is modelled too (deck, balustrades, ornamental posts, arches).
|
||||||
|
|
||||||
|
* CableStayed -- a red twin-tower cable-stayed span on the straightest
|
||||||
|
bay-side run, with fanned stay cables and aviation warning lights.
|
||||||
|
|
||||||
|
Both index into Circuit.samples, so they follow the road wherever it moves.
|
||||||
|
Span indices were chosen from the georeferenced OSM trace: Nihonbashi maps
|
||||||
|
to the Edobashi-junction samples, the cable-stayed span to the Shiodome
|
||||||
|
straight (lowest cumulative heading change of any elevated, non-tunnel run).
|
||||||
|
]]
|
||||||
|
local function buildBridges(map: Folder)
|
||||||
|
local folder = Instance.new("Folder")
|
||||||
|
folder.Name = "Bridges"
|
||||||
|
folder.Parent = map
|
||||||
|
|
||||||
|
local samples = Circuit.samples
|
||||||
|
local n = Circuit.SAMPLE_COUNT
|
||||||
|
|
||||||
|
local function mid(i: number): Vector3
|
||||||
|
local a = samples[i]
|
||||||
|
local b = samples[(i % n) + 1]
|
||||||
|
return (a.pos + b.pos) / 2
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------- Nihonbashi
|
||||||
|
do
|
||||||
|
local NB = Instance.new("Folder")
|
||||||
|
NB.Name = "NihonbashiViaduct"
|
||||||
|
NB.Parent = folder
|
||||||
|
|
||||||
|
local A, B = 46, 62
|
||||||
|
local pa, pb = samples[A].pos, samples[B].pos
|
||||||
|
local centre = (pa + pb) / 2
|
||||||
|
local axis = pb - pa
|
||||||
|
local flat = Vector3.new(axis.X, 0, axis.Z).Unit
|
||||||
|
local right = flat:Cross(UP)
|
||||||
|
local span = axis.Magnitude
|
||||||
|
|
||||||
|
-- Nihonbashi-gawa flowing under the viaduct
|
||||||
|
local waterCF = CFrame.lookAt(Vector3.new(centre.X, 2, centre.Z), Vector3.new(centre.X, 2, centre.Z) + flat, UP)
|
||||||
|
local water = part({
|
||||||
|
Name = "RiverWater",
|
||||||
|
Size = Vector3.new(150, 3, span + 300),
|
||||||
|
CFrame = waterCF,
|
||||||
|
Color = Color3.fromRGB(18, 32, 42),
|
||||||
|
Material = Enum.Material.Glass,
|
||||||
|
})
|
||||||
|
water.Reflectance = 0.35
|
||||||
|
water.Parent = NB
|
||||||
|
|
||||||
|
for _, side in ipairs({ -1, 1 }) do
|
||||||
|
local q = part({
|
||||||
|
Name = "RiverWall",
|
||||||
|
Size = Vector3.new(10, 26, span + 300),
|
||||||
|
CFrame = waterCF * CFrame.new(side * 80, 11, 0),
|
||||||
|
Color = Color3.fromRGB(58, 56, 52),
|
||||||
|
Material = Enum.Material.Concrete,
|
||||||
|
})
|
||||||
|
q.Parent = NB
|
||||||
|
end
|
||||||
|
|
||||||
|
-- the historic stone bridge, crossing UNDER the expressway
|
||||||
|
local bpos = Vector3.new(centre.X, 16, centre.Z)
|
||||||
|
local bcf = CFrame.lookAt(bpos, bpos + right, UP)
|
||||||
|
local deck = part({
|
||||||
|
Name = "NihonbashiStoneDeck",
|
||||||
|
Size = Vector3.new(46, 4, 180),
|
||||||
|
CFrame = bcf,
|
||||||
|
Color = Color3.fromRGB(150, 142, 128),
|
||||||
|
Material = Enum.Material.Slate,
|
||||||
|
})
|
||||||
|
deck.Parent = NB
|
||||||
|
|
||||||
|
for _, s in ipairs({ -1, 1 }) do
|
||||||
|
local bal = part({
|
||||||
|
Name = "Balustrade",
|
||||||
|
Size = Vector3.new(2.5, 5, 180),
|
||||||
|
CFrame = bcf * CFrame.new(s * 22, 4, 0),
|
||||||
|
Color = Color3.fromRGB(168, 160, 146),
|
||||||
|
Material = Enum.Material.Slate,
|
||||||
|
})
|
||||||
|
bal.Parent = NB
|
||||||
|
for k = -2, 2 do
|
||||||
|
local post = part({
|
||||||
|
Name = "BridgePost",
|
||||||
|
Size = Vector3.new(6, 14, 6),
|
||||||
|
CFrame = bcf * CFrame.new(s * 22, 8, k * 44),
|
||||||
|
Color = Color3.fromRGB(120, 112, 100),
|
||||||
|
Material = Enum.Material.Slate,
|
||||||
|
})
|
||||||
|
post.Parent = NB
|
||||||
|
if math.abs(k) == 2 then
|
||||||
|
local lamp = part({
|
||||||
|
Name = "BridgeLamp",
|
||||||
|
Size = Vector3.new(4, 6, 4),
|
||||||
|
CFrame = bcf * CFrame.new(s * 22, 17, k * 44),
|
||||||
|
Color = Color3.fromRGB(255, 214, 150),
|
||||||
|
Material = Enum.Material.Neon,
|
||||||
|
})
|
||||||
|
local pl = Instance.new("PointLight")
|
||||||
|
pl.Brightness = 2
|
||||||
|
pl.Range = 60
|
||||||
|
pl.Color = Color3.fromRGB(255, 214, 150)
|
||||||
|
pl.Parent = lamp
|
||||||
|
lamp.Parent = NB
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, s in ipairs({ -1, 1 }) do
|
||||||
|
local arch = part({
|
||||||
|
Name = "StoneArch",
|
||||||
|
Size = Vector3.new(40, 22, 70),
|
||||||
|
CFrame = bcf * CFrame.new(0, -10, s * 46),
|
||||||
|
Color = Color3.fromRGB(138, 130, 118),
|
||||||
|
Material = Enum.Material.Slate,
|
||||||
|
})
|
||||||
|
arch.Parent = NB
|
||||||
|
end
|
||||||
|
|
||||||
|
-- box girder + pier bents carrying the expressway deck
|
||||||
|
for i = A, B do
|
||||||
|
local m = mid(i)
|
||||||
|
local fwd = Circuit.forwardAt(i)
|
||||||
|
local cf = CFrame.lookAt(m, m + fwd, UP)
|
||||||
|
local g = part({
|
||||||
|
Name = "BoxGirder",
|
||||||
|
Size = Vector3.new(ROAD_WIDTH + 10, 9, 64),
|
||||||
|
CFrame = cf * CFrame.new(0, -6, 0),
|
||||||
|
Color = Color3.fromRGB(64, 64, 72),
|
||||||
|
Material = Enum.Material.Metal,
|
||||||
|
})
|
||||||
|
g.Parent = NB
|
||||||
|
|
||||||
|
if i % 3 == 0 then
|
||||||
|
local y = samples[i].pos.Y
|
||||||
|
for _, side in ipairs({ -1, 1 }) do
|
||||||
|
local leg = part({
|
||||||
|
Name = "ViaductPier",
|
||||||
|
Size = Vector3.new(9, math.max(y - 14, 4), 9),
|
||||||
|
CFrame = CFrame.new(Vector3.new(m.X, math.max(y - 14, 4) / 2 + 14, m.Z) + right * (side * 34)),
|
||||||
|
Color = Color3.fromRGB(72, 70, 76),
|
||||||
|
Material = Enum.Material.Concrete,
|
||||||
|
})
|
||||||
|
leg.Parent = NB
|
||||||
|
end
|
||||||
|
local cap = part({
|
||||||
|
Name = "PierCap",
|
||||||
|
Size = Vector3.new(88, 6, 12),
|
||||||
|
CFrame = cf * CFrame.new(0, -13, 0),
|
||||||
|
Color = Color3.fromRGB(72, 70, 76),
|
||||||
|
Material = Enum.Material.Concrete,
|
||||||
|
})
|
||||||
|
cap.Parent = NB
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------- cable-stayed
|
||||||
|
do
|
||||||
|
local CS = Instance.new("Folder")
|
||||||
|
CS.Name = "CableStayed"
|
||||||
|
CS.Parent = folder
|
||||||
|
|
||||||
|
local A, B = 646, 659
|
||||||
|
local pa, pb = samples[A].pos, samples[B].pos
|
||||||
|
local axis = pb - pa
|
||||||
|
local flat = Vector3.new(axis.X, 0, axis.Z).Unit
|
||||||
|
local right = flat:Cross(UP)
|
||||||
|
local deckY = pa.Y
|
||||||
|
local TOWER_H = 210
|
||||||
|
local RED = Color3.fromRGB(196, 72, 72)
|
||||||
|
|
||||||
|
local tops = {}
|
||||||
|
for _, idx in ipairs({ A + 3, B - 3 }) do
|
||||||
|
local c = mid(idx)
|
||||||
|
for _, side in ipairs({ -1, 1 }) do
|
||||||
|
local top = deckY + TOWER_H
|
||||||
|
local leg = part({
|
||||||
|
Name = "BridgeTower",
|
||||||
|
Size = Vector3.new(12, top, 12),
|
||||||
|
CFrame = CFrame.new(Vector3.new(c.X, top / 2, c.Z) + right * (side * 56)),
|
||||||
|
Color = RED,
|
||||||
|
Material = Enum.Material.Metal,
|
||||||
|
})
|
||||||
|
leg.Parent = CS
|
||||||
|
table.insert(tops, { pos = Vector3.new(c.X, top, c.Z) + right * (side * 56), idx = idx, side = side })
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, h in ipairs({ 0.55, 0.85 }) do
|
||||||
|
local y = deckY + TOWER_H * h
|
||||||
|
local beam = part({
|
||||||
|
Name = "TowerCrossbeam",
|
||||||
|
Size = Vector3.new(124, 8, 8),
|
||||||
|
CFrame = CFrame.lookAt(Vector3.new(c.X, y, c.Z), Vector3.new(c.X, y, c.Z) + flat, UP)
|
||||||
|
* CFrame.Angles(0, math.pi / 2, 0),
|
||||||
|
Color = RED,
|
||||||
|
Material = Enum.Material.Metal,
|
||||||
|
})
|
||||||
|
beam.Parent = CS
|
||||||
|
end
|
||||||
|
|
||||||
|
local al = part({
|
||||||
|
Name = "AviationLight",
|
||||||
|
Size = Vector3.new(6, 3, 6),
|
||||||
|
CFrame = CFrame.new(c.X, deckY + TOWER_H + 3, c.Z),
|
||||||
|
Color = Color3.fromRGB(255, 60, 60),
|
||||||
|
Material = Enum.Material.Neon,
|
||||||
|
})
|
||||||
|
local pl = Instance.new("PointLight")
|
||||||
|
pl.Brightness = 3
|
||||||
|
pl.Range = 140
|
||||||
|
pl.Color = Color3.fromRGB(255, 60, 60)
|
||||||
|
pl.Parent = al
|
||||||
|
al.Parent = CS
|
||||||
|
end
|
||||||
|
|
||||||
|
-- fanned stay cables
|
||||||
|
for _, t in ipairs(tops) do
|
||||||
|
for k = 1, 6 do
|
||||||
|
local target = math.clamp(t.idx + (k * 2 - 7), A, B)
|
||||||
|
local anchor = mid(target) + right * (t.side * 46) + Vector3.new(0, 2, 0)
|
||||||
|
local delta = anchor - t.pos
|
||||||
|
local dist = delta.Magnitude
|
||||||
|
if dist > 1 then
|
||||||
|
local cable = part({
|
||||||
|
Name = "Cable",
|
||||||
|
Size = Vector3.new(0.9, 0.9, dist),
|
||||||
|
CFrame = CFrame.lookAt(t.pos + delta / 2, anchor, UP),
|
||||||
|
Color = Color3.fromRGB(225, 225, 232),
|
||||||
|
Material = Enum.Material.Metal,
|
||||||
|
})
|
||||||
|
cable.Parent = CS
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = A, B do
|
||||||
|
local m = mid(i)
|
||||||
|
local fwd = Circuit.forwardAt(i)
|
||||||
|
local g = part({
|
||||||
|
Name = "BridgeGirder",
|
||||||
|
Size = Vector3.new(ROAD_WIDTH + 14, 7, 64),
|
||||||
|
CFrame = CFrame.lookAt(m, m + fwd, UP) * CFrame.new(0, -5, 0),
|
||||||
|
Color = RED,
|
||||||
|
Material = Enum.Material.Metal,
|
||||||
|
})
|
||||||
|
g.Parent = CS
|
||||||
|
end
|
||||||
|
|
||||||
|
local centre = (pa + pb) / 2
|
||||||
|
local bay = part({
|
||||||
|
Name = "BayWater",
|
||||||
|
Size = Vector3.new(900, 3, axis.Magnitude + 500),
|
||||||
|
CFrame = CFrame.lookAt(Vector3.new(centre.X, 1, centre.Z), Vector3.new(centre.X, 1, centre.Z) + flat, UP),
|
||||||
|
Color = Color3.fromRGB(14, 26, 38),
|
||||||
|
Material = Enum.Material.Glass,
|
||||||
|
})
|
||||||
|
bay.Reflectance = 0.4
|
||||||
|
bay.Parent = CS
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function MapBuilder.build(): Folder
|
function MapBuilder.build(): Folder
|
||||||
local existing = Workspace:FindFirstChild("Map")
|
local existing = Workspace:FindFirstChild("Map")
|
||||||
if existing then
|
if existing then
|
||||||
@@ -628,6 +918,7 @@ function MapBuilder.build(): Folder
|
|||||||
buildPA(map)
|
buildPA(map)
|
||||||
buildLandmarks(map)
|
buildLandmarks(map)
|
||||||
buildSkyline(map)
|
buildSkyline(map)
|
||||||
|
buildBridges(map)
|
||||||
|
|
||||||
-- Start/finish across the road at sample 1
|
-- Start/finish across the road at sample 1
|
||||||
local s1 = Circuit.samples[1]
|
local s1 = Circuit.samples[1]
|
||||||
|
|||||||
Reference in New Issue
Block a user