Rework the PA junction to match the reference drawing: yellow = PA -> highway (entry), red = highway -> PA (exit). Entry and exit swap sides (entry now EAST, exit WEST) and both trunks hug the PA perimeter instead of running concentric spirals. The simplification: both carriageways sit at the same deck level, adjacent, so the median opening IS the far-side access path. The under-deck crossing and merge runs are deleted outright -- 8 runs collapse to 4, and the girder-soffit ceiling logic that constrained the crossing goes with them. Part count 24,210 -> 21,691. The median opening is now load-bearing rather than cosmetic (it is the only route from the far carriageway to the ramps), so it widens from 6 to 9 samples per junction. Orientation is a single flip-able constant (ORIENT) so a mirrored result is a one-line correction, not a rewrite. Measured on the built map (not asserted): - 600 entry + 600 exit ramp parts - max grade 8.00% / 8.27%, under the 9% limit - 0 collisions with piers/masts/girders (558 structural parts) - segment continuity <= 2.5 studs; trunk->merge handoff <= 1.7 studs - merge lands at dY 0.0 (exact deck level), 33 studs lateral - median open at junctions: 0 parts vs. control ~7 on plain mainline Two traps worth recording, both caught by controls: 1. Studio's require() cache silently rebuilt STALE code. The first "passing" build reported 17,575 parts and zero ramps, and the map contained PAOffRamp/PAOnRamp parts whose names exist nowhere in the current source. Every measurement taken before this was describing an old map. Added tools/rebuild_fresh.luau, which clones the module to bust the cache; use it rather than requiring MapBuilder directly. 2. A reported "median never opens" bug was a bad probe, not a defect. It counted `Barrier` (the outer edge parapet, which must NEVER open) instead of `Median_<i>`. Re-run against the right name: control 7, junctions 0. Both were only visible because the probes carry controls -- a 0-hit result with no control is vacuous. The verification harnesses are kept in tools/ with their controls built in.
2785 lines
108 KiB
Luau
2785 lines
108 KiB
Luau
--!strict
|
|
--[[
|
|
MapBuilder.luau (SERVER)
|
|
v4 — "C1: Shuto Inner Circular". The road is no longer an invented
|
|
rounded-rectangle: it is the REAL Shuto Expressway Inner Circular Route
|
|
(首都高速都心環状線) traced from OpenStreetMap and driven straight off
|
|
Circuit.samples, so the map and the lap timing share one geometry.
|
|
|
|
What that buys you, all of it authentic:
|
|
* The true C1 shape — Tanimachi JCT, Miyakezaka, Kasumigaseki, Ginza,
|
|
Kyobashi, Edobashi, Hitotsubashi — in their real relative positions.
|
|
* Real tunnel sections (the C1 spends a lot of its length underground)
|
|
built as lit bores; real bridge/viaduct sections built elevated on
|
|
pillars over the city.
|
|
* Tokyo landmarks placed at their true coordinates relative to the
|
|
loop: Tokyo Tower, the Imperial Palace moat, Tokyo Station, Ginza,
|
|
the Shiodome towers, Roppongi Hills, the National Diet, Skytree.
|
|
* A Daikoku-style PA hanging off the straightest non-tunnel run.
|
|
|
|
Gameplay parts keep their contract names (SpawnPad, FinishLine, KillWall_*)
|
|
so nothing else in the codebase changes.
|
|
]]
|
|
|
|
local Workspace = game:GetService("Workspace")
|
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
local Circuit = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Circuit"))
|
|
|
|
local MapBuilder = {}
|
|
|
|
local ROAD_WIDTH = Circuit.ROAD_WIDTH
|
|
local WALL_HEIGHT = 26
|
|
|
|
-- Divided-expressway layout (real Wangan/C1 geometry) -------------------------
|
|
-- The deck is ONE slab; the median is what splits it into two carriageways
|
|
-- running in opposite directions. Outer edges get a visible concrete barrier
|
|
-- (you can see it, like real life) backed by an invisible taller wall so a car
|
|
-- at 300 km/h can't tunnel through it in a single physics step.
|
|
-- Dimensions and palette verified against real photographs of Daikoku JCT /
|
|
-- Daikoku Futo PA (Wikimedia Commons aerial + ground refs) and OSM tagging of
|
|
-- the junction (way data: lanes=2, oneway=yes per carriageway, layers 1-4).
|
|
-- Corrections made after looking at the real thing rather than guessing:
|
|
-- * Shutoko structures are PALE CONCRETE / off-white, not dark grey.
|
|
-- * The median is a NARROW raised kerb, not a wide chunky wall.
|
|
-- * The edge is a low white parapet TOPPED BY A DARK METAL RAILING -- that
|
|
-- silhouette is the single most recognisable feature of the road.
|
|
-- * Asphalt reads mid-grey in daylight, not near-black.
|
|
local MEDIAN_WIDTH = 6
|
|
local MEDIAN_HEIGHT = 3.2
|
|
local BARRIER_H = 5 -- white concrete parapet height
|
|
local BARRIER_T = 2.2 -- parapet thickness
|
|
local RAIL_H = 3 -- dark metal railing sitting on the parapet
|
|
-- Steel box-girder depth under bridge decks (鋼箱桁). Real Shutoko girders run
|
|
-- roughly 1/20 of the span depth; at ~35 m spans that's ~1.8 m => ~6.4 studs.
|
|
local GIRDER_H = 6.4
|
|
|
|
local C_ASPHALT = Color3.fromRGB(78, 78, 82)
|
|
local C_CONCRETE = Color3.fromRGB(214, 212, 205) -- Shutoko pale concrete
|
|
local C_PARAPET = Color3.fromRGB(226, 224, 218)
|
|
local C_RAIL = Color3.fromRGB(96, 100, 106)
|
|
-- Weathered grey-green steel of the box girders under the elevated sections.
|
|
local C_GIRDER = Color3.fromRGB(122, 128, 124)
|
|
local UP = Vector3.new(0, 1, 0)
|
|
|
|
-- REAL-WORLD SCALE ------------------------------------------------------------
|
|
-- The rest of the codebase converts speed with 0.28 m per stud, so all the
|
|
-- metric road-standard figures below are converted through the same constant.
|
|
-- Sanity check: the 49,383-stud loop comes out at 13.8 km against the real
|
|
-- C1 Inner Circular Route's 14.8 km, so the circuit is very close to 1:1.
|
|
local M = 1 / 0.28 -- studs per metre (~3.571)
|
|
|
|
-- Road markings per 全標協 (Japan Road Marking Assoc.) 2012 p.42 / 2018 pp.97-98,
|
|
-- read from the primary standard rather than repeated folklore:
|
|
-- * 車線境界線 (lane boundary, dashed): line length 3-10 m, gap = 1x to 2x the
|
|
-- line length, width 10-15 cm (15 cm "desirable"). The 8 m line / 12 m gap
|
|
-- used here is the high-speed end of that legal range (gap = 1.5x line) and
|
|
-- is the configuration expressways actually use -- it's what makes the
|
|
-- rhythm read as an expressway rather than a 5 m/5 m surface street.
|
|
-- * Where overtaking is undesirable (屈曲部 -- curves), the standard says the
|
|
-- line becomes SOLID rather than dashed. On expressways that no-lane-change
|
|
-- line is painted yellow.
|
|
-- * NOTE: the 45 cm width sometimes quoted for "expressway lines" is NOT the
|
|
-- lane boundary -- it's the boundary against a 変速車線 (accel/decel lane).
|
|
-- Applied below at ramps only.
|
|
local DASH_PAINT = 8 * M -- 28.6 studs
|
|
local DASH_GAP = 12 * M -- 42.9 studs
|
|
local DASH_CYCLE = DASH_PAINT + DASH_GAP -- 20 m
|
|
local LANE_LINE_W = 0.15 * M -- 0.54 studs
|
|
local EDGE_LINE_W = 0.20 * M -- 0.71 studs
|
|
local RAMP_LINE_W = 0.45 * M -- 1.61 studs; through lane vs speed-change lane
|
|
local LANE_W = 3.25 * M -- 11.6 studs; narrow urban lane used on the C1
|
|
-- 導流帯 (gore/zebra) per 全標協 2018 pp.97-98: 45 cm white solid lines repeated
|
|
-- every 1.0 m, bounded by a 15-20 cm edge line.
|
|
local ZEBRA_W = 0.45 * M
|
|
local ZEBRA_PITCH = 1.0 * M
|
|
local MARK_Y = 0.1 -- sits just proud of the deck to avoid z-fighting
|
|
local MARK_H = 2.1
|
|
-- Heading change per 60-stud sample above which the curve counts as tight
|
|
-- enough that the real road would paint a solid yellow no-lane-change line.
|
|
local TIGHT_CURVE = math.rad(4)
|
|
|
|
local C_PAINT_WHITE = Color3.fromRGB(232, 232, 236)
|
|
local C_PAINT_YELLOW = Color3.fromRGB(240, 190, 40) -- 黄色 no-lane-change line
|
|
|
|
-- Signage colours sampled from the official Japanese sign artwork (Wikimedia
|
|
-- Commons SVGs of the 標識令 plates), not eyeballed:
|
|
-- guide sign (案内標識) green #005D35
|
|
-- Shutoko route badge green #007F1F
|
|
local C_SIGN_GREEN = Color3.fromRGB(0, 93, 53)
|
|
local C_BADGE_GREEN = Color3.fromRGB(0, 127, 31)
|
|
local C_SIGN_WHITE = Color3.fromRGB(255, 255, 255)
|
|
|
|
-- Lighting. Japanese expressway/tunnel lighting is high-pressure sodium
|
|
-- (warm amber) with newer LED sections in cool white -- NOT the blue/teal
|
|
-- that reads as generic sci-fi. Tunnel basic lighting runs continuously.
|
|
local C_SODIUM = Color3.fromRGB(255, 176, 92)
|
|
local C_TUNNEL_LED = Color3.fromRGB(255, 244, 224)
|
|
|
|
local NEON_PINK = Color3.fromRGB(255, 40, 120)
|
|
local NEON_BLUE = Color3.fromRGB(40, 160, 255)
|
|
local NEON_TEAL = Color3.fromRGB(40, 230, 200)
|
|
local NEON_AMBER = Color3.fromRGB(255, 170, 60)
|
|
|
|
-- SECTION PROFILE (real C1 lane counts and posted speeds) ---------------------
|
|
-- The C1 is NOT uniform. Verified against the 車線・最高速度 table in the
|
|
-- 首都高速都心環状線 article:
|
|
--
|
|
-- 江戸橋JCT内 2 = 1+1 40 km/h
|
|
-- 江戸橋JCT - 汐留JCT 4 = 2+2 50 km/h
|
|
-- 汐留JCT - 浜崎橋JCT 6 = 3+3 50 km/h <-- widest section
|
|
-- 浜崎橋JCT内 4 = 2+2 40 km/h
|
|
-- 浜崎橋JCT - 江戸橋JCT 4 = 2+2 50 km/h
|
|
-- 京橋JCT - 旧白魚橋乗継所 3 = 1+2 40 km/h
|
|
--
|
|
-- So junction interiors are posted 40, mainline 50, and the Shiodome ->
|
|
-- Hamazakibashi run carries three lanes each way rather than two.
|
|
local JCT_ZONES: { [string]: boolean } = {
|
|
["Edobashi junction"] = true,
|
|
["Takebashi Junction"] = true,
|
|
["Miyakezaka JCT"] = true,
|
|
["Miyakezaka Junction"] = true,
|
|
["Tanimachi JCT"] = true,
|
|
}
|
|
-- 汐留JCT - 浜崎橋JCT: 6 lanes (3+3).
|
|
local WIDE_ZONES: { [string]: boolean } = { ["Shiodome"] = true }
|
|
-- 銀座: the 半地下 section built in the filled-in 楓川/築地川 river beds. Several
|
|
-- of the original river bridges survive as 跨道橋 crossing OVER the expressway,
|
|
-- and their piers stand BETWEEN the running lanes -- which is why the real road
|
|
-- bans lane changes through here.
|
|
local GINZA_ZONES: { [string]: boolean } = { ["Ginza"] = true }
|
|
|
|
-- Deck width needed for 3+3: 6 x 3.25 m lanes + median + shoulders.
|
|
local WIDE_ROAD_WIDTH = 82
|
|
local MAX_ROAD_WIDTH = math.max(ROAD_WIDTH, WIDE_ROAD_WIDTH)
|
|
|
|
local function zoneOf(i: number): string
|
|
return Circuit.samples[i].piece
|
|
end
|
|
local function isJctSample(i: number): boolean
|
|
return JCT_ZONES[zoneOf(i)] == true
|
|
end
|
|
local function lanesAt(i: number): number
|
|
return WIDE_ZONES[zoneOf(i)] and 3 or 2
|
|
end
|
|
|
|
-- Per-sample deck width, smoothed so the carriageway tapers into and out of the
|
|
-- 3+3 section instead of stepping by 22 studs between two adjacent segments.
|
|
local ROAD_W: { number } = {}
|
|
do
|
|
local n = #Circuit.samples
|
|
local base: { number } = {}
|
|
for i = 1, n do
|
|
base[i] = WIDE_ZONES[Circuit.samples[i].piece] and WIDE_ROAD_WIDTH or ROAD_WIDTH
|
|
end
|
|
local TAPER = 6
|
|
for i = 1, n do
|
|
local acc = 0
|
|
for k = -TAPER, TAPER do
|
|
acc += base[((i - 1 + k) % n) + 1]
|
|
end
|
|
ROAD_W[i] = acc / (2 * TAPER + 1)
|
|
end
|
|
end
|
|
local function widthAt(i: number): number
|
|
return ROAD_W[i]
|
|
end
|
|
|
|
local function part(props: { [string]: any }): Part
|
|
local p = Instance.new("Part")
|
|
p.Anchored = true
|
|
p.TopSurface = Enum.SurfaceType.Smooth
|
|
p.BottomSurface = Enum.SurfaceType.Smooth
|
|
for k, v in pairs(props) do
|
|
(p :: any)[k] = v
|
|
end
|
|
return p
|
|
end
|
|
|
|
-- Deterministic pseudo-random so every server builds the same city ------------
|
|
local function det(seed: number): number
|
|
local x = math.sin(seed * 12.9898) * 43758.5453
|
|
return x - math.floor(x)
|
|
end
|
|
|
|
-- Real interchange names. Japanese is what's actually painted on the gantry;
|
|
-- the romaji underneath is the smaller secondary line every Shutoko guide sign
|
|
-- carries. Names cross-checked against OpenStreetMap's tagging of the real
|
|
-- Inner Circular Route (name / name:en on the motorway_junction nodes).
|
|
local JUNCTION_NAMES: { [string]: { string } } = {
|
|
["Kyobashi"] = { "京橋", "Kyobashi" },
|
|
["Takaracho"] = { "宝町", "Takaracho" },
|
|
["Edobashi junction"] = { "江戸橋JCT", "Edobashi JCT" },
|
|
["Takebashi Junction"] = { "竹橋JCT", "Takebashi JCT" },
|
|
["Hitotsubashi"] = { "一ツ橋", "Hitotsubashi" },
|
|
["Kitanomaru"] = { "北の丸", "Kitanomaru" },
|
|
["Daikancho"] = { "代官町", "Daikancho" },
|
|
["Miyakezaka JCT"] = { "三宅坂JCT", "Miyakezaka JCT" },
|
|
["Miyakezaka Junction"] = { "三宅坂JCT", "Miyakezaka JCT" },
|
|
["Kasumigaseki"] = { "霞が関", "Kasumigaseki" },
|
|
["Tanimachi JCT"] = { "谷町JCT", "Tanimachi JCT" },
|
|
["Iikura"] = { "飯倉", "Iikura" },
|
|
["Shiodome"] = { "汐留", "Shiodome" },
|
|
["Ginza"] = { "銀座", "Ginza" },
|
|
["Shintomicho"] = { "新富町", "Shintomicho" },
|
|
}
|
|
|
|
-- Japanese expressway guide sign (案内標識). Green board, white text, white
|
|
-- hairline border inset from the edge, Japanese place name large with the
|
|
-- romaji reading beneath it, and the circular route badge on the left.
|
|
local function makeSign(text: string, cf: CFrame, parent: Instance)
|
|
local names = JUNCTION_NAMES[text] or { text, text }
|
|
local ja, romaji = names[1], names[2]
|
|
|
|
local board = part({
|
|
Name = "Sign",
|
|
Size = Vector3.new(34, 10, 1),
|
|
CFrame = cf,
|
|
Color = C_SIGN_GREEN,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
board.Parent = parent
|
|
|
|
local gui = Instance.new("SurfaceGui")
|
|
gui.Face = Enum.NormalId.Front
|
|
gui.CanvasSize = Vector2.new(680, 200)
|
|
gui.Parent = board
|
|
|
|
-- Thin white border inset from the panel edge -- present on every real
|
|
-- Japanese guide sign and a big part of why they read as "Japanese".
|
|
local border = Instance.new("Frame")
|
|
border.Size = UDim2.new(1, -24, 1, -24)
|
|
border.Position = UDim2.fromOffset(12, 12)
|
|
border.BackgroundTransparency = 1
|
|
border.BorderSizePixel = 3
|
|
border.BorderColor3 = C_SIGN_WHITE
|
|
border.Parent = gui
|
|
|
|
-- Route badge: the C1 shield, green disc with white ring and "C1".
|
|
local badge = Instance.new("Frame")
|
|
badge.Size = UDim2.fromOffset(104, 104)
|
|
badge.Position = UDim2.fromOffset(34, 48)
|
|
badge.BackgroundColor3 = C_BADGE_GREEN
|
|
badge.BorderSizePixel = 0
|
|
badge.Parent = gui
|
|
local badgeCorner = Instance.new("UICorner")
|
|
badgeCorner.CornerRadius = UDim.new(1, 0)
|
|
badgeCorner.Parent = badge
|
|
local badgeStroke = Instance.new("UIStroke")
|
|
badgeStroke.Color = C_SIGN_WHITE
|
|
badgeStroke.Thickness = 5
|
|
badgeStroke.Parent = badge
|
|
local badgeText = Instance.new("TextLabel")
|
|
badgeText.Size = UDim2.fromScale(1, 1)
|
|
badgeText.BackgroundTransparency = 1
|
|
badgeText.Font = Enum.Font.GothamBold
|
|
badgeText.TextColor3 = C_SIGN_WHITE
|
|
badgeText.TextScaled = true
|
|
badgeText.Text = "C1"
|
|
badgeText.Parent = badge
|
|
|
|
-- Japanese name (primary, large)
|
|
local jaLabel = Instance.new("TextLabel")
|
|
jaLabel.Size = UDim2.new(1, -190, 0, 96)
|
|
jaLabel.Position = UDim2.fromOffset(168, 36)
|
|
jaLabel.BackgroundTransparency = 1
|
|
jaLabel.Font = Enum.Font.GothamBold
|
|
jaLabel.TextColor3 = C_SIGN_WHITE
|
|
jaLabel.TextScaled = true
|
|
jaLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
jaLabel.Text = ja
|
|
jaLabel.Parent = gui
|
|
|
|
-- Romaji reading (secondary, smaller, beneath)
|
|
local enLabel = Instance.new("TextLabel")
|
|
enLabel.Size = UDim2.new(1, -190, 0, 44)
|
|
enLabel.Position = UDim2.fromOffset(168, 132)
|
|
enLabel.BackgroundTransparency = 1
|
|
enLabel.Font = Enum.Font.Gotham
|
|
enLabel.TextColor3 = C_SIGN_WHITE
|
|
enLabel.TextScaled = true
|
|
enLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
enLabel.Text = romaji
|
|
enLabel.Parent = gui
|
|
end
|
|
|
|
-- PA junction planning -----------------------------------------------------------
|
|
-- Daikoku PA sits inside a loop of ramp road, and is reachable from BOTH
|
|
-- directions of travel. That means the median has to OPEN at the junctions,
|
|
-- otherwise the divider we just added would wall the PA off from the far
|
|
-- carriageway. buildRoad and buildPA must agree on exactly where those
|
|
-- openings are, so the plan is computed once, here, and shared.
|
|
local PAPlan = nil :: any
|
|
|
|
local function planPA()
|
|
if PAPlan then
|
|
return PAPlan
|
|
end
|
|
local samples = Circuit.samples
|
|
local c = Circuit.PA_CENTER
|
|
-- Nearest non-tunnel sample to the PA = where the ramps meet the mainline.
|
|
local bestIdx, bestD = 1, math.huge
|
|
for i, s in ipairs(samples) do
|
|
local d = (Vector3.new(s.pos.X, 0, s.pos.Z) - Vector3.new(c.X, 0, c.Z)).Magnitude
|
|
if not s.tunnel and d < bestD then
|
|
bestIdx, bestD = i, d
|
|
end
|
|
end
|
|
local n = #samples
|
|
|
|
-- TWO PERIMETER RAMPS, FAR SIDE SERVED BY THE MEDIAN OPENING (2026-08-03,
|
|
-- fourth pass, user drawing: yellow = PA -> highway, red = highway -> PA,
|
|
-- "this is why the middle barriers/dividers are important because each
|
|
-- side will have access to on and off ramps going to and from daikoku
|
|
-- futo pa").
|
|
--
|
|
-- What changed from the shared-trunk version:
|
|
--
|
|
-- 1. ENTRY AND EXIT SWAP SIDES. The drawing puts red (into the PA) on
|
|
-- the EAST and yellow (out of the PA) on the WEST. The previous pass
|
|
-- had it the other way round -- exit forked at bearing 19 deg (east),
|
|
-- entry at 149 deg (west).
|
|
--
|
|
-- 2. THE TRUNKS HUG THE PA PERIMETER. Previously the trunk snapped out
|
|
-- to its full radius over the first 22% of the sweep and then held a
|
|
-- constant circle. Now the radius eases across the WHOLE sweep, ring
|
|
-- gate -> fork, so each ramp spirals out from the apron edge and
|
|
-- flares to meet the mainline -- the shape in the drawing.
|
|
--
|
|
-- 3. THE UNDER-DECK CROSSING IS DELETED. This is the big one. The two
|
|
-- carriageways are at the SAME deck height and directly adjacent, so
|
|
-- the far carriageway does not need its own slip diving under the
|
|
-- expressway -- it just needs a gap in the centre divider. The
|
|
-- median opening IS the far-side access path. That removes the
|
|
-- `Cross` and `Merge` runs (8 built runs -> 4) and with them BOTH
|
|
-- outstanding defects, which lived entirely in that geometry:
|
|
-- PAExitRampNear -> PierColumn at seg 120
|
|
-- PAExitRampTrunk -> PierColumn + 6x PAMast
|
|
-- They stop existing rather than needing a fix.
|
|
--
|
|
-- The two ramps never overlap in bearing -- red spans 270..392 deg,
|
|
-- yellow spans 132..250 deg -- so no ramp-vs-ramp separation constraint
|
|
-- binds and no nested-circle radius ladder is needed.
|
|
--
|
|
-- Measured mainline geometry these numbers are fitted to (probe_ramp_geo,
|
|
-- ReplicatedStorage.Shared.Circuit, 823 samples):
|
|
-- PA_CENTER -4124, 46, -7470 PA_RADIUS 260 ring 390
|
|
-- PAFloor 828 x 828 -> X -4538..-3710 Z -7884..-7056
|
|
-- deck is FLAT Y=102 across the whole PA frontage (56 studs above apron)
|
|
-- nearest approach 330 studs at sample 540; usable band 533..551
|
|
-- bearing 0 = +X (east), 90 = +Z, 270 = -Z (the PA's south edge)
|
|
--
|
|
-- ORIENT flips the whole layout east<->west in one place. The drawing was
|
|
-- read as image-right = world east (+X, toward sample 550). If it turns
|
|
-- out mirrored, set ORIENT = -1 and rebuild -- nothing else changes.
|
|
local ORIENT = 1
|
|
local LANE_OFF = ROAD_WIDTH / 4 -- centre of one carriageway from the centreline
|
|
local APRON_Y = c.Y + 1
|
|
|
|
-- Point on carriageway `side` at sample i, plus that carriageway's actual
|
|
-- direction of travel (the far carriageway drives the other way).
|
|
local function carriagewayPoint(i, side)
|
|
local s = samples[i].pos
|
|
local nxt = samples[(i % n) + 1].pos
|
|
local h = math.atan2(nxt.Z - s.Z, nxt.X - s.X)
|
|
local nx, nz = -math.sin(h), math.cos(h)
|
|
return Vector3.new(s.X + nx * LANE_OFF * side, s.Y, s.Z + nz * LANE_OFF * side),
|
|
(side > 0) and h or (h + math.pi)
|
|
end
|
|
|
|
-- `k` is the offset from the PA-nearest sample (540), so these stay valid
|
|
-- if the circuit is resampled. `az` is the RING GATE bearing; the fork
|
|
-- lands at az + turn*sweep. Angles in degrees, lengths in studs.
|
|
--
|
|
-- Eastbound traffic (increasing sample index) meets yellow's merge at
|
|
-- 537 first, then red's divergence at 542 -- the left-then-right order in
|
|
-- the drawing.
|
|
local SHARED = {
|
|
{
|
|
name = "PAEntryRamp", -- RED: highway -> PA, east side
|
|
kind = "off",
|
|
k = 7, -- fork at sample 547, bearing 32 deg
|
|
forkR = 470.0, -- mainline is 533 out at that bearing: 63 in from it
|
|
az = 270.0, -- ring gate on the PA's south edge
|
|
turn = 1, -- CCW: 270 -> 392 (=32), down the east side
|
|
sweep = 122.0,
|
|
yFork = 96,
|
|
m = -5, -- diverges from the carriageway at sample 542
|
|
out = 0, -- unused: no under-deck crossing any more
|
|
nearSide = -1, -- PA is on the -Z side of the mainline here
|
|
},
|
|
{
|
|
name = "PAExitRamp", -- YELLOW: PA -> highway, west side
|
|
kind = "on",
|
|
k = -6, -- fork at sample 534, bearing 132 deg
|
|
forkR = 470.0,
|
|
az = 250.0, -- ring gate on the south edge, 135 studs west of red's
|
|
turn = -1, -- CW: 250 -> 132, up the west side
|
|
sweep = 118.0,
|
|
yFork = 96,
|
|
m = 3, -- merges onto the carriageway at sample 537, heading east
|
|
out = 0,
|
|
nearSide = -1,
|
|
},
|
|
}
|
|
for _, def in ipairs(SHARED) do
|
|
if ORIENT < 0 then
|
|
def.az = (180 - def.az) % 360
|
|
def.turn = -def.turn
|
|
def.k = -def.k
|
|
def.m = -def.m
|
|
end
|
|
end
|
|
|
|
local junctionDefs = {}
|
|
for _, def in ipairs(SHARED) do
|
|
local i0 = ((bestIdx - 1 + def.k) % n) + 1
|
|
local nearIdxM = ((i0 - 1 + def.m) % n) + 1
|
|
local nearPos, nearTravel = carriagewayPoint(nearIdxM, def.nearSide)
|
|
local farPos, farTravel = carriagewayPoint(nearIdxM, -def.nearSide)
|
|
-- Where the far slip emerges from under the deck, `out` studs beyond
|
|
-- the far carriageway edge, still at fork height.
|
|
local basePos = carriagewayPoint(i0, -def.nearSide)
|
|
local s0 = samples[i0].pos
|
|
local nx0 = samples[(i0 % n) + 1].pos
|
|
local h0 = math.atan2(nx0.Z - s0.Z, nx0.X - s0.X)
|
|
local nrm = Vector3.new(-math.sin(h0), 0, math.cos(h0))
|
|
local emerge = Vector3.new(basePos.X, def.yFork, basePos.Z) - nrm * (def.out * def.nearSide)
|
|
table.insert(junctionDefs, {
|
|
name = def.name,
|
|
idx = i0,
|
|
mergeIdx = nearIdxM,
|
|
kind = def.kind,
|
|
radius = def.forkR,
|
|
az = math.rad(def.az),
|
|
turn = def.turn,
|
|
sweep = math.rad(def.sweep),
|
|
yFork = def.yFork,
|
|
nearSide = def.nearSide,
|
|
-- `side` kept for the barrier/median code, which still thinks in
|
|
-- terms of one carriageway per junction; the near side is the one
|
|
-- whose parapet the trunk actually leaves through.
|
|
side = def.nearSide,
|
|
nearPos = nearPos,
|
|
nearTravel = nearTravel,
|
|
farPos = farPos,
|
|
farTravel = farTravel,
|
|
emerge = emerge,
|
|
-- The fork point, in world space. Everything downstream (slips,
|
|
-- gates) hangs off this.
|
|
fork = Vector3.new(
|
|
c.X + math.cos(math.rad(def.az) + def.turn * math.rad(def.sweep)) * def.forkR,
|
|
def.yFork,
|
|
c.Z + math.sin(math.rad(def.az) + def.turn * math.rad(def.sweep)) * def.forkR
|
|
),
|
|
pos = nearPos,
|
|
travel = nearTravel,
|
|
})
|
|
end
|
|
|
|
-- The median opening is now LOAD-BEARING, not cosmetic. It is the only
|
|
-- path from the far carriageway to the ramps, so it must be wide enough
|
|
-- to actually turn through at speed -- hence 9 samples either side rather
|
|
-- than 6. This is the "middle barriers/dividers are important" point in
|
|
-- the user's drawing.
|
|
local SPAN = 9 -- samples of median opening around each junction
|
|
local open = {}
|
|
for _, jd in ipairs(junctionDefs) do
|
|
for k = -SPAN, SPAN do
|
|
open[((jd.idx + k - 1) % n) + 1] = true
|
|
end
|
|
end
|
|
-- The OUTER edge barrier needs its own, much narrower opening -- and it is
|
|
-- NOT the same thing as the median opening.
|
|
--
|
|
-- BUG (2026-08-02): buildRoad reused `medianOpen` to skip the outer
|
|
-- barriers too, and did it inside `for side in {-1,1}`, so at all 13
|
|
-- samples per junction the mainline lost its parapet, railing AND kill
|
|
-- wall on BOTH edges -- 26 samples of expressway with no walls at all,
|
|
-- including the far side where no ramp exists. That is the "some of the
|
|
-- wangan has no walls, jumbled near the parking area" report.
|
|
--
|
|
-- `medianOpen` exists so both carriageways can reach the PA (the median is
|
|
-- the centre divider). The outer edge is a different structure with a drop
|
|
-- behind it, so it may only open on the ONE side the ramp leaves through,
|
|
-- and only right at the ramp mouth.
|
|
local RAMP_SPAN = 2 -- 5 samples, vs 13 for the median
|
|
local rampOpen = {}
|
|
-- Which SIDE of the road each opening is on. With four ramps the two
|
|
-- carriageways leave through opposite edges of the deck, so a single
|
|
-- "this sample is open" flag is no longer enough -- opening both edges
|
|
-- would strip the parapet off the far carriageway again (the 2026-08-02
|
|
-- "no walls near the parking area" bug, in a new form).
|
|
local rampOpenSide = {}
|
|
for _, jd in ipairs(junctionDefs) do
|
|
for k = -RAMP_SPAN, RAMP_SPAN do
|
|
local i = ((jd.idx + k - 1) % n) + 1
|
|
rampOpen[i] = true
|
|
rampOpenSide[i] = jd.side
|
|
end
|
|
end
|
|
PAPlan = {
|
|
nearIdx = bestIdx,
|
|
-- Kept for anything still referencing the old two-ramp names; they now
|
|
-- point at the forward carriageway's pair.
|
|
entryIdx = junctionDefs[2] and junctionDefs[2].idx or bestIdx,
|
|
exitIdx = junctionDefs[1] and junctionDefs[1].idx or bestIdx,
|
|
junctionDefs = junctionDefs,
|
|
medianOpen = open,
|
|
rampOpen = rampOpen,
|
|
rampOpenSide = rampOpenSide,
|
|
ringRadius = Circuit.PA_RADIUS + 130,
|
|
}
|
|
return PAPlan
|
|
end
|
|
|
|
-- The expressway itself ---------------------------------------------------------
|
|
local function buildRoad(map: Folder)
|
|
local road = Instance.new("Folder")
|
|
road.Name = "Expressway"
|
|
road.Parent = map
|
|
local struct = Instance.new("Folder")
|
|
struct.Name = "Structure"
|
|
struct.Parent = map
|
|
|
|
local samples = Circuit.samples
|
|
local n = #samples
|
|
local plan = planPA()
|
|
|
|
-- Running arc length around the loop, so lane dashes keep a continuous
|
|
-- 20 m rhythm instead of restarting at every sample.
|
|
local segStart = 0
|
|
|
|
for i = 1, n do
|
|
local a = samples[i]
|
|
local b = samples[(i % n) + 1]
|
|
local delta = b.pos - a.pos
|
|
if delta.Magnitude < 0.5 then
|
|
continue
|
|
end
|
|
local len = delta.Magnitude + 2
|
|
local mid = (a.pos + b.pos) / 2
|
|
local dir = delta.Unit
|
|
local flatDir = Vector3.new(dir.X, 0, dir.Z)
|
|
flatDir = flatDir.Magnitude > 0.01 and flatDir.Unit or Vector3.xAxis
|
|
local right = flatDir:Cross(UP)
|
|
local cf = CFrame.lookAt(mid, mid + dir, UP)
|
|
|
|
-- How hard this segment turns, used to decide where the real road
|
|
-- would forbid lane changes (solid yellow) rather than allow them.
|
|
local prev = samples[((i - 2) % n) + 1]
|
|
local inDir = (a.pos - prev.pos)
|
|
inDir = Vector3.new(inDir.X, 0, inDir.Z)
|
|
local curvature = 0
|
|
if inDir.Magnitude > 0.01 then
|
|
curvature = math.acos(math.clamp(inDir.Unit:Dot(flatDir), -1, 1))
|
|
end
|
|
|
|
local deck = part({
|
|
Name = "Road_" .. i,
|
|
Size = Vector3.new(widthAt(i), 2, len),
|
|
CFrame = cf,
|
|
Color = C_ASPHALT,
|
|
Material = Enum.Material.Asphalt,
|
|
})
|
|
deck.Parent = road
|
|
|
|
-- CENTRAL DIVIDER ---------------------------------------------------
|
|
-- Real Wangan has a solid median, not a painted line: two lanes each
|
|
-- way, physically separated. This is collidable, so a head-on into
|
|
-- oncoming traffic isn't possible -- you hit the divider instead.
|
|
local HALF = widthAt(i) / 2
|
|
local MED_HALF = MEDIAN_WIDTH / 2
|
|
-- ...except at the PA junctions, where it opens so both directions of
|
|
-- travel can cross into the parking area (see planPA).
|
|
if not plan.medianOpen[i] then
|
|
local median = part({
|
|
Name = "Median_" .. i,
|
|
Size = Vector3.new(MEDIAN_WIDTH, MEDIAN_HEIGHT, len),
|
|
CFrame = cf * CFrame.new(0, 1 + MEDIAN_HEIGHT / 2, 0),
|
|
Color = C_PARAPET,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
median.Parent = struct
|
|
-- Dark steel anti-glare fence along the median -- on the real road
|
|
-- this strip of vertical blades is what separates the directions.
|
|
local fence = part({
|
|
Name = "MedianFence",
|
|
Size = Vector3.new(0.8, 2.6, len),
|
|
CFrame = cf * CFrame.new(0, 1 + MEDIAN_HEIGHT + 1.3, 0),
|
|
Color = C_RAIL,
|
|
Material = Enum.Material.Metal,
|
|
})
|
|
fence.Parent = struct
|
|
end
|
|
|
|
-- LANE MARKINGS -----------------------------------------------------
|
|
-- Lane count varies by section (see SECTION PROFILE above): most of the
|
|
-- C1 is 2+2, but 汐留JCT-浜崎橋JCT is 3+3. A 3-lane carriageway needs
|
|
-- TWO boundary lines, not one, so the markings are generated per lane
|
|
-- boundary rather than assuming a single centre dash.
|
|
--
|
|
-- Dashes are driven by ARC LENGTH along the circuit, not by sample
|
|
-- index. That matters: samples are 60 studs (16.8 m) apart but the
|
|
-- legal dash cycle is 20 m, so an index-based dash can never land on
|
|
-- the right rhythm -- it has to be computed in metres and clipped to
|
|
-- each segment.
|
|
local nLanes = lanesAt(i)
|
|
-- Lane changes are prohibited (yellow line) through the tunnels, on the
|
|
-- tight curves, and through Ginza -- where 跨道橋 piers stand between
|
|
-- the lanes, which is exactly why the real road forbids it there.
|
|
local noLaneChange = a.tunnel or curvature > TIGHT_CURVE or GINZA_ZONES[a.piece] == true
|
|
local laneColor = noLaneChange and C_PAINT_YELLOW or C_PAINT_WHITE
|
|
|
|
for _, s in ipairs({ -1, 1 }) do
|
|
-- 車道外側線: solid, continuous, 0.20 m wide.
|
|
local edge = part({
|
|
Name = "EdgeLine",
|
|
Size = Vector3.new(EDGE_LINE_W, MARK_H, len),
|
|
CFrame = cf * CFrame.new(s * (HALF - 1.6), MARK_Y, 0),
|
|
Color = C_PAINT_WHITE,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
edge.Parent = road
|
|
|
|
-- 車線境界線: one per boundary between adjacent lanes.
|
|
for lane = 1, nLanes - 1 do
|
|
local cwMid = MED_HALF + LANE_W * lane
|
|
if laneColor == C_PAINT_YELLOW then
|
|
-- Yellow no-lane-change line is SOLID, not dashed.
|
|
local solid = part({
|
|
Name = "LaneLine",
|
|
Size = Vector3.new(LANE_LINE_W, MARK_H, len),
|
|
CFrame = cf * CFrame.new(s * cwMid, MARK_Y, 0),
|
|
Color = C_PAINT_YELLOW,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
solid.Parent = road
|
|
else
|
|
-- Dashed: paint the parts of the 20 m cycle that fall inside
|
|
-- this segment. segStart is the distance travelled around the
|
|
-- loop, so the rhythm stays continuous across segment joins.
|
|
local segLen = delta.Magnitude
|
|
local k = math.floor(segStart / DASH_CYCLE)
|
|
while k * DASH_CYCLE < segStart + segLen do
|
|
local pStart = math.max(k * DASH_CYCLE, segStart)
|
|
local pEnd = math.min(k * DASH_CYCLE + DASH_PAINT, segStart + segLen)
|
|
if pEnd > pStart then
|
|
-- Convert back to an offset from the segment centre.
|
|
local mid01 = ((pStart + pEnd) / 2 - segStart) / segLen
|
|
local dash = part({
|
|
Name = "Dash",
|
|
Size = Vector3.new(LANE_LINE_W, MARK_H, pEnd - pStart),
|
|
CFrame = cf * CFrame.new(s * cwMid, MARK_Y, (mid01 - 0.5) * segLen),
|
|
Color = C_PAINT_WHITE,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
dash.Parent = road
|
|
end
|
|
k += 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- OUTER EDGE BARRIERS -----------------------------------------------
|
|
-- Visible concrete Jersey barrier sitting ON the deck (this is the part
|
|
-- you actually see), then an invisible taller wall just outside it as a
|
|
-- backstop against tunnelling at high speed.
|
|
-- At a PA junction the barrier AND the kill wall must open on the side
|
|
-- the ramp leaves through, or the ramp would dead-end into a wall.
|
|
--
|
|
-- FIXED (2026-08-02): this used `plan.medianOpen[i]` and `continue`d
|
|
-- for BOTH sides, stripping barrier + rail + kill wall from 26 samples
|
|
-- of mainline on both edges. Two corrections:
|
|
-- * use `rampOpen` (5 samples) not `medianOpen` (13) -- the median
|
|
-- opening is a different, wider thing for a different purpose.
|
|
-- * only skip the ONE side the PA is actually on. The far edge of
|
|
-- the expressway has a drop behind it and must never open.
|
|
--
|
|
-- UPDATED (2026-08-03) for four ramps. "Which side is the PA on" is no
|
|
-- longer the right question: both carriageways now have their own
|
|
-- ramps, and the opposite carriageway's ramp leaves through the edge
|
|
-- FACING AWAY from the PA before spiralling round to it. Asking about
|
|
-- the PA direction would open the wrong edge there and wall that ramp
|
|
-- off. planPA already knows which edge each junction uses, so read it
|
|
-- rather than re-deriving it from geometry that no longer implies it.
|
|
local junction = plan.rampOpen[i]
|
|
local paSide = junction and (plan.rampOpenSide[i] or 0) or 0
|
|
for _, side in ipairs({ -1, 1 }) do
|
|
if junction and side == paSide then
|
|
continue
|
|
end
|
|
local boff = right * (side * (HALF - BARRIER_T / 2))
|
|
local bcf = CFrame.lookAt(mid + boff, mid + boff + dir, UP)
|
|
-- White concrete parapet...
|
|
local barrier = part({
|
|
Name = "Barrier",
|
|
Size = Vector3.new(BARRIER_T, BARRIER_H, len),
|
|
CFrame = bcf * CFrame.new(0, 1 + BARRIER_H / 2, 0),
|
|
Color = C_PARAPET,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
barrier.Parent = struct
|
|
-- ...topped by the dark metal railing. Every photo of the Shutoko
|
|
-- shows this two-tone parapet+railing edge; without the railing the
|
|
-- road reads as a generic slab instead of a Japanese expressway.
|
|
local rail = part({
|
|
Name = "EdgeRail",
|
|
Size = Vector3.new(0.6, RAIL_H, len),
|
|
CFrame = bcf * CFrame.new(0, 1 + BARRIER_H + RAIL_H / 2, 0),
|
|
Color = C_RAIL,
|
|
Material = Enum.Material.Metal,
|
|
})
|
|
rail.Parent = struct
|
|
|
|
local woff = right * (side * (HALF + 1))
|
|
local wall = part({
|
|
Name = "KillWall",
|
|
Size = Vector3.new(2, WALL_HEIGHT, len + 6),
|
|
CFrame = CFrame.lookAt(mid + woff, mid + woff + dir, UP) * CFrame.new(0, WALL_HEIGHT / 2, 0),
|
|
Transparency = 1,
|
|
CanCollide = true,
|
|
})
|
|
wall.Parent = struct
|
|
end
|
|
|
|
if a.tunnel then
|
|
-- Yamate/C1-style bore: walls, ceiling, strip lighting
|
|
local h = 24
|
|
for _, side in ipairs({ -1, 1 }) do
|
|
local woff = right * (side * (HALF + 1.5))
|
|
local w = part({
|
|
Name = "TunnelWall",
|
|
Size = Vector3.new(3, h, len + 2),
|
|
CFrame = CFrame.lookAt(mid + woff, mid + woff + dir, UP) * CFrame.new(0, h / 2, 0),
|
|
Color = Color3.fromRGB(38, 38, 46),
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
w.Parent = struct
|
|
end
|
|
local ceil = part({
|
|
Name = "TunnelCeiling",
|
|
Size = Vector3.new(widthAt(i) + 6, 2, len + 2),
|
|
CFrame = cf * CFrame.new(0, h + 1, 0),
|
|
Color = Color3.fromRGB(30, 30, 38),
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
ceil.Parent = struct
|
|
-- Tunnel lighting is high-pressure sodium (warm amber) with newer
|
|
-- LED sections in cool white. Real tunnel "basic lighting" is a
|
|
-- continuous run, not isolated dots, so the strip is placed on
|
|
-- every segment rather than every other one.
|
|
local strip = part({
|
|
Name = "TunnelLight",
|
|
Size = Vector3.new(widthAt(i) * 0.5, 0.6, len),
|
|
CFrame = cf * CFrame.new(0, h - 0.5, 0),
|
|
Color = (i % 8 < 4) and C_SODIUM or C_TUNNEL_LED,
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
strip.Parent = struct
|
|
else
|
|
-- BRIDGE / VIADUCT SUPERSTRUCTURE ------------------------------
|
|
-- Circuit.luau flags 474 samples as bridge=1, but nothing was ever
|
|
-- reading that flag -- every elevated span rendered as the same
|
|
-- plain slab on square columns. On the real C1 the flagged
|
|
-- sections are steel box-girder spans (鋼箱桁橋) over the old
|
|
-- river/canal alignments: a deep girder under the deck, sitting on
|
|
-- a transverse pier cap carried by round columns on a wider pitch.
|
|
if a.bridge then
|
|
-- Deep girder immediately under the deck -- this is the single
|
|
-- most recognisable feature of a Shutoko viaduct in profile.
|
|
local girder = part({
|
|
Name = "BridgeGirder",
|
|
Size = Vector3.new(widthAt(i) * 0.72, GIRDER_H, len),
|
|
CFrame = cf * CFrame.new(0, -1 - GIRDER_H / 2, 0),
|
|
Color = C_GIRDER,
|
|
Material = Enum.Material.Metal,
|
|
})
|
|
girder.Parent = struct
|
|
-- Girders are fabricated in segments; the transverse stiffener
|
|
-- ribs between them are what give the underside its rhythm.
|
|
local rib = part({
|
|
Name = "GirderRib",
|
|
Size = Vector3.new(widthAt(i) * 0.78, GIRDER_H * 0.82, 1.2),
|
|
CFrame = cf * CFrame.new(0, -1 - GIRDER_H / 2, len / 2),
|
|
Color = C_GIRDER,
|
|
Material = Enum.Material.Metal,
|
|
})
|
|
rib.Parent = struct
|
|
|
|
-- Piers on a wider pitch than the viaduct columns, with the
|
|
-- pier cap (橋脚梁) spanning the full deck width.
|
|
if i % 6 == 0 and a.pos.Y > 14 then
|
|
local capY = a.pos.Y - 1 - GIRDER_H - 1.6
|
|
local cap = part({
|
|
Name = "PierCap",
|
|
Size = Vector3.new(widthAt(i) * 0.92, 3.2, 7),
|
|
CFrame = CFrame.new(mid.X, capY, mid.Z) * CFrame.Angles(0, math.atan2(flatDir.X, flatDir.Z), 0),
|
|
Color = C_CONCRETE,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
cap.Parent = struct
|
|
local colH = capY - 1.6
|
|
local col = part({
|
|
Name = "PierColumn",
|
|
-- Enum.PartType.Cylinder puts the circular faces on the
|
|
-- LOCAL X axis: Size.X is the length ALONG the axis and
|
|
-- Size.Y/Z are the diameter.
|
|
--
|
|
-- BUG (found 2026-08-03 by measuring the built parts):
|
|
-- this passed Size = (8, colH, 8), i.e. 8 studs along the
|
|
-- axis by colH wide -- a disc, not a column. Measured on
|
|
-- all 80 instances: world vertical extent 8.0 studs for a
|
|
-- column that should stand 35.4, leaving each pier
|
|
-- floating with a 13.7-stud gap down to the ground.
|
|
-- The Z rotation below was already correct; only the Size
|
|
-- components were transposed.
|
|
Size = Vector3.new(colH, 8, 8),
|
|
Color = C_CONCRETE,
|
|
Material = Enum.Material.Concrete,
|
|
Shape = Enum.PartType.Cylinder,
|
|
})
|
|
-- Cylinder's axis is X, so stand it upright.
|
|
col.CFrame = CFrame.new(mid.X, colH / 2, mid.Z) * CFrame.Angles(0, 0, math.rad(90))
|
|
col.Parent = struct
|
|
end
|
|
-- Elevated viaduct: support pillars down to street level
|
|
elseif i % 3 == 0 and a.pos.Y > 18 then
|
|
local pil = part({
|
|
Name = "Pillar",
|
|
Size = Vector3.new(9, a.pos.Y, 9),
|
|
CFrame = CFrame.new(mid.X, a.pos.Y / 2 - 1, mid.Z),
|
|
Color = C_CONCRETE,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
pil.Parent = struct
|
|
end
|
|
-- Sodium lamps along the deck. Japanese expressway lighting is
|
|
-- warm high-pressure sodium on a ~35 m pole pitch; samples are
|
|
-- 60 studs (16.8 m) apart so every 2nd sample ~= 33.6 m.
|
|
if i % 2 == 0 then
|
|
local lamp = part({
|
|
Name = "RoadLight",
|
|
Size = Vector3.new(1.4, 1.4, 1.4),
|
|
CFrame = CFrame.new(mid + right * HALF + Vector3.new(0, 5.5, 0)),
|
|
Color = C_SODIUM,
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
lamp.Parent = struct
|
|
end
|
|
-- Delineators (視線誘導標): small reflective posts on the outer
|
|
-- edge that mark the road's line ahead at night. Real spacing on
|
|
-- curves is tight; ~17 m here.
|
|
local delin = part({
|
|
Name = "Delineator",
|
|
Size = Vector3.new(0.5, 1.1, 0.5),
|
|
CFrame = CFrame.new(mid + right * (HALF - 2.6) + Vector3.new(0, 2.5, 0)),
|
|
Color = C_PAINT_WHITE,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
delin.Parent = struct
|
|
local eye = part({
|
|
Name = "DelineatorEye",
|
|
Size = Vector3.new(0.36, 0.36, 0.36),
|
|
CFrame = CFrame.new(mid + right * (HALF - 2.6) + Vector3.new(0, 3.0, 0)),
|
|
Color = Color3.fromRGB(255, 190, 80),
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
eye.Parent = struct
|
|
if i % 9 == 0 then
|
|
local pylon = part({
|
|
Name = "Pylon",
|
|
Size = Vector3.new(1, 14, 1),
|
|
CFrame = CFrame.new(mid - right * (HALF + 3) + Vector3.new(0, 7, 0)),
|
|
Color = (i % 18 == 0) and NEON_PINK or NEON_BLUE,
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
pylon.Parent = struct
|
|
end
|
|
end
|
|
|
|
-- GINZA 跨道橋 (bridges crossing OVER the expressway) ----------------
|
|
-- The Ginza section runs in the filled-in 楓川・築地川 river beds, so it
|
|
-- sits BELOW street level and the surviving city bridges cross OVER it.
|
|
-- Their piers land in the median, physically between the carriageways --
|
|
-- which is the real reason lane changes are banned here (the yellow
|
|
-- line above is driven by GINZA_ZONES for exactly this reason).
|
|
-- Placed outside the tunnel/viaduct branch: Ginza has 0 bridge-flagged
|
|
-- samples, so nesting this under `a.bridge` would never render it.
|
|
if GINZA_ZONES[a.piece] and i % 7 == 0 then
|
|
local deckY = a.pos.Y + 26
|
|
local yaw = math.atan2(flatDir.X, flatDir.Z)
|
|
local ov = part({
|
|
Name = "GinzaOverpass",
|
|
Size = Vector3.new(widthAt(i) + 70, 3.4, 26),
|
|
CFrame = CFrame.new(mid.X, deckY, mid.Z) * CFrame.Angles(0, yaw, 0),
|
|
Color = C_CONCRETE,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
ov.Parent = struct
|
|
for _, es in ipairs({ -1, 1 }) do
|
|
local par = part({
|
|
Name = "OverpassParapet",
|
|
Size = Vector3.new(widthAt(i) + 70, 3.6, 1.4),
|
|
CFrame = CFrame.new(mid.X, deckY + 3.4, mid.Z)
|
|
* CFrame.Angles(0, yaw, 0)
|
|
* CFrame.new(0, 0, es * 12.3),
|
|
Color = C_PARAPET,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
par.Parent = struct
|
|
end
|
|
-- Centre pier, standing in the median between the directions.
|
|
local pierH = deckY - a.pos.Y - 1.7
|
|
local pier = part({
|
|
Name = "OverpassPier",
|
|
Size = Vector3.new(4.2, pierH, 9),
|
|
CFrame = CFrame.new(mid.X, a.pos.Y + pierH / 2 + 1, mid.Z) * CFrame.Angles(0, yaw, 0),
|
|
Color = C_CONCRETE,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
pier.Parent = struct
|
|
end
|
|
|
|
-- 導流帯 / gore zebra at the PA ramp junctions --------------------
|
|
-- Where the ramp splits off the mainline the real road paints a
|
|
-- zebra-hatched no-drive wedge in the gore. 45 cm bars every 1.0 m
|
|
-- per 全標協 2018 pp.97-98.
|
|
if plan.medianOpen[i] then
|
|
local nBars = math.floor(delta.Magnitude / ZEBRA_PITCH)
|
|
for k = 0, nBars - 1 do
|
|
local z = (k * ZEBRA_PITCH - delta.Magnitude / 2) + ZEBRA_PITCH / 2
|
|
for _, s in ipairs({ -1, 1 }) do
|
|
local bar = part({
|
|
Name = "GoreZebra",
|
|
Size = Vector3.new(6, MARK_H, ZEBRA_W),
|
|
CFrame = cf * CFrame.new(s * (HALF - 5), MARK_Y, z),
|
|
Color = C_PAINT_WHITE,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
bar.Parent = road
|
|
end
|
|
end
|
|
-- 45 cm boundary line separating the through lane from the
|
|
-- speed-change lane, per 全標協 2012 p.42.
|
|
for _, s in ipairs({ -1, 1 }) do
|
|
local rl = part({
|
|
Name = "RampBoundary",
|
|
Size = Vector3.new(RAMP_LINE_W, MARK_H, len),
|
|
CFrame = cf * CFrame.new(s * (HALF - 8.5), MARK_Y, 0),
|
|
Color = C_PAINT_WHITE,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
rl.Parent = road
|
|
end
|
|
end
|
|
|
|
-- Advance the running arc length so the next segment's dashes
|
|
-- continue the 20 m rhythm instead of restarting.
|
|
segStart += delta.Magnitude
|
|
end
|
|
end
|
|
|
|
-- Regulatory speed limit sign (規制標識 323 最高速度) -----------------------------
|
|
-- Circular, white face, red annulus, black numerals. The real C1 is posted at
|
|
-- 50 km/h (40 in the tightest junction sections) -- verified against the route
|
|
-- article's own signage table, not the 80 km/h used on the Bayshore Route.
|
|
local function makeSpeedSign(kmh: number, cf: CFrame, parent: Instance)
|
|
local post = part({
|
|
Name = "SignPost",
|
|
Size = Vector3.new(0.6, 12, 0.6),
|
|
CFrame = cf * CFrame.new(0, -6, 0),
|
|
Color = C_RAIL,
|
|
Material = Enum.Material.Metal,
|
|
})
|
|
post.Parent = parent
|
|
|
|
local face = part({
|
|
Name = "SpeedLimitSign",
|
|
Size = Vector3.new(6, 6, 0.4),
|
|
CFrame = cf,
|
|
Color = Color3.fromRGB(255, 255, 255),
|
|
Material = Enum.Material.SmoothPlastic,
|
|
Shape = Enum.PartType.Cylinder,
|
|
})
|
|
-- Cylinder's flat faces point along X, so rotate it to face the driver.
|
|
face.CFrame = cf * CFrame.Angles(0, math.rad(90), 0)
|
|
face.Parent = parent
|
|
|
|
for _, faceId in ipairs({ Enum.NormalId.Right, Enum.NormalId.Left }) do
|
|
local gui = Instance.new("SurfaceGui")
|
|
gui.Face = faceId
|
|
gui.CanvasSize = Vector2.new(256, 256)
|
|
gui.Parent = face
|
|
|
|
local disc = Instance.new("Frame")
|
|
disc.Size = UDim2.fromScale(1, 1)
|
|
disc.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
|
|
disc.BorderSizePixel = 0
|
|
disc.Parent = gui
|
|
local dc = Instance.new("UICorner")
|
|
dc.CornerRadius = UDim.new(1, 0)
|
|
dc.Parent = disc
|
|
-- Red annulus is ~11% of the diameter on the JP plate.
|
|
local ring = Instance.new("UIStroke")
|
|
ring.Color = Color3.fromRGB(200, 30, 45)
|
|
ring.Thickness = 28
|
|
ring.Parent = disc
|
|
|
|
local num = Instance.new("TextLabel")
|
|
num.Size = UDim2.fromScale(0.72, 0.56)
|
|
num.Position = UDim2.fromScale(0.14, 0.22)
|
|
num.BackgroundTransparency = 1
|
|
num.Font = Enum.Font.GothamBold
|
|
num.TextColor3 = Color3.fromRGB(20, 20, 20)
|
|
num.TextScaled = true
|
|
num.Text = tostring(kmh)
|
|
num.Parent = disc
|
|
end
|
|
end
|
|
|
|
-- Kilometre post (キロポスト) ----------------------------------------------------
|
|
local function makeKmPost(km: number, cf: CFrame, parent: Instance)
|
|
local board = part({
|
|
Name = "KmPost",
|
|
Size = Vector3.new(2.6, 3.2, 0.3),
|
|
CFrame = cf,
|
|
Color = C_SIGN_GREEN,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
board.Parent = parent
|
|
local gui = Instance.new("SurfaceGui")
|
|
gui.Face = Enum.NormalId.Front
|
|
gui.CanvasSize = Vector2.new(130, 160)
|
|
gui.Parent = board
|
|
local label = Instance.new("TextLabel")
|
|
label.Size = UDim2.fromScale(1, 1)
|
|
label.BackgroundTransparency = 1
|
|
label.Font = Enum.Font.GothamBold
|
|
label.TextColor3 = C_SIGN_WHITE
|
|
label.TextScaled = true
|
|
label.Text = string.format("%.1f", km)
|
|
label.Parent = gui
|
|
end
|
|
|
|
-- Junction gantry signs at each real interchange ---------------------------------
|
|
local function buildJunctionSigns(map: Folder)
|
|
local folder = Instance.new("Folder")
|
|
folder.Name = "Signs"
|
|
folder.Parent = map
|
|
|
|
local samples = Circuit.samples
|
|
local n = #samples
|
|
local seen: { [string]: boolean } = {}
|
|
for i = 1, n do
|
|
local s = samples[i]
|
|
if s.tunnel or seen[s.piece] then
|
|
continue
|
|
end
|
|
seen[s.piece] = true
|
|
local fwd = Circuit.forwardAt(i)
|
|
local pos = s.pos + Vector3.new(0, 15, 0)
|
|
makeSign(s.piece, CFrame.lookAt(pos, pos - fwd, UP), folder)
|
|
end
|
|
|
|
-- Speed limit repeaters and kilometre posts around the loop.
|
|
local dist = 0
|
|
local nextKm = 0
|
|
for i = 1, n do
|
|
local a = samples[i]
|
|
local b = samples[(i % n) + 1]
|
|
local seg = (b.pos - a.pos).Magnitude
|
|
local fwd = Circuit.forwardAt(i)
|
|
local flat = Vector3.new(fwd.X, 0, fwd.Z)
|
|
flat = flat.Magnitude > 0.01 and flat.Unit or Vector3.xAxis
|
|
local right = flat:Cross(UP)
|
|
|
|
-- Posted speed: 50 km/h on the mainline, but 40 km/h inside the
|
|
-- junction sections (江戸橋JCT内, 浜崎橋JCT内, 京橋JCT-旧白魚橋乗継所
|
|
-- are all posted 40 in the route's own 車線・最高速度 table). Signs
|
|
-- repeat roughly every 1 km; junction plates go up more often because
|
|
-- the restricted stretches are short.
|
|
local jct = isJctSample(i)
|
|
local every = jct and 24 or 60
|
|
if i % every == 0 and not a.tunnel then
|
|
local p = a.pos + right * (widthAt(i) / 2 - 3) + Vector3.new(0, 9, 0)
|
|
makeSpeedSign(jct and 40 or 50, CFrame.lookAt(p, p - flat, UP), folder)
|
|
end
|
|
|
|
-- Kilometre posts every 0.5 km of real distance (0.5 km = 1785 studs).
|
|
if dist >= nextKm * 500 * M then
|
|
local p = a.pos + right * (widthAt(i) / 2 - 2) + Vector3.new(0, 4, 0)
|
|
makeKmPost(nextKm * 0.5, CFrame.lookAt(p, p - flat, UP), folder)
|
|
nextKm += 1
|
|
end
|
|
dist += seg
|
|
end
|
|
end
|
|
|
|
-- Daikoku-style PA ---------------------------------------------------------------
|
|
local function buildPA(map: Folder)
|
|
local pa = Instance.new("Folder")
|
|
pa.Name = "ParkingArea"
|
|
pa.Parent = map
|
|
|
|
local c = Circuit.PA_CENTER
|
|
local R = Circuit.PA_RADIUS
|
|
|
|
-- Real Daikoku Futo PA: the parking apron is a huge flat lot, and the loop
|
|
-- ramps sweep AROUND and OVER it on pillars (see the aerial reference) --
|
|
-- the parking is inside the ring, at deck level, with the ring elevated
|
|
-- above it. The apron reaches out under the ring so there is no void gap.
|
|
local APRON = Circuit.PA_RADIUS + 130 + 24 -- ringRadius + half ring width
|
|
local slab = part({
|
|
Name = "PAFloor",
|
|
Size = Vector3.new(APRON * 2, 2, APRON * 2),
|
|
CFrame = CFrame.new(c),
|
|
Color = C_ASPHALT,
|
|
Material = Enum.Material.Asphalt,
|
|
})
|
|
slab.Parent = pa
|
|
|
|
-- REAL DAIKOKU FUTO PA LAYOUT ---------------------------------------------
|
|
-- Footprint measured from OpenStreetMap way 59608205 (大黒ふ頭パーキングエリア):
|
|
-- the apron is 181 m x 174 m (1.6 ha) -- a big, essentially SQUARE lot, not
|
|
-- a circular one. The spiral ramps sweep around and over it, which is the
|
|
-- "螺旋状の道路" the PA is famous for as a photo backdrop.
|
|
--
|
|
-- Capacity from ja.wikipedia 大黒パーキングエリア (首都高速で最大のPA, opened
|
|
-- 1989-09-27): 小型 341 + 大型 59 + 身障者用 4 = 404 spaces. Those three
|
|
-- counts are reproduced EXACTLY below rather than approximated, because the
|
|
-- lot's character is the sheer amount of parking.
|
|
--
|
|
-- Stall dimensions (小型 2.5 x 5.0 m, 大型 3.3 x 12 m, 身障者用 3.5 x 5.0 m,
|
|
-- 6 m aisles) are conventional Japanese parking-design values -- NOT read
|
|
-- from a primary standard, unlike the road markings. Paint is 15 cm, which
|
|
-- does match the 全標協 line width used on the mainline.
|
|
-- APRON EXTENT. Previously 181 x 174 m, which was the bounding box of the
|
|
-- OSM parking polygon alone. That polygon covers only the STALLS -- the
|
|
-- buildings, terrace and fountain sit alongside it and are separate OSM
|
|
-- ways, so a slab sized to the stalls left 本館, フォンターナ and the Lawson
|
|
-- hanging over the edge (measured: 4 of 12 features off-slab).
|
|
--
|
|
-- Sized here to the FULL surveyed site instead. After the +32 m north shift
|
|
-- and 0.88 uniform scale applied to the survey (see the building block),
|
|
-- the outermost features reach ~100 m radius, so a 210 x 200 m slab clears
|
|
-- everything with a margin for circulation. Still inside the ring road,
|
|
-- whose clear inner radius is 103.0 m.
|
|
local APRON_X = 210 * M
|
|
local APRON_Z = 200 * M
|
|
local STALL_W = 2.5 * M
|
|
local STALL_D = 5.0 * M
|
|
local BIG_W = 3.3 * M
|
|
local BIG_D = 12.0 * M
|
|
local ACC_W = 3.5 * M
|
|
local AISLE = 6.0 * M
|
|
local LINE_W = 0.15 * M
|
|
local DECK_Y = c.Y + 1 -- top face of the apron slab
|
|
local C_PAINT = Color3.fromRGB(235, 235, 240)
|
|
|
|
-- Painted markings are PAINT, not kerbs. They must be flush with the apron.
|
|
--
|
|
-- BUG (2026-08-02, test drive): these were spawned 2.2 studs tall sitting
|
|
-- at DECK_Y + 0.1, so every stall divider stood ~1.2 studs PROUD of the
|
|
-- tarmac -- a kerb-height ridge exactly where you park. Driving the rows
|
|
-- meant bouncing over 300+ concrete strips ("like a whole foot out of the
|
|
-- ground" -- measured 1.2, and a stud is ~a foot, so: exactly right).
|
|
--
|
|
-- Fix: 0.12 studs thick, sunk so its TOP sits a hair above the deck. A
|
|
-- decal would be flatter still, but a thin slab keeps this buildable from
|
|
-- pure geometry with no asset IDs, and 0.06 studs proud is under tyre
|
|
-- resolution -- you cannot feel it.
|
|
local PAINT_T = 0.12
|
|
|
|
-- STALL PAINT DISABLED (2026-08-02, user call): the generated stall
|
|
-- markings are being replaced with pre-made parking assets from the
|
|
-- Creator Store. Set this back to true to restore the painted lines.
|
|
--
|
|
-- Deliberately a FLAG rather than deleting markRow and its callers: the
|
|
-- row placement below is surveyed against OSM (二番館 frontage, the 大型
|
|
-- rows, the 身障者用 bays) and feeds PACapacity, which the headless
|
|
-- verifier asserts. Ripping it out would throw away that survey work and
|
|
-- silently change the recorded capacity. With the flag off, every stall
|
|
-- position is still computed -- they just emit no geometry.
|
|
local DRAW_STALL_PAINT = false
|
|
|
|
local function paint(name, size, x, z, colour)
|
|
if not DRAW_STALL_PAINT then
|
|
return nil
|
|
end
|
|
local m = part({
|
|
Name = name,
|
|
-- caller passes the old 2.2 in size.Y; ignore it and use PAINT_T
|
|
Size = Vector3.new(size.X, PAINT_T, size.Z),
|
|
CFrame = CFrame.new(x, DECK_Y + PAINT_T / 2 - 0.06, z),
|
|
Color = colour or C_PAINT,
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
m.Parent = pa
|
|
return m
|
|
end
|
|
|
|
-- One row of stalls: n+1 divider lines perpendicular to the aisle, plus the
|
|
-- head line the cars nose up to. `sign` is the direction the stalls extend
|
|
-- away from the aisle edge.
|
|
local function markRow(name, x0, zEdge, n, w, d, sign)
|
|
local zMid = zEdge + sign * d / 2
|
|
for k = 0, n do
|
|
paint(name .. "Line", Vector3.new(LINE_W, 2.2, d), x0 + k * w, zMid)
|
|
end
|
|
paint(name .. "Head", Vector3.new(n * w, 2.2, LINE_W), x0 + n * w / 2, zEdge)
|
|
end
|
|
|
|
-- 小型 341: eleven rows of 31, stacked as back-to-back pairs with 6 m aisles.
|
|
-- Z origin is derived, not eyeballed: the block starts one stall-depth clear
|
|
-- of the フォンターナ frontage and must finish north of the 大型 rows. Placing
|
|
-- it by hand overlapped the truck bays by 42 studs -- see verify_daikoku_pa.
|
|
--
|
|
-- 2026-08-02: this used to hang off the old (wrong) north-edge 二番館
|
|
-- position. The building is now surveyed to OSM way 88790141, centre
|
|
-- (+13.9 E, +123.1 N) m with a 43 m long axis, so its southern frontage is
|
|
-- at 123.1 - 43/2 = 101.6 m north in OSM coords. That must go through the
|
|
-- SAME shift+scale as the buildings themselves (OSM_SHIFT_N = 32 m,
|
|
-- OSM_SCALE = 0.88, both defined with the building block below) or the
|
|
-- stalls detach from the building they are supposed to line up with.
|
|
local SMALL_PER_ROW = 31
|
|
local SMALL_ROWS = 11 -- 31 x 11 = 341 exactly
|
|
local smallX0 = c.X - 20 * M
|
|
local pairPitch = 2 * STALL_D + AISLE
|
|
local nibanFace = c.Z - (123.1 - 43 / 2 - 32.0) * 0.88 * M
|
|
local smallZ0 = nibanFace + STALL_D + 8
|
|
local placedSmall = 0
|
|
for row = 0, SMALL_ROWS - 1 do
|
|
local pairIdx = math.floor(row / 2)
|
|
local zc = smallZ0 + pairIdx * pairPitch
|
|
local sign = (row % 2 == 0) and -1 or 1
|
|
markRow("Stall", smallX0, zc, SMALL_PER_ROW, STALL_W, STALL_D, sign)
|
|
placedSmall += SMALL_PER_ROW
|
|
end
|
|
|
|
-- 大型 59: trucks and buses park along the south edge where the bays are
|
|
-- long enough -- three rows of 20/20/19.
|
|
local bigZ = c.Z + APRON_Z / 2 - BIG_D - 14
|
|
local bigCounts = { 20, 20, 19 } -- = 59
|
|
local placedBig = 0
|
|
for r, n in ipairs(bigCounts) do
|
|
markRow("BigStall", c.X - APRON_X / 2 + 18 * M, bigZ - (r - 1) * (BIG_D + AISLE), n, BIG_W, BIG_D, 1)
|
|
placedBig += n
|
|
end
|
|
|
|
-- 身障者用 4: hard against the 本館 entrance, as they are in reality.
|
|
local accX = c.X - APRON_X / 2 + 34 * M
|
|
markRow("AccStall", accX, c.Z - 2 * ACC_W, 4, ACC_W, STALL_D, 1)
|
|
local placedAcc = 4
|
|
|
|
-- Recorded so the headless verifier can assert the real capacity.
|
|
local cap = Instance.new("StringValue")
|
|
cap.Name = "PACapacity"
|
|
cap.Value = ("small=%d,large=%d,accessible=%d,total=%d"):format(
|
|
placedSmall,
|
|
placedBig,
|
|
placedAcc,
|
|
placedSmall + placedBig + placedAcc
|
|
)
|
|
cap.Parent = pa
|
|
|
|
-- Light towers at the apron corners.
|
|
-- Height 60 -> 40 (2026-08-02). With PA_LIFT at 56 the mainline slab
|
|
-- underside sits at ~101; a 60-stud mast topped out at 107 and would have
|
|
-- speared the deck -- swapping one clash for another. At 40 the head sits
|
|
-- at 87, ~14 studs clear. Real PA lighting under a viaduct is short for
|
|
-- exactly this reason. If PA_LIFT changes again, re-check this.
|
|
local MAST_H = 40
|
|
for _, sx in ipairs({ -1, 1 }) do
|
|
for _, sz in ipairs({ -1, 1 }) do
|
|
local lx = c.X + sx * (APRON_X / 2 - 10 * M)
|
|
local lz = c.Z + sz * (APRON_Z / 2 - 10 * M)
|
|
local mast = part({
|
|
Name = "PAMast",
|
|
Size = Vector3.new(3, MAST_H, 3),
|
|
CFrame = CFrame.new(lx, DECK_Y + MAST_H / 2, lz),
|
|
Color = Color3.fromRGB(70, 70, 78),
|
|
Material = Enum.Material.Metal,
|
|
})
|
|
mast.Parent = pa
|
|
local head = part({
|
|
Name = "PALight",
|
|
Size = Vector3.new(10, 2, 10),
|
|
CFrame = CFrame.new(lx, DECK_Y + MAST_H, lz),
|
|
Color = Color3.fromRGB(255, 240, 200),
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
head.Parent = pa
|
|
local pl = Instance.new("PointLight")
|
|
pl.Brightness = 3
|
|
pl.Range = 120
|
|
pl.Color = Color3.fromRGB(255, 235, 190)
|
|
pl.Parent = head
|
|
end
|
|
end
|
|
|
|
-- RING ROAD + ON/OFF RAMPS ------------------------------------------------
|
|
-- At the real Daikoku Futo PA the slip road wraps right around the parking
|
|
-- area in a full circle, with the on-ramp and off-ramp feeding that loop.
|
|
-- Build the ring as a fan of short arc segments, then two ramps joining it
|
|
-- to the expressway at the junctions the median opens for.
|
|
local samples = Circuit.samples
|
|
local plan = planPA()
|
|
local RING_R = plan.ringRadius
|
|
local RING_W = 44
|
|
-- Ramp deck width. Deliberately NARROWER than RING_W: the four loop ramps
|
|
-- nest at radii 70-130 studs apart and the offline collision solve was run
|
|
-- at 32 studs, which is what the measured 51-stud minimum separation below
|
|
-- is a clearance FOR. Widening this eats directly into that margin. 32 is
|
|
-- also right on its own terms: real Daikoku loop ramps are single-lane, ~9 m.
|
|
local RAMP_W = 32
|
|
|
|
-- Per-ramp wrap and landing profile. NOT tunable by eye -- solved offline
|
|
-- as a joint search over all four ramps at once (solve_land.py).
|
|
--
|
|
-- WHY PER-RAMP, and why this was not obvious: the nested-radii argument
|
|
-- ("concentric circles at different radii cannot intersect") only holds
|
|
-- while each ramp HOLDS its radius. Every ramp must eventually peel inward
|
|
-- to the ring to actually deliver the car onto the apron, and that peel
|
|
-- cuts straight across every inner ramp's circle. A first pass used one
|
|
-- shared 300-degree sweep and a shared landing fraction, and measured 7
|
|
-- studs of ramp-to-ramp separation -- four ramps merging into each other
|
|
-- at the gate. The "concentric circles can't collide" reasoning was right
|
|
-- about the wrap and silently wrong about the landing.
|
|
--
|
|
-- The crossing is unavoidable in plan view, so it is cleared in HEIGHT
|
|
-- instead: each ramp drops 55 studs over its wrap, so staggering the
|
|
-- sweeps staggers the altitude at which each one crosses the others.
|
|
-- Measured result of these values: 51 studs minimum separation in 3D.
|
|
--
|
|
-- Read by BOTH the junction/gate placement (which needs to know where the
|
|
-- spiral ends, to cut the parapet opening there) and the deck builder.
|
|
-- One table, so those two can never disagree.
|
|
-- SWEEP NOW COMES FROM planPA, NOT FROM HERE (2026-08-03, shared-ramp
|
|
-- pass). It used to be a hand-tuned per-ramp constant, staggered so four
|
|
-- concentric ramps crossed at different altitudes. With two shared trunks
|
|
-- the sweep is a SOLVED quantity -- it is what positions the fork point so
|
|
-- that both slips can reach their carriageways without hitting anything --
|
|
-- so it travels with the junction (`jn.sweep`) and this table only carries
|
|
-- the landing fraction.
|
|
--
|
|
-- The old hazard the per-ramp values guarded against is still real and
|
|
-- still handled: the "Horizon Racing garage" is pre-existing map decor, so
|
|
-- ramp-vs-ramp and ramp-vs-road checks are blind to it. The offline solver
|
|
-- tests every candidate against the FULL static-obstacle set (garage
|
|
-- included), which is why the accepted sweeps report 0 obstacle hits.
|
|
local RAMP_PROFILE_DEFAULT = { land = 0.10 }
|
|
local RAMP_PROFILE = {}
|
|
local EASE_R = 0.15 -- fraction of the ramp spent easing onto its radius
|
|
local ARC_N = 72
|
|
|
|
local function deck(name, size, cframe, colour)
|
|
local d = part({
|
|
Name = name,
|
|
Size = size,
|
|
CFrame = cframe,
|
|
Color = colour or Color3.fromRGB(38, 38, 44),
|
|
Material = Enum.Material.Asphalt,
|
|
})
|
|
d.Parent = pa
|
|
return d
|
|
end
|
|
|
|
-- The ring itself
|
|
local ringY = c.Y
|
|
|
|
-- Ramp junctions, resolved BEFORE the ring is built so the ring wall can
|
|
-- open a gate at each one.
|
|
--
|
|
-- BUG (2026-08-02): the parapet loop below ran `for sgn in {1,-1}` over
|
|
-- EVERY segment, walling both edges of the ring with no opening anywhere.
|
|
-- The inner lip therefore sealed the parking apron completely: 72 of 72
|
|
-- rays cast outward from the SpawnPad hit a wall/barrier standing ~5.7
|
|
-- studs above the apron (tools/diag_pawall.luau), well over the ~2-3 studs
|
|
-- a car can climb -- so there was no way to drive out of the PA at all.
|
|
-- The mainline barrier already handles this correctly at PA junctions
|
|
-- ("the barrier AND the kill wall must open on the side the ramp leaves
|
|
-- through", see the deck loop above); the ring never got the same
|
|
-- treatment.
|
|
--
|
|
-- TWO gates are needed per junction, and both matter:
|
|
-- * INNER lip -- so the apron connects to the ring road at all.
|
|
-- * OUTER lip -- so the ring road connects to the ramp deck.
|
|
-- Opening only one still leaves the lot sealed.
|
|
--
|
|
-- The two gates are deliberately different sizes. Inside the ring the
|
|
-- apron is solid deck, so a wide opening is free. OUTSIDE the ring is a
|
|
-- ~46-stud drop to ground level, so that gate is kept tight and every
|
|
-- stud of it is backed by ramp/flare deck (see PARampFlare below) --
|
|
-- otherwise we would re-create the old "drove off the side and fell past
|
|
-- FallenPartsDestroyHeight" failure this structure was built to prevent.
|
|
local GATE_IN = 70 -- half-width of the apron-side opening
|
|
local GATE_OUT = 30 -- half-width of the ramp-side opening
|
|
local junctions = {}
|
|
-- FOUR junctions now, one per ramp, resolved in planPA (see the big note
|
|
-- there). Each carries its own carriageway attach point, its own direction
|
|
-- of travel and its own concentric radius; nothing is shared between the
|
|
-- two directions any more.
|
|
for _, jd in ipairs(plan.junctionDefs or {}) do
|
|
local road = jd.nearPos
|
|
-- Turn direction, start angle and sweep are SOLVED in planPA now (they
|
|
-- are what place the fork point), so they are read here rather than
|
|
-- re-derived. Re-deriving them was safe when a ramp just had to point
|
|
-- the right way; with a fork feeding two carriageways, any
|
|
-- disagreement between this block and the deck builder would put the
|
|
-- parapet opening somewhere the ramp never arrives.
|
|
local th0 = jd.az
|
|
local turn = jd.turn
|
|
local thEnd = th0 + turn * jd.sweep
|
|
table.insert(junctions, {
|
|
name = jd.name,
|
|
road = road,
|
|
roadY = road.Y,
|
|
idx = jd.idx,
|
|
mergeIdx = jd.mergeIdx,
|
|
side = jd.side,
|
|
kind = jd.kind,
|
|
radius = jd.radius,
|
|
turn = turn,
|
|
th0 = th0,
|
|
sweep = jd.sweep,
|
|
yFork = jd.yFork,
|
|
nearSide = jd.nearSide,
|
|
nearPos = jd.nearPos,
|
|
nearTravel = jd.nearTravel,
|
|
farPos = jd.farPos,
|
|
farTravel = jd.farTravel,
|
|
emerge = jd.emerge,
|
|
fork = jd.fork,
|
|
-- `travel` is this CARRIAGEWAY's direction of travel, not the
|
|
-- centreline heading -- the opposite carriageway drives the
|
|
-- other way and its slip has to leave pointing that way.
|
|
heading = jd.nearTravel,
|
|
ringPt = Vector3.new(c.X + math.cos(th0) * RING_R, ringY, c.Z + math.sin(th0) * RING_R),
|
|
})
|
|
end
|
|
|
|
local function nearJunction(pos, halfWidth)
|
|
for _, jn in ipairs(junctions) do
|
|
local d = Vector3.new(pos.X - jn.ringPt.X, 0, pos.Z - jn.ringPt.Z)
|
|
if d.Magnitude <= halfWidth then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
for i = 0, ARC_N - 1 do
|
|
local a0 = (i / ARC_N) * math.pi * 2
|
|
local a1 = ((i + 1) / ARC_N) * math.pi * 2
|
|
local p0 = Vector3.new(c.X + math.cos(a0) * RING_R, ringY, c.Z + math.sin(a0) * RING_R)
|
|
local p1 = Vector3.new(c.X + math.cos(a1) * RING_R, ringY, c.Z + math.sin(a1) * RING_R)
|
|
local segMid = (p0 + p1) / 2
|
|
local segLen = (p1 - p0).Magnitude + 1.5
|
|
local segCF = CFrame.lookAt(segMid, p1, UP)
|
|
-- PARing deck REMOVED (2026-08-02, user call): this laid a 44-stud-wide
|
|
-- band of road deck in a circle around the PA. Measured flush with the
|
|
-- apron (both tops at y=47.00) and entirely inside the apron footprint
|
|
-- (ring bbox X -4535..-3712 vs apron X -4538..-3710), so it was not
|
|
-- structural -- just a differently-shaded ring of pavement painted over
|
|
-- asphalt the apron already provides. Reads as a leftover ring where
|
|
-- the old barriers used to be. Deleting it leaves no hole: the apron
|
|
-- slab underneath is continuous.
|
|
--
|
|
-- The loop itself still runs: `junctions` and the ramp/flare geometry
|
|
-- below are positioned off these arc points, so the circle is still
|
|
-- the reference line -- it just no longer emits a deck.
|
|
|
|
-- Ring edge: NO barrier on the circulation road any more.
|
|
--
|
|
-- CHANGE (2026-08-02, user call, third pass): the edge used to be
|
|
-- emitted here, per ring-road arc segment, so it inherited the ring's
|
|
-- CIRCLE -- radius ~412 about the PA centre. But the parking area is a
|
|
-- SQUARE 828 x 828 apron (real Daikoku Futo is 181 m x 174 m, see the
|
|
-- layout note above). A circle inscribed in that square bulges past
|
|
-- the apron at the four corners and cuts INSIDE it at the four edge
|
|
-- midpoints, so the barrier visibly sliced across the lot instead of
|
|
-- following its perimeter -- read as a second ring.
|
|
--
|
|
-- The perimeter barrier is now built ONCE, after this loop, as four
|
|
-- straight runs along the apron edge. Nothing is emitted per-arc.
|
|
|
|
-- No pillars under the ring any more. The ring is now a GROUND-LEVEL
|
|
-- circulation road sharing the apron slab (Circuit.PA_LIFT raises the
|
|
-- MAINLINE over the PA instead of leaving both at y=46). It is the
|
|
-- expressway that flies overhead on pillars here, exactly as in the
|
|
-- reference photos: a flat lot with parked cars, elevated deck above.
|
|
-- Mainline pillars are built in buildRoad; see PA_LIFT.
|
|
end
|
|
|
|
-- PERIMETER BARRIER -------------------------------------------------------
|
|
-- A single simple barrier following the EDGE OF THE PARKING AREA, which is
|
|
-- a square apron (APRON half-extent), not the circular ring road. Built as
|
|
-- four straight runs of short segments so gaps can be opened wherever a
|
|
-- ramp crosses the boundary -- otherwise the barrier would seal the PA and
|
|
-- trap the player. Segment length is kept modest so a gap lands cleanly.
|
|
do
|
|
local PERIM_H = 3.0
|
|
local PERIM_T = 1.6
|
|
local SEG = 40
|
|
-- Sit the barrier just inside the slab edge so it reads as the lot's
|
|
-- own kerb line rather than floating off the asphalt.
|
|
local half = APRON - PERIM_T / 2 - 1
|
|
local baseY = DECK_Y + 1 + PERIM_H / 2
|
|
|
|
-- Ramps tee in at the ring; open the perimeter where a ramp's centre
|
|
-- line crosses the square, plus clearance for the full ramp width.
|
|
local GATE = 90
|
|
local function blocked(x, z)
|
|
for _, jn in ipairs(junctions) do
|
|
-- Project the junction outward onto the square boundary and
|
|
-- compare against this segment's centre.
|
|
local d = Vector3.new(x - jn.ringPt.X, 0, z - jn.ringPt.Z)
|
|
if d.Magnitude <= GATE then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local n = 0
|
|
for _, side in ipairs({ "N", "S", "E", "W" }) do
|
|
local along = -half
|
|
while along < half do
|
|
local segLen = math.min(SEG, half - along)
|
|
local mid = along + segLen / 2
|
|
local x, z, sizeV
|
|
if side == "N" then
|
|
x, z = c.X + mid, c.Z - half
|
|
sizeV = Vector3.new(segLen + 0.5, PERIM_H, PERIM_T)
|
|
elseif side == "S" then
|
|
x, z = c.X + mid, c.Z + half
|
|
sizeV = Vector3.new(segLen + 0.5, PERIM_H, PERIM_T)
|
|
elseif side == "W" then
|
|
x, z = c.X - half, c.Z + mid
|
|
sizeV = Vector3.new(PERIM_T, PERIM_H, segLen + 0.5)
|
|
else
|
|
x, z = c.X + half, c.Z + mid
|
|
sizeV = Vector3.new(PERIM_T, PERIM_H, segLen + 0.5)
|
|
end
|
|
if not blocked(x, z) then
|
|
local seg = part({
|
|
Name = "PAPerimeter",
|
|
Size = sizeV,
|
|
CFrame = CFrame.new(x, baseY, z),
|
|
Color = C_PARAPET,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
seg.Parent = pa
|
|
n = n + 1
|
|
end
|
|
along = along + segLen
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Two ramps: mainline -> ring (entry) and ring -> mainline (exit).
|
|
-- `junctions` was resolved above (the ring wall needed it to place gates);
|
|
-- reuse it so the ramp deck and the wall opening can never disagree.
|
|
--
|
|
-- PA-CONCENTRIC LOOP RAMPS (2026-08-03, second pass).
|
|
--
|
|
-- The first helix attempt spiralled around a circle placed one radius
|
|
-- perpendicular to the MAINLINE. That fixed the grade (13.6% -> ~2%) but
|
|
-- produced the wrong shape, and the measurements said so plainly: the two
|
|
-- ramps swung 414..1674 studs from the PA centre and covered only 60-75
|
|
-- degrees of compass around it. They were long sweeping curves that
|
|
-- wandered off over open water -- not loops, and they never went round the
|
|
-- parking area at all, because nothing in the construction referenced the
|
|
-- parking area.
|
|
--
|
|
-- THE FIX: spiral concentric with THE PA CENTRE, not with the mainline.
|
|
-- Each ramp holds a fixed radius about `c` and wraps ~300 degrees while
|
|
-- descending, so it visibly circles the parking structure exactly like the
|
|
-- real Daikoku loop ramps.
|
|
--
|
|
-- Radii are NESTED (620 / 690 / 840 / 970 studs), which is what makes four
|
|
-- overlapping ramps safe: concentric circles of different radii cannot
|
|
-- intersect, so no pair can ever collide no matter how far they wrap. The
|
|
-- junctions are chosen (in planPA) at points where the carriageway is
|
|
-- ALREADY near its ramp's radius, so no ramp has to cut radially across a
|
|
-- neighbour's circle to reach its own.
|
|
--
|
|
-- For scale: an OSM Overpass pull of the real interchange's curved ways,
|
|
-- Kasa-circle-fitted, gives cleanest radii of 147 m and 124 m = 525 and
|
|
-- 443 studs. The apron's corner radius here is 518 studs, so a ramp has to
|
|
-- sit somewhat outside that to clear the lot -- 620 is the tightest that
|
|
-- does. See docs/daikoku-reference-spec.md.
|
|
--
|
|
-- Solved and verified offline against all 823 mainline samples:
|
|
-- grade 2.0 - 3.8% (was 10.6 / 13.6% straight, ~6% is the target)
|
|
-- wrap 210 - 255 deg (was 60-75)
|
|
-- radius 390..1093 (was 414..1674)
|
|
-- ramp/ramp 51 studs min 3D separation for a 32-stud deck
|
|
-- ramp/road 17 studs min vertical clearance at crossings
|
|
local LIFT = Circuit.PA_LIFT or 0
|
|
local GIRDER_DEPTH = 8 -- deck soffit sits this far under the running surface
|
|
local CAR_H = 9
|
|
|
|
local function ease(t)
|
|
if t < 0 then
|
|
t = 0
|
|
elseif t > 1 then
|
|
t = 1
|
|
end
|
|
return t * t * (3 - 2 * t)
|
|
end
|
|
|
|
-- Is `p` inside the mainline's horizontal footprint? Used to stop ramp
|
|
-- pillars being planted in the expressway (or in its piers) where the
|
|
-- under-deck crossing passes beneath it.
|
|
local function underMainline(p, pad)
|
|
for _, s in ipairs(samples) do
|
|
local dx, dz = p.X - s.pos.X, p.Z - s.pos.Z
|
|
if dx * dx + dz * dz < (ROAD_WIDTH / 2 + pad) ^ 2 then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- One emitter for every run of ramp deck. Takes a plain list of world
|
|
-- points (the vertical profile is already baked in) plus flags for which
|
|
-- ends are mouths that must stay open.
|
|
--
|
|
-- Previously the deck, pillars, kerbs and chevrons were written inline in
|
|
-- a single spiral loop. There are now FOUR runs per ramp (trunk, near
|
|
-- slip, under-deck crossing, far merge), so that code has to be shared or
|
|
-- the four runs drift apart in appearance and in kerb behaviour.
|
|
local function emitRun(runName, pts, openHead, openTail, wantChevrons)
|
|
for s = 1, #pts - 1 do
|
|
local a, b = pts[s], pts[s + 1]
|
|
local segMid = (a + b) / 2
|
|
-- Overlap consecutive segments slightly: a sloped deck is a
|
|
-- rotated box, so butt-jointed segments leave a thin wedge gap at
|
|
-- each seam that a wheel can catch on.
|
|
local segLen = (b - a).Magnitude + 3
|
|
local dck = deck(runName, Vector3.new(RAMP_W, 2, segLen), CFrame.lookAt(segMid, b, UP))
|
|
dck.Name = runName .. "_" .. s
|
|
|
|
-- Support pillar down to the apron under the raised part of the
|
|
-- ramp, matching the reference photos' elevated structure.
|
|
--
|
|
-- Round, not square (corrected 2026-08-03 against the underside
|
|
-- footage in honmoku_jct_sheet3, which is actually shot beneath
|
|
-- Daikoku itself): every column carrying the ramp decks there is
|
|
-- a smooth cylindrical concrete pier. These were 6x6 boxes.
|
|
--
|
|
-- The base has to be chosen per-pillar. The old code always cut
|
|
-- the column off at `ringY - 1` (Y=45, the underside of the PA
|
|
-- apron slab). That is right only while the ramp is still over
|
|
-- the slab. The measured helix climbs up to 1182 studs BEYOND
|
|
-- the slab edge, and out there nothing exists at Y=45 -- no
|
|
-- terrain, no baseplate -- so all 55 columns ended in mid-air.
|
|
-- Beyond the slab, run them to ground like the viaduct piers do.
|
|
local overApron = math.abs(segMid.X - c.X) <= APRON and math.abs(segMid.Z - c.Z) <= APRON
|
|
local baseY = overApron and (ringY - 1) or -1
|
|
local h = segMid.Y - baseY
|
|
-- ...and NOT where the run passes beneath the expressway. The
|
|
-- under-deck crossing threads the gap between the girder soffit
|
|
-- and the ground; a column raised there would be planted through
|
|
-- the mainline deck itself (and through its piers), which is
|
|
-- exactly the class of solid-obstacle hit the offline solve was
|
|
-- run to eliminate. Those spans are short and are carried by the
|
|
-- piers on either side.
|
|
local blocked = underMainline(segMid, RAMP_W)
|
|
if h > 10 and s % 2 == 0 and not blocked then
|
|
local pil = part({
|
|
Name = "PARampPillar",
|
|
-- Cylinder axis is local X, so Size.X is the height and
|
|
-- Size.Y/Z the diameter; the Z rotation stands it upright.
|
|
-- (Same transposition that had all 80 viaduct PierColumns
|
|
-- built as 8-stud discs -- see the PierColumn note above.)
|
|
Size = Vector3.new(h, 6, 6),
|
|
CFrame = CFrame.new(segMid.X, segMid.Y - h / 2, segMid.Z) * CFrame.Angles(0, 0, math.rad(90)),
|
|
Color = C_CONCRETE,
|
|
Material = Enum.Material.Concrete,
|
|
Shape = Enum.PartType.Cylinder,
|
|
})
|
|
pil.Parent = pa
|
|
end
|
|
-- Edge kerbs so a car can't slide off the side of a raised ramp.
|
|
--
|
|
-- BUG (2026-08-02, found by test drive + tools/diag_ramp.luau):
|
|
-- the guard used to be `h > 8`, which is a HEIGHT test on a ramp
|
|
-- that starts at apron level. The first 2-3 segments of every ramp
|
|
-- (heightAboveApron 0.5 .. 5.6) therefore got no kerb at all, so
|
|
-- the barrier visibly stopped part-way down the ramp -- the
|
|
-- "side barriers are not actually fully finished" report. The low
|
|
-- end is not a fall hazard, but it still needs an edge or the
|
|
-- structure just ends in mid-air visually.
|
|
--
|
|
-- The other half of that report -- "barriers extend too far into
|
|
-- the main highway" -- is the opposite end: the kerb ran right to
|
|
-- the road end of the ramp, so a kerb stood IN the mainline
|
|
-- driving lane at the merge (measured: a 2.4-stud step 14 studs
|
|
-- right of the centreline, hit as Road_526 -> PARampKerb). A merge
|
|
-- has to be open, so the last segment carries no kerb.
|
|
-- Kerb EVERY segment except the merge mouth. The height test is
|
|
-- gone entirely: `h > 8` then `h > 2` both still failed on the
|
|
-- foot of the ramp (h = 0.5 .. 0.8 at apron level), which is
|
|
-- exactly where the barrier was seen "ending in mid-air" -- the
|
|
-- edge began one segment UP the ramp, leaving the first ~40 studs
|
|
-- of both ramps completely open on both sides. A ramp edge is
|
|
-- continuous in reality regardless of how high it happens to be,
|
|
-- so the only exclusion left is the merge, which must stay open.
|
|
-- The merge mouth is now the LAST path point (the ramp is a helix,
|
|
-- so `STEPS` no longer exists -- the path length varies per ramp).
|
|
-- The mouth is no longer always the tail: a run can be open at
|
|
-- either end now (the fork end of the near slip is open to the
|
|
-- trunk, the merge end is open to the mainline), so the caller
|
|
-- declares which ends are mouths. `#pathPts` no longer exists.
|
|
local isMergeSeg = (openTail and s >= #pts - 2) or (openHead and s <= 2)
|
|
if not isMergeSeg then
|
|
local fwd2 = (b - a)
|
|
fwd2 = Vector3.new(fwd2.X, 0, fwd2.Z)
|
|
if fwd2.Magnitude > 0.01 then
|
|
local rightV = fwd2.Unit:Cross(UP)
|
|
for _, sg in ipairs({ -1, 1 }) do
|
|
local kerb = part({
|
|
Name = "PARampKerb",
|
|
Size = Vector3.new(1.6, 3.2, segLen),
|
|
CFrame = CFrame.lookAt(
|
|
segMid + rightV * (sg * RAMP_W / 2),
|
|
segMid + rightV * (sg * RAMP_W / 2) + (b - a).Unit,
|
|
UP
|
|
) * CFrame.new(0, 2.6, 0),
|
|
Color = C_PARAPET,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
kerb.Parent = pa
|
|
end
|
|
|
|
-- Red/white chevron delineators.
|
|
--
|
|
-- CORRECTED 2026-08-03 against 4K dusk onboard footage of
|
|
-- the junction (jct_tsubasa_dusk, t=110..140s). These are
|
|
-- NOT road-surface paint, which is what the first pass
|
|
-- built -- a flat red slab lying on the deck. In reality
|
|
-- they are VERTICAL PANELS bolted to the outside face of
|
|
-- the concrete barrier, alternating red and white, running
|
|
-- continuously along the outside of the curve. They are
|
|
-- the main thing that makes a helix legible from inside
|
|
-- the car, and on the deck they were nearly invisible.
|
|
--
|
|
-- Roblox part granularity can't do a diagonal stripe
|
|
-- inside one panel, so alternating whole panels per
|
|
-- segment stands in for the diagonal hatching.
|
|
--
|
|
-- Outward side of the curve. Taking the raw radial
|
|
-- (segMid - c) as the offset direction is WRONG on the
|
|
-- landing peel: there the radial is no longer
|
|
-- perpendicular to travel, so panels skew along the road
|
|
-- (measured: up to 33 studs of along-travel error over the
|
|
-- last ~9 segments, walking the panel line diagonally
|
|
-- across the deck instead of tracking its edge).
|
|
--
|
|
-- Offset along the true perpendicular `rightV` always, and
|
|
-- use the radial ONLY to choose which side is outboard.
|
|
-- The ramps spiral concentrically about the PA centre, so
|
|
-- `c` is the axis to measure outboard from. (This read `O`,
|
|
-- the old per-ramp helix origin, which no longer exists.)
|
|
local radial = Vector3.new(segMid.X - c.X, 0, segMid.Z - c.Z)
|
|
if wantChevrons and radial.Magnitude > 0.01 and rightV.Magnitude > 0.01 then
|
|
local sgn = (rightV:Dot(radial.Unit) >= 0) and 1 or -1
|
|
local face = segMid + rightV * (sgn * RAMP_W / 2)
|
|
local chev = part({
|
|
Name = "PARampChevron",
|
|
Size = Vector3.new(0.5, 2.0, segLen),
|
|
CFrame = CFrame.lookAt(face, face + (b - a).Unit, UP) * CFrame.new(0, 2.7, 0),
|
|
Color = (s % 2 == 0) and Color3.fromRGB(198, 46, 40) or Color3.fromRGB(240, 238, 232),
|
|
Material = Enum.Material.SmoothPlastic,
|
|
})
|
|
chev.Parent = pa
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- SHARED TRUNK + FORK, one pair of slips per ramp -------------------------
|
|
-- Built from the SOLVED parameters in planPA. Each of the four runs below
|
|
-- is generated by exactly the same arithmetic the offline solver and the
|
|
-- independent verifier used, so what drives here is what was measured:
|
|
-- trunk ring -> fork, concentric spiral about the PA centre, climbing
|
|
-- apron level -> yFork
|
|
-- near fork -> near carriageway, descending to the LOCAL deck height
|
|
-- cross fork -> emerge, held FLAT at yc so it passes under the mainline
|
|
-- merge emerge -> far carriageway, climbing to the LOCAL deck height
|
|
--
|
|
-- Sample counts match the verifier (401 on the trunk, 201 on the slips) so
|
|
-- the built centreline IS the verified centreline, not a re-approximation
|
|
-- of it at some other resolution.
|
|
local APRON_Y = c.Y + 1
|
|
local LANE_OFF = ROAD_WIDTH / 4
|
|
local nS = #samples
|
|
|
|
-- Centre of one carriageway at sample i. Must match planPA's
|
|
-- carriagewayPoint exactly -- if these two disagree the slip lands beside
|
|
-- the road instead of on it.
|
|
local function carriagewayPoint(i, side)
|
|
local s = samples[i].pos
|
|
local nxt = samples[(i % nS) + 1].pos
|
|
local hh = math.atan2(nxt.Z - s.Z, nxt.X - s.X)
|
|
return Vector3.new(s.X - math.sin(hh) * LANE_OFF * side, s.Y, s.Z + math.cos(hh) * LANE_OFF * side)
|
|
end
|
|
|
|
for _, jn in ipairs(junctions) do
|
|
local rampR = jn.radius
|
|
local yf = jn.yFork
|
|
local ns = jn.nearSide
|
|
local mergeIdx = jn.mergeIdx
|
|
-- ACTUAL local deck height at the merge, not a constant 102. The
|
|
-- mainline is a real profile; the exit merge sits at 96.6.
|
|
local yd = samples[mergeIdx].pos.Y
|
|
|
|
-- The under-deck crossing is gone, so the girder-soffit ceiling that
|
|
-- constrained it is no longer needed here.
|
|
|
|
-- 1. TRUNK -- perimeter spiral, ring gate out to the fork point. The
|
|
-- radius eases across the WHOLE sweep (previously it snapped out over
|
|
-- the first 22% and then held a constant circle), so the ramp hugs
|
|
-- the PA edge and flares out to meet the mainline -- the shape in the
|
|
-- drawing. Climb is spread over the full run too: 50 studs across a
|
|
-- ~900-stud arc is ~5.5%, well inside the 9% limit, so there is no
|
|
-- reason to front-load it.
|
|
local trunk = {}
|
|
for s = 0, 400 do
|
|
local f = s / 400
|
|
local th = jn.th0 + jn.turn * jn.sweep * f
|
|
local r = RING_R + (rampR - RING_R) * ease(f)
|
|
local y = APRON_Y + (yf - APRON_Y) * ease(f)
|
|
table.insert(trunk, Vector3.new(c.X + math.cos(th) * r, y, c.Z + math.sin(th) * r))
|
|
end
|
|
local fork = trunk[#trunk]
|
|
|
|
-- 2. SLIP -- fork to the carriageway on the PA side, at deck level.
|
|
-- There is only ONE slip now. The far carriageway is reached through
|
|
-- the gap in the centre divider (see `open` in planPA), which is what
|
|
-- the user's drawing is about: "this is why the middle
|
|
-- barriers/dividers are important because each side will have access
|
|
-- to on and off ramps". Both carriageways sit at the same deck height
|
|
-- and are directly adjacent, so a second slip diving under the
|
|
-- expressway was solving a problem that does not exist.
|
|
local tgtNear = carriagewayPoint(mergeIdx, ns)
|
|
local near = {}
|
|
for s = 0, 200 do
|
|
local f = s / 200
|
|
table.insert(
|
|
near,
|
|
Vector3.new(
|
|
fork.X + (tgtNear.X - fork.X) * f,
|
|
yf + (yd - yf) * ease(f),
|
|
fork.Z + (tgtNear.Z - fork.Z) * f
|
|
)
|
|
)
|
|
end
|
|
|
|
-- An on-ramp is this same geometry driven the other way -- the deck is
|
|
-- undirected, so only which ends are MOUTHS matters, and every run
|
|
-- here is open at both ends (ring gate / fork, fork / carriageway).
|
|
-- Chevrons go on the trunk only: that is the curved helix they exist
|
|
-- to make legible, and on the straight slip they would just be a red
|
|
-- wall alongside the merge.
|
|
emitRun(jn.name .. "Trunk", trunk, true, true, true)
|
|
emitRun(jn.name .. "Near", near, true, true, false)
|
|
|
|
-- Flare the ramp mouth across the outer gate so the opening in the
|
|
-- ring parapet is fully backed by deck -- the trunk meets the ring at
|
|
-- a tangent and would otherwise leave triangular gaps at the corners.
|
|
local toRing = Vector3.new(jn.ringPt.X - jn.road.X, 0, jn.ringPt.Z - jn.road.Z)
|
|
if toRing.Magnitude > 1 then
|
|
local flareCF = CFrame.lookAt(
|
|
Vector3.new(jn.ringPt.X, ringY, jn.ringPt.Z),
|
|
Vector3.new(jn.road.X, ringY, jn.road.Z),
|
|
UP
|
|
)
|
|
local flare = deck("PARampFlare", Vector3.new(GATE_OUT * 2 + RAMP_W, 2, RAMP_W + 24), flareCF)
|
|
flare.Color = Color3.fromRGB(38, 38, 44)
|
|
end
|
|
end
|
|
|
|
-- The PA apron used to be a bare 520x520 slab floating ~46 studs up with
|
|
-- nothing at its edges: endurance testing drove straight off the side and
|
|
-- fell past FallenPartsDestroyHeight (-500), destroying the car and
|
|
-- stranding the player seatless. That is now solved structurally -- the
|
|
-- apron below extends all the way out to the ring road, and the ring's
|
|
-- outer lip walls the whole thing -- so there is no exposed edge to fall
|
|
-- off and no need for the old four-sided perimeter box.
|
|
|
|
-- SPAWN PAD PLACEMENT. This used to sit exactly on PA_CENTER, which was
|
|
-- fine while the apron centre was empty. It is NOT fine after the survey
|
|
-- landed: the OSM stall block is centred near the apron middle, so the pad
|
|
-- punched a 120x120 dark hole straight through the painted 小型 rows
|
|
-- (visible as a black rectangle in the overhead capture).
|
|
--
|
|
-- The offset below is MEASURED, not chosen: a 15-stud grid search over the
|
|
-- whole apron scored every candidate by its clearance to all 804 built PA
|
|
-- obstacles (buildings, stalls, props) while staying inside the ring road.
|
|
-- (-165, -150) is the best cell, with 93 studs of clearance -- comfortably
|
|
-- more than the pad's 60-stud half-width. It lands on the open southwest
|
|
-- circulation area, which is empty tarmac on the real site too.
|
|
local SPAWN_OFF = Vector3.new(-165, 0.3, -150)
|
|
local pad = part({
|
|
Name = "SpawnPad",
|
|
Size = Vector3.new(120, 2.4, 120),
|
|
CFrame = CFrame.new(c + SPAWN_OFF),
|
|
Color = Color3.fromRGB(20, 20, 26),
|
|
Material = Enum.Material.Neon,
|
|
Transparency = 0.25,
|
|
})
|
|
pad.Parent = pa
|
|
|
|
-- Roblox drops the character at (0,0,0) unless a real SpawnLocation exists,
|
|
-- and CarManager spawns the car onto the character -- so without this the
|
|
-- player starts ~8,600 studs off-circuit regardless of where SpawnPad is.
|
|
-- Neutral + non-collidable so it never becomes an obstacle on the pad.
|
|
-- Must sit ON the pad -- it is keyed to the same measured offset, because
|
|
-- CarManager spawns the car onto the character. Leaving this at PA_CENTER
|
|
-- while the pad moved would drop the player into the middle of the 小型
|
|
-- stall rows instead of onto clear tarmac.
|
|
local spawnLoc = Instance.new("SpawnLocation")
|
|
spawnLoc.Name = "PASpawn"
|
|
spawnLoc.Size = Vector3.new(24, 1, 24)
|
|
spawnLoc.CFrame = CFrame.new(c + Vector3.new(SPAWN_OFF.X, 2, SPAWN_OFF.Z))
|
|
spawnLoc.Transparency = 1
|
|
spawnLoc.CanCollide = false
|
|
spawnLoc.Anchored = true
|
|
spawnLoc.Neutral = true
|
|
spawnLoc.Duration = 0
|
|
spawnLoc.Parent = pa
|
|
|
|
-- BUILDINGS ---------------------------------------------------------------
|
|
-- SURVEYED LAYOUT, not art direction. Every position below is read from
|
|
-- OpenStreetMap and converted to studs, so the arrangement matches the real
|
|
-- 大黒ふ頭PA rather than approximating it.
|
|
--
|
|
-- Sources (fetched 2026-08-02 via the OSM API, not Overpass -- Overpass
|
|
-- 504s repeatedly on this bbox):
|
|
-- * way 59608205 amenity=parking 大黒ふ頭パーキングエリア -- the apron
|
|
-- * way 405966005 highway=rest_area 大黒PA (wikidata Q10938704)
|
|
-- * building ways + amenity/shop nodes within 230 m of the apron centroid
|
|
--
|
|
-- Local frame: origin = apron polygon centroid (35.461455 N, 139.680201 E),
|
|
-- +xe = EAST metres, +zn = NORTH metres. Mapped into the game world as
|
|
-- East -> +X and North -> -Z, which is the convention the rest of the map
|
|
-- already uses.
|
|
--
|
|
-- THE BIG CORRECTION: the apron is NOT the axis-aligned square the previous
|
|
-- code assumed. Measured from the polygon, it is a PARALLELOGRAM lying on a
|
|
-- SW-NE diagonal -- principal axis bearing ~37 deg, longest edge 155.8 m at
|
|
-- bearing 45.4 deg, area 1.60 ha. The 181 x 174 m figure quoted before is
|
|
-- just the bounding box of that diagonal lot, which is why buildings placed
|
|
-- on N/S/E/W edges landed in the wrong places. The two named buildings sit
|
|
-- at OPPOSITE ENDS OF THE DIAGONAL, ~175 m apart:
|
|
-- 本館 (main building) at the SOUTH-WEST end, near the neck
|
|
-- フォンターナ・ダイコク at the NORTH-EAST tip
|
|
-- That single fact is most of what makes the lot read as Daikoku.
|
|
local C_BLDG = Color3.fromRGB(214, 210, 200)
|
|
local C_GLASS = Color3.fromRGB(120, 150, 165)
|
|
|
|
-- OSM local metres -> world studs.
|
|
--
|
|
-- Two corrections are folded into this transform, both forced by MEASURED
|
|
-- constraints rather than taste:
|
|
--
|
|
-- 1. ORIGIN SHIFT (+32 m North). The OSM parking polygon's centroid is not
|
|
-- the centre of the SITE -- the buildings all sit north and east of the
|
|
-- stalls, so the full site bbox spans east -87..+111 and north -53..+137.
|
|
-- Centring on the parking centroid pushed 7 of 12 features off the apron.
|
|
-- Solving for the origin that minimises the enclosing radius gives
|
|
-- (+0.0 E, +32.0 N), which is the value used here.
|
|
--
|
|
-- 2. UNIFORM SCALE (0.88). The real site needs a 113.9 m radius after the
|
|
-- shift, but the PA ring road only offers 103.0 m of clear space inside
|
|
-- it (ring centreline PA_RADIUS+130 = 390 studs, ring 44 studs wide).
|
|
-- The ring CANNOT simply be grown: the nearest mainline sample is only
|
|
-- 330 studs from PA_CENTER (measured in-engine), so the ring is already
|
|
-- wider than its clearance to the expressway and enlarging it would push
|
|
-- the loop through the road deck and break the ramp junctions.
|
|
-- Scaling the survey down by 0.88 instead fits it in the existing
|
|
-- envelope. Because the scale is UNIFORM, every distance and angle keeps
|
|
-- its true proportion -- the relative arrangement of the buildings, which
|
|
-- is what makes the lot recognisable as Daikoku, is preserved exactly.
|
|
local OSM_SHIFT_N = 32.0
|
|
local OSM_SCALE = 0.88
|
|
local function osmX(xe: number): number
|
|
return c.X + xe * OSM_SCALE * M
|
|
end
|
|
local function osmZ(zn: number): number
|
|
return c.Z - (zn - OSM_SHIFT_N) * OSM_SCALE * M
|
|
end
|
|
-- OSM edge bearing (degrees, measured as atan2(east, north) the way the
|
|
-- survey reports it) -> Roblox Y rotation. Derivation: we want the part's
|
|
-- LookVector (-sin t, 0, -cos t) to lie along the OSM direction
|
|
-- (cos a East, sin a North) = (cos a, 0, -sin a) in world axes, which
|
|
-- solves to t = a - 90 deg.
|
|
local function osmYaw(angDeg: number): number
|
|
return math.rad(angDeg - 90)
|
|
end
|
|
|
|
-- GENERATED PA STRUCTURES DISABLED (2026-08-02, user call): the placeholder
|
|
-- buildings and props are being replaced with Creator Store assets placed
|
|
-- by hand into workspace.PAProps. Flip this back to true to restore them.
|
|
--
|
|
-- A flag, not a deletion: every position below is surveyed against a real
|
|
-- OSM way (本館, フォンターナ way 88790141, toilets 88790136, kiosk 88790109,
|
|
-- hut 88790185) via osmX/osmZ with OSM_SHIFT_N + OSM_SCALE. That survey is
|
|
-- the useful part -- it tells you exactly where each real structure stands,
|
|
-- which is the reference for placing the store assets. Keeping the calls
|
|
-- live but silent preserves it.
|
|
local DRAW_PA_STRUCTURES = false
|
|
|
|
-- `long` runs along the building's own long axis, `wide` across it.
|
|
-- Footprints are scaled by the same OSM_SCALE as the positions, so the lot
|
|
-- stays a faithful uniform reduction rather than a stretched one.
|
|
local function building(name, long_m, height_m, wide_m, xe, zn, angDeg)
|
|
if not DRAW_PA_STRUCTURES then
|
|
return nil
|
|
end
|
|
local sy = height_m * M
|
|
local cf = CFrame.new(osmX(xe), DECK_Y + sy / 2, osmZ(zn)) * CFrame.Angles(0, osmYaw(angDeg), 0)
|
|
local b = part({
|
|
Name = name,
|
|
Size = Vector3.new(wide_m * OSM_SCALE * M, sy, long_m * OSM_SCALE * M),
|
|
CFrame = cf,
|
|
Color = C_BLDG,
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
b.Parent = pa
|
|
-- Ground-floor glazing so it reads as a rest stop, not a warehouse.
|
|
local glassH = math.min(4 * M, sy * 0.7)
|
|
local glass = part({
|
|
Name = name .. "Glazing",
|
|
Size = Vector3.new(wide_m * OSM_SCALE * M + 0.6, glassH, long_m * OSM_SCALE * M + 0.6),
|
|
CFrame = cf * CFrame.new(0, -sy / 2 + glassH / 2 + 0.2 * M, 0),
|
|
Color = C_GLASS,
|
|
Material = Enum.Material.Glass,
|
|
Transparency = 0.35,
|
|
})
|
|
glass.Parent = pa
|
|
return b
|
|
end
|
|
|
|
-- 本館 (main building) -- SW end of the lot. Three storeys (1F shop/toilet
|
|
-- block, 2F rest area reopened 2025-10-30, 3F closed). Its footprint is not
|
|
-- mapped as a building way in OSM, so the extent is reconstructed from the
|
|
-- tight POI cluster that sits inside it: food_court (-22,-64), Bay Side
|
|
-- Restaurant (-25,-62), bakery Little Marmaid (-31,-62), toilets (-22,-59),
|
|
-- elevator (-34,-51), telephone (-21,-49), Blue Seal (-42,-50), cafe
|
|
-- Cafe de Crie (-43,-49), convenience e-Mart (-40,-45). Those span roughly
|
|
-- x -45..-18, z -66..-43, hence a ~46 x 34 m block centred (-32,-53)
|
|
-- squared up to the 45 deg lot grid.
|
|
building("PAHonkan", 46, 13, 34, -32, -53, 45)
|
|
if DRAW_PA_STRUCTURES then
|
|
makeSign("本館 MAIN BUILDING", CFrame.new(osmX(-32), DECK_Y + 15 * M, osmZ(-53) + 20 * M), pa)
|
|
end
|
|
|
|
-- フォンターナ・ダイコク / ベイサイドテラス -- NE tip. OSM way 88790141,
|
|
-- oriented bbox 28 x 43 m at bearing +43 deg, centre (+13.9, +123.1).
|
|
building("PANibankan", 43, 7, 28, 13.9, 123.1, 43)
|
|
if DRAW_PA_STRUCTURES then
|
|
makeSign("フォンターナ・ダイコク", CFrame.new(osmX(13.9), DECK_Y + 9 * M, osmZ(123.1) + 16 * M), pa)
|
|
end
|
|
|
|
-- Remaining mapped structures, each from its own OSM building way.
|
|
building("PAToiletNorth", 21, 5, 7, -17.5, 109.8, -44) -- way 88790136
|
|
building("PAKioskWest", 16, 5, 9, -87.2, 24.5, 46) -- way 88790109
|
|
building("PAHutWest", 14, 4.5, 10, -71.9, 42.2, -45) -- way 88790185
|
|
|
|
-- Free-standing amenity nodes. Small props, but they are what stops the
|
|
-- apron reading as an empty car park.
|
|
local function prop(name, sx_m, sy_m, sz_m, xe, zn, colour, mat)
|
|
if not DRAW_PA_STRUCTURES then
|
|
return nil
|
|
end
|
|
local p = part({
|
|
Name = name,
|
|
Size = Vector3.new(sx_m * OSM_SCALE * M, sy_m * M, sz_m * OSM_SCALE * M),
|
|
CFrame = CFrame.new(osmX(xe), DECK_Y + (sy_m * M) / 2, osmZ(zn)),
|
|
Color = colour,
|
|
Material = mat or Enum.Material.Metal,
|
|
})
|
|
p.Parent = pa
|
|
return p
|
|
end
|
|
|
|
-- Lawson (+21,+136.7) and the north toilet block (-3.3,+124) flank Fontana.
|
|
prop("PALawson", 14, 4, 10, 21.0, 136.7, Color3.fromRGB(70, 130, 200), Enum.Material.Concrete)
|
|
prop("PAToiletBlockN", 12, 4, 8, -3.3, 124.0, Color3.fromRGB(205, 200, 190), Enum.Material.Concrete)
|
|
-- 噴水 fountain (+5.9,+122.5) -- the photo spot on the north terrace.
|
|
prop("PAFountain", 9, 0.8, 9, 5.9, 122.5, Color3.fromRGB(150, 170, 185), Enum.Material.Slate)
|
|
-- Smoking shelters: north terrace (+1.4,+103.9) and by the main building.
|
|
prop("PASmokingN", 7, 3, 5, 1.4, 103.9, Color3.fromRGB(150, 150, 155))
|
|
prop("PASmokingS", 7, 3, 5, -47.7, -31.5, Color3.fromRGB(150, 150, 155))
|
|
-- 二輪車駐車場: motorcycles park separately, right by the main building.
|
|
prop("PAMotorcycleBay", 20, 0.05, 12, 9.0, -44.9, Color3.fromRGB(70, 70, 78), Enum.Material.Asphalt)
|
|
|
|
-- EV rapid charger: first on any Japanese expressway, installed 2008-10-10.
|
|
-- OSM charging_station node sits at (-58.3, +30.2), on the west flank.
|
|
for k = 0, 1 do
|
|
if not DRAW_PA_STRUCTURES then
|
|
break
|
|
end
|
|
local ev = part({
|
|
Name = "PAEVCharger",
|
|
Size = Vector3.new(1.6 * M, 2.2 * M, 0.9 * M),
|
|
CFrame = CFrame.new(osmX(-58.3) + k * 4 * M, DECK_Y + 1.1 * M, osmZ(30.2)),
|
|
Color = Color3.fromRGB(60, 130, 90),
|
|
Material = Enum.Material.Metal,
|
|
})
|
|
ev.Parent = pa
|
|
end
|
|
|
|
-- 段差: the real PA had bumps retrofitted specifically to keep illegally
|
|
-- lowered 車高短 cars out. Deliberately shallow here (~0.1 m) -- enough to
|
|
-- be visible and to unsettle a stiff car, not enough to beach anything.
|
|
for k = -1, 1 do
|
|
local bump = part({
|
|
Name = "PALowCarBump",
|
|
Size = Vector3.new(26 * M, 0.1 * M, 0.8 * M),
|
|
CFrame = CFrame.new(c.X + k * 28 * M, DECK_Y + 0.05 * M, c.Z + APRON_Z / 2 - 4 * M),
|
|
Color = Color3.fromRGB(200, 190, 90),
|
|
Material = Enum.Material.Concrete,
|
|
})
|
|
bump.Parent = pa
|
|
end
|
|
|
|
makeSign("大黒PA DAIKOKU PA", CFrame.new(c + Vector3.new(0, 26, -R + 10)), pa)
|
|
end
|
|
|
|
-- Tokyo landmarks, at their true positions relative to the loop -----------------
|
|
local function buildLandmarks(map: Folder)
|
|
local folder = Instance.new("Folder")
|
|
folder.Name = "Landmarks"
|
|
folder.Parent = map
|
|
|
|
-- Tokyo Tower (-3557, -5638): orange/white lattice, twin decks, antenna
|
|
do
|
|
local base = Vector3.new(-3557, 0, -5638)
|
|
local seg = 12
|
|
local H = 333 / 0.28 / 6 -- scaled for readability at driving speed
|
|
for i = 0, seg - 1 do
|
|
local f0 = i / seg
|
|
local w0 = 90 * (1 - f0) ^ 1.6 + 8
|
|
local h = H / seg
|
|
local col = (i % 2 == 0) and Color3.fromRGB(235, 90, 40) or Color3.fromRGB(240, 240, 245)
|
|
local s = part({
|
|
Name = "TokyoTower",
|
|
Size = Vector3.new(w0, h, w0),
|
|
CFrame = CFrame.new(base + Vector3.new(0, f0 * H + h / 2, 0)),
|
|
Color = col,
|
|
Material = Enum.Material.DiamondPlate,
|
|
Transparency = 0.12,
|
|
})
|
|
s.Parent = folder
|
|
end
|
|
for _, dh in ipairs({ 0.38, 0.66 }) do
|
|
local deck = part({
|
|
Name = "TowerDeck",
|
|
Size = Vector3.new(70, 10, 70),
|
|
CFrame = CFrame.new(base + Vector3.new(0, dh * H, 0)),
|
|
Color = Color3.fromRGB(250, 250, 255),
|
|
Material = Enum.Material.Neon,
|
|
Transparency = 0.15,
|
|
})
|
|
deck.Parent = folder
|
|
end
|
|
local ant = part({
|
|
Name = "TowerAntenna",
|
|
Size = Vector3.new(5, H * 0.22, 5),
|
|
CFrame = CFrame.new(base + Vector3.new(0, H + H * 0.11, 0)),
|
|
Color = Color3.fromRGB(220, 90, 60),
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
ant.Parent = folder
|
|
end
|
|
|
|
-- Tokyo Skytree (17523, 14829): far to the north-east, visible on the horizon
|
|
do
|
|
local base = Vector3.new(17523, 0, 14829)
|
|
local H = 634 / 0.28 / 6
|
|
for i = 0, 9 do
|
|
local f = i / 10
|
|
local w = 70 * (1 - f) ^ 1.3 + 10
|
|
local s = part({
|
|
Name = "Skytree",
|
|
Size = Vector3.new(w, H / 10, w),
|
|
CFrame = CFrame.new(base + Vector3.new(0, f * H + H / 20, 0)),
|
|
Color = Color3.fromRGB(215, 225, 240),
|
|
Material = Enum.Material.Neon,
|
|
Transparency = 0.25,
|
|
})
|
|
s.Parent = folder
|
|
end
|
|
end
|
|
|
|
-- Imperial Palace (-1173, 4933): dark moat ring + green grounds + stone wall
|
|
do
|
|
local c = Vector3.new(-1173, 0, 4933)
|
|
local grounds = part({
|
|
Name = "PalaceGrounds",
|
|
Size = Vector3.new(1500, 6, 1400),
|
|
CFrame = CFrame.new(c + Vector3.new(0, 3, 0)),
|
|
Color = Color3.fromRGB(24, 52, 32),
|
|
Material = Enum.Material.Grass,
|
|
})
|
|
grounds.Parent = folder
|
|
for i = 0, 23 do
|
|
local a = (i / 24) * math.pi * 2
|
|
local moat = part({
|
|
Name = "PalaceMoat",
|
|
Size = Vector3.new(150, 3, 60),
|
|
CFrame = CFrame.new(c + Vector3.new(math.sin(a) * 800, 5, math.cos(a) * 760)) * CFrame.Angles(0, -a, 0),
|
|
Color = Color3.fromRGB(18, 34, 48),
|
|
Material = Enum.Material.Water,
|
|
})
|
|
moat.Parent = folder
|
|
local wall = part({
|
|
Name = "PalaceWall",
|
|
Size = Vector3.new(150, 26, 20),
|
|
CFrame = CFrame.new(c + Vector3.new(math.sin(a) * 720, 13, math.cos(a) * 690))
|
|
* CFrame.Angles(0, -a, 0),
|
|
Color = Color3.fromRGB(90, 88, 80),
|
|
Material = Enum.Material.Slate,
|
|
})
|
|
wall.Parent = folder
|
|
end
|
|
end
|
|
|
|
-- Ginza (2764, -525): bright signage district
|
|
do
|
|
local c = Vector3.new(2764, 0, -525)
|
|
for i = 1, 26 do
|
|
local a = det(i * 3.1) * math.pi * 2
|
|
local r = 260 + det(i * 7.7) * 900
|
|
local h = 260 + det(i * 5.3) * 420
|
|
local p = c + Vector3.new(math.sin(a) * r, h / 2, math.cos(a) * r)
|
|
local b = part({
|
|
Name = "GinzaBlock",
|
|
Size = Vector3.new(150 + det(i) * 90, h, 150 + det(i * 2) * 90),
|
|
CFrame = CFrame.new(p),
|
|
Color = Color3.fromRGB(26, 26, 34),
|
|
Material = Enum.Material.Glass,
|
|
Reflectance = 0.25,
|
|
})
|
|
b.Parent = folder
|
|
local sign = part({
|
|
Name = "GinzaSign",
|
|
Size = Vector3.new(16, h * 0.5, 6),
|
|
CFrame = CFrame.new(p + Vector3.new(80, 0, 0)),
|
|
Color = ({ NEON_PINK, NEON_BLUE, NEON_TEAL, NEON_AMBER })[(i % 4) + 1],
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
sign.Parent = folder
|
|
end
|
|
end
|
|
|
|
-- Shiodome towers (1117, -2847): the glass cluster the C1 threads past
|
|
do
|
|
local c = Vector3.new(1117, 0, -2847)
|
|
for i = 1, 7 do
|
|
local h = 700 + det(i * 11.3) * 500
|
|
local p = c + Vector3.new((det(i * 2.2) - 0.5) * 1100, h / 2, (det(i * 4.4) - 0.5) * 1100)
|
|
local t = part({
|
|
Name = "ShiodomeTower",
|
|
Size = Vector3.new(170, h, 170),
|
|
CFrame = CFrame.new(p),
|
|
Color = Color3.fromRGB(30, 34, 44),
|
|
Material = Enum.Material.Glass,
|
|
Reflectance = 0.35,
|
|
})
|
|
t.Parent = folder
|
|
local crown = part({
|
|
Name = "TowerCrown",
|
|
Size = Vector3.new(175, 8, 175),
|
|
CFrame = CFrame.new(p + Vector3.new(0, h / 2, 0)),
|
|
Color = NEON_BLUE,
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
crown.Parent = folder
|
|
end
|
|
end
|
|
|
|
-- Tokyo Station (3448, 3367): red brick Marunouchi facade
|
|
do
|
|
local c = Vector3.new(3448, 0, 3367)
|
|
local hall = part({
|
|
Name = "TokyoStation",
|
|
Size = Vector3.new(900, 120, 220),
|
|
CFrame = CFrame.new(c + Vector3.new(0, 60, 0)),
|
|
Color = Color3.fromRGB(120, 52, 44),
|
|
Material = Enum.Material.Brick,
|
|
})
|
|
hall.Parent = folder
|
|
for _, sx in ipairs({ -320, 0, 320 }) do
|
|
local dome = part({
|
|
Name = "StationDome",
|
|
Shape = Enum.PartType.Ball,
|
|
Size = Vector3.new(150, 150, 150),
|
|
CFrame = CFrame.new(c + Vector3.new(sx, 140, 0)),
|
|
Color = Color3.fromRGB(60, 74, 70),
|
|
Material = Enum.Material.Slate,
|
|
})
|
|
dome.Parent = folder
|
|
end
|
|
end
|
|
|
|
-- Roppongi Hills (-8715, -4899) + National Diet (-3712, 1280)
|
|
do
|
|
local rh = Vector3.new(-8715, 0, -4899)
|
|
local mori = part({
|
|
Name = "RoppongiHills",
|
|
Size = Vector3.new(260, 1300, 260),
|
|
CFrame = CFrame.new(rh + Vector3.new(0, 650, 0)),
|
|
Color = Color3.fromRGB(34, 36, 46),
|
|
Material = Enum.Material.Glass,
|
|
Reflectance = 0.3,
|
|
})
|
|
mori.Parent = folder
|
|
|
|
local diet = Vector3.new(-3712, 0, 1280)
|
|
local body = part({
|
|
Name = "NationalDiet",
|
|
Size = Vector3.new(700, 130, 260),
|
|
CFrame = CFrame.new(diet + Vector3.new(0, 65, 0)),
|
|
Color = Color3.fromRGB(150, 148, 140),
|
|
Material = Enum.Material.Granite,
|
|
})
|
|
body.Parent = folder
|
|
local tower = part({
|
|
Name = "DietTower",
|
|
Size = Vector3.new(150, 190, 150),
|
|
CFrame = CFrame.new(diet + Vector3.new(0, 190, 0)),
|
|
Color = Color3.fromRGB(150, 148, 140),
|
|
Material = Enum.Material.Granite,
|
|
})
|
|
tower.Parent = folder
|
|
end
|
|
end
|
|
|
|
-- Procedural night city filling the space around the loop -----------------------
|
|
local function buildSkyline(map: Folder)
|
|
local folder = Instance.new("Folder")
|
|
folder.Name = "Skyline"
|
|
folder.Parent = map
|
|
|
|
-- PA keep-out. Returns true if a `w`-wide box centred at (x,z) clears the
|
|
-- Daikoku PA island entirely. RING_OUTER mirrors buildPA's own geometry:
|
|
-- ringRadius (PA_RADIUS + 130) + half the 44-stud ring width.
|
|
local PA_C = Circuit.PA_CENTER
|
|
local RING_OUTER = Circuit.PA_RADIUS + 130 + 22
|
|
local PA_MARGIN = 60
|
|
local function paKeepout(x: number, z: number, w: number): boolean
|
|
local dx, dz = x - PA_C.X, z - PA_C.Z
|
|
local r = math.sqrt(dx * dx + dz * dz)
|
|
return r - w / 2 > RING_OUTER + PA_MARGIN
|
|
end
|
|
|
|
local samples = Circuit.samples
|
|
local n = #samples
|
|
local placed = 0
|
|
|
|
for i = 1, 900 do
|
|
local a = det(i * 1.7) * math.pi * 2
|
|
local r = 900 + det(i * 2.9) ^ 0.6 * 9000
|
|
local x = math.sin(a) * r - 477
|
|
local z = math.cos(a) * r - 473
|
|
|
|
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.
|
|
-- Use the WIDEST deck (3+3 Shiodome section), otherwise buildings sited
|
|
-- against the 2+2 width would intrude into the widened carriageway.
|
|
local CLEAR = MAX_ROAD_WIDTH / 2 + 60
|
|
local near = math.huge
|
|
for k = 1, n do
|
|
local s = samples[k].pos
|
|
local d = (Vector3.new(s.X, 0, s.Z) - Vector3.new(x, 0, z)).Magnitude
|
|
if d < near then
|
|
near = d
|
|
end
|
|
end
|
|
if near - w / 2 < CLEAR then
|
|
continue
|
|
end
|
|
|
|
-- ...but `samples` is the MAINLINE centreline ONLY. The PA ring road,
|
|
-- its two ramps and the parking apron are generated separately (see
|
|
-- buildPA) and appear nowhere in `samples`, so the test above is blind
|
|
-- to the entire PA island. A 186-wide tower landed at r=428 from
|
|
-- PA_CENTER, spanning radius 335->521 and punching straight through the
|
|
-- ring road band [368,412] -- a skyscraper standing in the road.
|
|
--
|
|
-- Exclude the whole PA disc, not just the ring band: nothing generated
|
|
-- belongs inside the ring at all. Test the building's INNER FACE
|
|
-- (r - w/2) against the ring's outer kerb + the same 60-stud margin.
|
|
if not paKeepout(x, z, w) then
|
|
continue
|
|
end
|
|
|
|
local b = part({
|
|
Name = "Building",
|
|
Size = Vector3.new(w, h, w),
|
|
CFrame = CFrame.new(x, h / 2, z),
|
|
Color = Color3.fromRGB(18 + det(i) * 16, 18 + det(i * 2) * 16, 26 + det(i * 3) * 20),
|
|
Material = Enum.Material.Glass,
|
|
Reflectance = 0.18,
|
|
})
|
|
b.Parent = folder
|
|
placed += 1
|
|
|
|
-- signs hang off the +X face, so they need their own clearance check
|
|
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 and paKeepout(sx, z, 12) then
|
|
local sign = part({
|
|
Name = "NeonSign",
|
|
Size = Vector3.new(12, h * 0.42, 5),
|
|
CFrame = CFrame.new(sx, h * 0.62, z),
|
|
Color = ({ NEON_PINK, NEON_BLUE, NEON_TEAL, NEON_AMBER })[(i % 4) + 1],
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
sign.Parent = folder
|
|
end
|
|
end
|
|
if placed > 620 then
|
|
break
|
|
end
|
|
end
|
|
|
|
-- ground plane under the whole city
|
|
local ground = part({
|
|
Name = "Ground",
|
|
Size = Vector3.new(26000, 4, 26000),
|
|
CFrame = CFrame.new(-477, -2, -473),
|
|
Color = Color3.fromRGB(14, 14, 18),
|
|
Material = Enum.Material.Asphalt,
|
|
})
|
|
ground.Parent = folder
|
|
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(widthAt(i) + 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(widthAt(i) + 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
|
|
local existing = Workspace:FindFirstChild("Map")
|
|
if existing then
|
|
existing:Destroy()
|
|
end
|
|
|
|
local map = Instance.new("Folder")
|
|
map.Name = "Map"
|
|
|
|
buildRoad(map)
|
|
buildJunctionSigns(map)
|
|
buildPA(map)
|
|
buildLandmarks(map)
|
|
buildSkyline(map)
|
|
buildBridges(map)
|
|
|
|
-- Start/finish across the road at sample 1
|
|
local s1 = Circuit.samples[1]
|
|
local fwd = Circuit.forwardAt(1)
|
|
local line = part({
|
|
Name = "FinishLine",
|
|
Size = Vector3.new(widthAt(1), 0.6, 8),
|
|
CFrame = CFrame.lookAt(s1.pos + Vector3.new(0, 1.4, 0), s1.pos + Vector3.new(0, 1.4, 0) + fwd, UP),
|
|
Color = Color3.fromRGB(250, 250, 255),
|
|
Material = Enum.Material.Neon,
|
|
})
|
|
line.Parent = map
|
|
|
|
map.Parent = Workspace
|
|
return map
|
|
end
|
|
|
|
return MapBuilder
|