Aux-lane suppression + PA geometry fixes; probe/verify tooling
CI / quality (push) Failing after 1m9s

MapBuilder.luau
  - Suppress the aux lane across samples 525-550 (AUX_SUPPRESS_FROM/TO),
    honoring the user's hand deletion in Studio so a rebuild no longer
    regenerates it.
  - Smoothstep the width ramp over AUX_BLEND=5 samples on each side. A hard
    cut produced a 26-stud lateral fence jog; the blend removes it.
  - DO NOT remove this suppression. Re-enabling it resurrects geometry the
    user deleted by hand.

rokit.toml
  - rojo 7.5.1 -> 7.7.0 to match the auto-updating Studio plugin.
    Server older than plugin fails the handshake with
    "attempt to index number with 'protocolVersion'".

tools/
  - 34 read-only probe/verify/capture scripts used to measure the datamodel
    instead of reasoning from source.

Verified-good baseline at this commit:
  total parts       16788
  PAWestSlip        60 segs, 34.4 deg total bend, worst 1.4, 0 kinks >5
  PAEastSlip        60 segs, 30.7 deg total bend, worst 1.3, 0 kinks >5
  AuxLane parts     30 (samples 525-550 deliberately absent)
  barrier coverage  823 samples, 0 unguarded
  fence jogs >10st  0
This commit is contained in:
2026-08-06 08:27:47 -04:00
parent c297b20aed
commit 6e50f9718d
36 changed files with 3723 additions and 1 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[tools] [tools]
rojo = "rojo-rbx/rojo@7.5.1" rojo = "rojo-rbx/rojo@7.7.0"
wally = "UpliftGames/[email protected]" wally = "UpliftGames/[email protected]"
selene = "Kampfkarren/[email protected]" selene = "Kampfkarren/[email protected]"
stylua = "JohnnyMorganz/[email protected]" stylua = "JohnnyMorganz/[email protected]"
+539
View File
@@ -95,6 +95,187 @@ local ZEBRA_PITCH = 1.0 * M
-- Gore hatching + speed-change-lane boundary lines only make sense where a -- Gore hatching + speed-change-lane boundary lines only make sense where a
-- ramp actually diverges. Tied to BUILD_PA_RAMPS in buildPA (2026-08-03). -- ramp actually diverges. Tied to BUILD_PA_RAMPS in buildPA (2026-08-03).
local RAMP_GORE_MARKINGS = false local RAMP_GORE_MARKINGS = false
-- PA SLIP ROADS (2026-08-03, fifth pass) --------------------------------------
-- Declared up here rather than next to buildPARamps because buildRoad has to
-- know where the ramps meet the expressway BEFORE it builds the parapets --
-- otherwise the slip mouth is sealed off by the mainline's own barrier and kill
-- wall and the ramp dead-ends into concrete.
--
-- `join` = mainline sample where the slip meets the expressway (the mouth).
-- `land` = mainline sample the slip is abreast of when it reaches the apron.
-- The run is |land - join| samples long, ~60 studs each. Both are kept at 12
-- samples (~720 studs) so the 56-stud drop stays at 7.6% average / 11.4% peak
-- under the eased profile -- steep-ish but well inside what the cars climb.
local PA_SLIP = {
{ name = "PAWestSlip", join = 525, land = 537 },
{ name = "PAEastSlip", join = 551, land = 539 },
}
-- Which edge of the deck the slips leave through. PA_CENTER sits at Z -7469.6
-- and the mainline crosses the lot's north strip at Z ~ -7142, so the lot is on
-- the -Z side; with the roadway heading east there, right = +Z, hence -1.
local PA_SLIP_SIDE = -1
-- Samples of bare deck edge at the ramp nose itself, where the ramp deck has
-- taken over and neither the mainline parapet nor the aux-lane parapet belongs.
-- Kept at 1: the ramp leaves at a very shallow angle, so by one sample past the
-- mouth its inner edge is still within a stud of the deck edge.
local PA_SLIP_NOSE = 1
-- SPEED-CHANGE (AUXILIARY) LANES ---------------------------------------------
-- FIXED 2026-08-03: the slips used to start at lat = 0 -- dead on the mainline
-- CENTRELINE -- so a 26-stud ramp deck speared straight through the running
-- lanes to reach the deck edge. A real interchange never does that: the ramp
-- gets its OWN lane alongside the mainline so traffic can accelerate to speed
-- before merging (on-ramp) or drop out of the traffic stream before slowing
-- (off-ramp). That lane is what was missing.
--
-- The aux lane is a full-width lane bolted to the outside of the deck, level
-- with it, tapering back into the deck edge at its far end. Measured from the
-- mouth (sample `join`) along the merge direction:
-- d in [-NOSE .. 0] ramp nose; the ramp deck covers this edge
-- d in ( 0 .. SPAN] full-width speed-change lane
-- d in (SPAN .. SPAN+TAPER] lane tapering away into the deck edge
-- The ramp body runs the OTHER way (join -> land), so ramp and aux lane meet
-- end-to-end AT the mouth at the same offset and width -- one continuous path
-- (lot -> ramp -> aux lane -> merge), never a crossing.
local PA_AUX_W = 26 -- matches the slip deck exactly, so the join is flush
-- 2026-08-04: mainline sample range where the aux/weaving lane is suppressed,
-- matching the run the user deleted by hand in Studio. Both the lane itself and
-- the parapet opening are keyed off this, so the deck edge stays fenced.
--
-- 2026-08-04 (2nd pass): widened from 527..548 to the TRUE aux boundary.
-- Measured: the aux lane tapers to zero at 525 and does not resume until 551,
-- but the suppression range started at 527 -- so samples 525,526,549,550 had
-- neither an aux lane to carry the relocated parapet NOR a forced-closed
-- mainline parapet. Result: a 4-sample hole (the user's left oval). Covering
-- 525..550 closes it and, because the parapet now switches back to the
-- mainline edge exactly where the aux lane actually ends, it also removes the
-- 26-stud lateral snap at 527/549 that made the barrier visibly stick out
-- (the user's right circle).
local AUX_SUPPRESS_FROM = 525
local AUX_SUPPRESS_TO = 550
-- Samples over which the aux lane (and therefore its outer fence) is faded to
-- zero on each approach to the suppressed range, instead of being cut off in a
-- single sample. A hard cut teleported the fence 26 studs inboard and left the
-- protruding stub the user circled in their screenshot.
local AUX_BLEND = 5
-- 1 outside the suppressed range, 0 inside it, smoothstepped across AUX_BLEND
-- samples on each approach so the fence walks in instead of snapping.
local function auxSuppressFactor(i: number): number
if i >= AUX_SUPPRESS_FROM and i <= AUX_SUPPRESS_TO then
return 0
end
local function ss(u: number): number
u = math.clamp(u, 0, 1)
return u * u * (3 - 2 * u)
end
-- approaching the start of the suppressed run from upstream
local dIn = AUX_SUPPRESS_FROM - i
if dIn > 0 and dIn <= AUX_BLEND then
return ss(dIn / AUX_BLEND)
end
-- leaving the end of the suppressed run downstream
local dOut = i - AUX_SUPPRESS_TO
if dOut > 0 and dOut <= AUX_BLEND then
return ss(dOut / AUX_BLEND)
end
return 1
end
local PA_AUX_SPAN = 10 -- ~600 studs of full-width lane to get up to speed
local PA_AUX_TAPER = 6 -- ~360 studs of taper back into the deck edge
-- DECELERATION SIDE (the mirror of the above, added 2026-08-03).
-- The accel lane only serves traffic JOINING the mainline. Traffic LEAVING it
-- had no lane at all: a car had to brake in the running lanes and turn off
-- cold, which is exactly the original defect pointed the other way. So the
-- same mouth also gets a lane on the UPSTREAM side, measured with e = -d - NOSE
-- (distance back up the mainline from the ramp nose):
-- e in [0 .. DEC_SPAN] full-width deceleration lane
-- e in (DEC_SPAN .. +DEC_TAPER] lane tapering out of the deck edge
-- Read in the direction of travel that is the reverse: the lane opens out of
-- the deck edge, runs full width, and delivers the car to the ramp nose having
-- already left the traffic stream. Deceleration needs less room than
-- acceleration (braking beats spooling up), hence the shorter span.
local PA_DEC_SPAN = 8 -- ~480 studs of full-width lane to shed speed in
local PA_DEC_TAPER = 6 -- ~360 studs of taper opening out of the deck edge
-- Signed distance in samples from a slip mouth, measured along that slip's
-- merge direction. Wrap-safe around the end of the sample array.
local function auxOffsetOf(i: number, slip: { join: number, land: number }): number
local n = #Circuit.samples
local raw = (i - slip.join) % n
if raw > n / 2 then
raw -= n
end
-- The ramp descends join -> land, so the speed-change lane runs the other
-- way; that is what puts them end-to-end instead of on top of each other.
local dir = (slip.land > slip.join) and -1 or 1
local d = raw * dir
-- OFF-BY-ONE (caught by measurement 2026-08-03): a deck segment with index
-- i spans [i, i+1], so "the segment at sample i" is not symmetric about the
-- mouth. When the aux lane runs toward INCREASING indices the ramp covers
-- segments below the join and the aux must start AT the join; when it runs
-- toward decreasing indices the ramp covers the join itself. Without this
-- shift the east slip left a ~60-stud hole between ramp and aux lane
-- (measured: 88.2 stud mouth->aux gap vs 36.2 on the west).
if dir == 1 then
d += 1
end
return d
end
-- Aux lane width at mainline sample i, and whether i is the bare ramp nose.
-- Covers BOTH speed-change lanes at each mouth: the deceleration lane upstream
-- of the ramp nose and the acceleration lane downstream of it. Returns 0 when
-- this sample has no speed-change lane.
--
-- OVERLAP (measured 2026-08-03): the two mouths are only 26 samples apart, so
-- one mouth's accel taper lands on the same samples as the next mouth's decel
-- lane. Returning the FIRST slip that matches left one-segment holes in the
-- middle of continuous pavement (measured: gaps at samples 540 and 549). Take
-- the WIDEST claim across all slips instead -- where an accel taper and a decel
-- lane overlap the result is a continuous full-width weaving section, which is
-- what the real road does between closely spaced ramps.
local function auxInfoAt(i: number): (number, boolean)
local best, bestNose = 0, false
for _, slip in ipairs(PA_SLIP) do
local d = auxOffsetOf(i, slip)
local w, nose = 0, false
if d >= -PA_SLIP_NOSE and d <= PA_AUX_SPAN + PA_AUX_TAPER then
if d <= 0 then
w, nose = PA_AUX_W, true
elseif d <= PA_AUX_SPAN then
w = PA_AUX_W
else
local u = (d - PA_AUX_SPAN) / PA_AUX_TAPER
w = PA_AUX_W * (1 - u * u * (3 - 2 * u))
end
else
-- Deceleration lane: upstream of the nose, so d < -NOSE. Measure
-- how far back up the mainline we are and mirror the accel profile.
local e = -d - PA_SLIP_NOSE
if e > 0 and e <= PA_DEC_SPAN + PA_DEC_TAPER then
if e <= PA_DEC_SPAN then
w = PA_AUX_W
else
local u = (e - PA_DEC_SPAN) / PA_DEC_TAPER
w = PA_AUX_W * (1 - u * u * (3 - 2 * u))
end
end
end
-- A nose claim wins outright: the ramp deck owns that edge, and
-- building aux pavement there would double up on the ramp.
if nose then
return w, true
end
if w > best then
best = w
end
end
return best, bestNose
end
local MARK_Y = 0.1 -- sits just proud of the deck to avoid z-fighting local MARK_Y = 0.1 -- sits just proud of the deck to avoid z-fighting
local MARK_H = 2.1 local MARK_H = 2.1
-- Heading change per 60-stud sample above which the curve counts as tight -- Heading change per 60-stud sample above which the curve counts as tight
@@ -546,6 +727,27 @@ local function planPA()
end end
end end
end end
-- SLIP MOUTHS (2026-08-03). The old polar ramps stay off, but the new
-- linear slips are real and DO need the outer parapet opened where they
-- meet the deck -- otherwise the mouth is walled shut and the ramp is
-- decorative.
--
-- UPDATED: the opening now tracks the SPEED-CHANGE LANE, not just the
-- mouth. Wherever an aux lane exists the mainline's own parapet and kill
-- wall would sit right in the middle of the running surface (the aux lane
-- is bolted onto the outside of that edge), so the edge has to be open for
-- the whole aux run -- and buildRoad re-fences the OUTER side of the aux
-- lane instead, so there is still never an unguarded drop.
for i = 1, n do
-- Same suppression the deck builder applies, so rampOpen agrees with
-- where the aux lane actually exists. Without this the parapet would be
-- opened across the suppressed run with no aux lane to re-fence it.
local w = auxInfoAt(i) * auxSuppressFactor(i)
if w > 0.5 then
rampOpen[i] = true
rampOpenSide[i] = PA_SLIP_SIDE
end
end
PAPlan = { PAPlan = {
nearIdx = bestIdx, nearIdx = bestIdx,
-- Kept for anything still referencing the old two-ramp names; they now -- Kept for anything still referencing the old two-ramp names; they now
@@ -641,6 +843,92 @@ local function buildRoad(map: Folder)
fence.Parent = struct fence.Parent = struct
end end
-- SPEED-CHANGE LANE ---------------------------------------------------
-- Where a slip meets the deck, the ramp does NOT merge straight into
-- the running lanes. It feeds this auxiliary lane, bolted to the
-- outside of the carriageway at deck level, which runs alongside the
-- mainline and tapers away -- so traffic accelerates to speed here and
-- merges laterally, the way a real interchange works.
--
-- The mainline's own parapet/kill wall is opened along this stretch
-- (planPA marks it), so the aux lane re-fences its OUTER edge below;
-- the edge is never left unguarded.
local auxW, auxIsNose = auxInfoAt(i)
-- 2026-08-04: the user deleted the middle weaving run by hand in Studio.
-- That is the stretch BETWEEN the two mouths where one mouth's accel
-- taper overlaps the next mouth's decel lane, producing a continuous
-- full-width aux lane. They do not want it. Suppress it in source so
-- rebuilds stop resurrecting it, and so the mainline parapet is NOT
-- opened along here (see planPA) -- otherwise removing the lane leaves
-- the deck edge bare, which was the measured barrier gap.
--
-- 2026-08-04 (3rd pass): a HARD cut here made the OUTER FENCE snap.
-- The aux fence rides at HALF + auxW, so zeroing auxW in one sample
-- teleported the parapet 26 studs inboard, leaving the protruding stub
-- the user circled. Real merges never do that -- they taper. So instead
-- of chopping, ramp auxW down to zero across AUX_BLEND samples on each
-- approach to the suppressed range. The fence then walks in smoothly
-- with pavement under it the whole way, and the full-width lane the
-- user actually deleted still never comes back.
auxW = auxW * auxSuppressFactor(i)
if auxW < 0.5 then
auxW, auxIsNose = 0, false
end
if auxW > 0 and not auxIsNose then
local aoff = right * (PA_SLIP_SIDE * (HALF + auxW / 2))
local acf = CFrame.lookAt(mid + aoff, mid + aoff + dir, UP)
local auxDeck = part({
Name = "AuxLane_" .. i,
Size = Vector3.new(auxW, 2, len),
CFrame = acf,
Color = C_ASPHALT,
Material = Enum.Material.Asphalt,
})
auxDeck.Parent = road
-- 車道外側線 down the outer edge of the speed-change lane.
local auxEdge = part({
Name = "EdgeLine",
Size = Vector3.new(EDGE_LINE_W, MARK_H, len),
CFrame = acf * CFrame.new(PA_SLIP_SIDE * (auxW / 2 - 1.6), MARK_Y, 0),
Color = C_PAINT_WHITE,
Material = Enum.Material.SmoothPlastic,
})
auxEdge.Parent = road
end
-- Outer fence for the speed-change lane. Follows the taper, so the
-- parapet closes back onto the deck edge at the end of the merge.
if auxW > 0 and not auxIsNose then
local foff = right * (PA_SLIP_SIDE * (HALF + auxW - BARRIER_T / 2))
local fcf = CFrame.lookAt(mid + foff, mid + foff + dir, UP)
local auxBarrier = part({
Name = "Barrier",
Size = Vector3.new(BARRIER_T, BARRIER_H, len),
CFrame = fcf * CFrame.new(0, 1 + BARRIER_H / 2, 0),
Color = C_PARAPET,
Material = Enum.Material.Concrete,
})
auxBarrier.Parent = struct
local auxRail = part({
Name = "EdgeRail",
Size = Vector3.new(0.6, RAIL_H, len),
CFrame = fcf * CFrame.new(0, 1 + BARRIER_H + RAIL_H / 2, 0),
Color = C_RAIL,
Material = Enum.Material.Metal,
})
auxRail.Parent = struct
local koff = right * (PA_SLIP_SIDE * (HALF + auxW + 1))
local auxKill = part({
Name = "KillWall",
Size = Vector3.new(2, WALL_HEIGHT, len + 6),
CFrame = CFrame.lookAt(mid + koff, mid + koff + dir, UP) * CFrame.new(0, WALL_HEIGHT / 2, 0),
Transparency = 1,
CanCollide = true,
})
auxKill.Parent = struct
end
-- LANE MARKINGS ----------------------------------------------------- -- LANE MARKINGS -----------------------------------------------------
-- Lane count varies by section (see SECTION PROFILE above): most of the -- 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 -- C1 is 2+2, but 汐留JCT-浜崎橋JCT is 3+3. A 3-lane carriageway needs
@@ -734,6 +1022,15 @@ local function buildRoad(map: Folder)
-- rather than re-deriving it from geometry that no longer implies it. -- rather than re-deriving it from geometry that no longer implies it.
local junction = plan.rampOpen[i] local junction = plan.rampOpen[i]
local paSide = junction and (plan.rampOpenSide[i] or 0) or 0 local paSide = junction and (plan.rampOpenSide[i] or 0) or 0
-- 2026-08-04: the aux lane is suppressed across AUX_SUPPRESS_FROM..TO
-- (the run the user deleted). The parapet is normally OPENED along a
-- speed-change lane because the aux lane re-fences the outer edge --
-- but with no aux lane there is nothing to re-fence with, leaving the
-- deck edge bare. That is the measured barrier gap at samples 527..548.
-- Force the parapet CLOSED here so the mainline keeps its own edge.
if i >= AUX_SUPPRESS_FROM and i <= AUX_SUPPRESS_TO then
junction, paSide = nil, 0
end
for _, side in ipairs({ -1, 1 }) do for _, side in ipairs({ -1, 1 }) do
if junction and side == paSide then if junction and side == paSide then
continue continue
@@ -2905,6 +3202,247 @@ local function buildBridges(map: Folder)
end end
end end
-- PA CONNECTOR RAMPS (2026-08-03, FIFTH pass -- ordinary slip roads) ---------
--
-- WHAT WAS WRONG WITH EVERY EARLIER PASS. All four previous attempts described
-- the ramp in POLAR terms -- a gate on the PA ring at bearing `az`, swept
-- `sweep` degrees at radius `forkR`. That formulation can only ever produce
-- something that wraps around the lot, so each "fix" just slid a big curve to a
-- new place: helix, then spiral, then a 110 deg / R=420 / 806-stud arc. The
-- user's drawing is not polar at all. It is LINEAR: peel off the shoulder at a
-- shallow angle, run PARALLEL to the mainline, drop onto the apron. Confirmed
-- verbatim: "Just like most on and off ramps."
--
-- So this pass throws the angle ladder away and defines each ramp as an OFFSET
-- CURVE of the mainline itself: at every step, take the mainline centreline at
-- fractional sample `f`, push it `lateral(t)` studs to the PA side, and set the
-- height to `y(t)`. Because `lateral` eases with zero derivative at both ends,
-- the ramp is automatically TANGENT to the expressway at the join (no kink to
-- fix by hand) and lands parallel to it, which is what both drawings show.
--
-- FOUR MARKS, TWO CORRIDORS. The top-down markup has four red lines: a pair at
-- each end, each pair converging to ONE point on the highway with opposed
-- arrows. Opposed arrows meeting at a single mouth = one two-way ramp, not two
-- one-way ones. So this builds TWO corridors (west and east), each carrying
-- both directions on one deck -- which is exactly how a real PA/rest-area
-- connector is laid out, and reproduces all four drawn lines.
--
-- Measured anchors (probe_slip_envelope.luau, live Edit DataModel, 823 samples):
-- mainline deck top Y = 103 (Road_* centre 102, 2 thick)
-- PA apron top Y = 47 (PAFloor centre 46, 2 thick)
-- drop 56 (== Circuit.PA_LIFT, so it checks out)
-- mainline is STRAIGHT across the frontage: samples 525..545 all heading -6
-- PA footprint X[-4538..-3710] Z[-7884..-7056]
-- mainline crosses at Z ~ -7142, i.e. over the lot's north strip
--
-- The joins sit OUTSIDE the lot footprint (sample 525 is X=-4971, west of the
-- slab; sample 551 is X~-3450, east of it) and the landings sit inside it --
-- which is what the drawing shows, the marks running past the slab edge before
-- meeting the road.
--
-- The PA_SLIP table itself lives at the top of the file, because buildRoad has
-- to read it to open the parapet at each mouth before this ever runs.
--
-- OLD ARC VERSION (kept only as a record of what the drawing is NOT):
--
-- The two earlier attempts tried to reproduce Daikoku's famous spiral loops and
-- both failed (wrong shape, then wandering off over open water). `BUILD_PA_RAMPS`
-- above stays false; this is a deliberately plain replacement drawn from the
-- user's own markup: ONE ramp down off the mainline into the lot, ONE ramp back
-- up. No helix, no concentric wrapping -- just two constant-radius arcs.
--
-- Measured anchors (from the live Studio DataModel, not estimated):
-- mainline deck top Y = 103 (Road_* centre 102, 2 thick)
-- PA apron top Y = 47 (PAFloor centre 46, 2 thick)
-- drop 56 (== Circuit.PA_LIFT, so it checks out)
-- mainline crosses the PA north strip at samples 531..546
-- PA footprint X[-4538..-3710] Z[-7884..-7056]
--
-- Each ramp is a circular arc of radius R swept 110 degrees. The arc centre is
-- placed R studs off the mainline on the -Z side, so at the mainline end the
-- arc is exactly TANGENT to the road -- the ramp leaves (and rejoins) straight,
-- with no kink at the join. R = 420 gives 806 studs of run for the 56 stud
-- drop: 6.9% average grade, ~10.4% at the steepest point of the eased profile.
local function buildPARamps(map: Folder)
local ramps = Instance.new("Folder")
ramps.Name = "PARamps"
ramps.Parent = map
local samples = Circuit.samples
local n = #samples
local WIDTH = 26 -- two 11.6-stud lanes plus shoulders
local DECK_TOP = 103 -- mainline surface (Road_* centre 102, 2 thick)
local PA_TOP = 47 -- apron surface (PAFloor centre 46, 2 thick)
local STEPS = 60 -- subdivisions per slip
-- Lateral offset from the mainline centreline at the far (apron) end. The
-- deck half-width is 30, the slip half-width is 13, so 78 clears the
-- mainline's kill wall (HALF + 1 = 31) by a comfortable 34 studs and puts
-- the slip well clear of traffic before it starts dropping.
local OFFSET = 78
-- Smoothstep: zero derivative at t=0 and t=1. Used for BOTH the lateral
-- pull-away and the descent, which is what makes the slip tangent to the
-- mainline at the mouth (leaves straight, no kink) and flush/level with the
-- apron at the bottom (no lip to catch a car).
local function ease(t: number): number
return t * t * (3 - 2 * t)
end
-- Mainline centreline and heading at a FRACTIONAL sample index, so the slip
-- can be subdivided finer than the 60-stud sample spacing.
local function centreAt(f: number): (Vector3, Vector3)
local i0 = math.floor(f)
local frac = f - i0
local a = samples[((i0 - 1) % n) + 1]
local b = samples[(i0 % n) + 1]
local pos = a.pos:Lerp(b.pos, frac)
local d = b.pos - a.pos
local flat = Vector3.new(d.X, 0, d.Z)
flat = flat.Magnitude > 0.01 and flat.Unit or Vector3.xAxis
return pos, flat
end
for _, slip in ipairs(PA_SLIP) do
-- Walk from the mainline join toward the landing sample. `step` is +1
-- or -1 so the west slip runs east and the east slip runs west, which
-- is what makes them converge on the lot from opposite ends -- the
-- four marks in the drawing.
local span = slip.land - slip.join
local prev: Vector3? = nil
-- FIXED 2026-08-03: the lateral sweep used to start at 0 -- the
-- mainline CENTRELINE -- so the ramp deck cut clean across the running
-- lanes on its way to the deck edge. It now starts at the centreline
-- of the speed-change lane, i.e. exactly where the aux lane ends, so
-- the ramp is an end-to-end continuation of its own lane and never
-- crosses mainline traffic.
local joinIdx = ((slip.join - 1) % n) + 1
local INNER = widthAt(joinIdx) / 2 + PA_AUX_W / 2
-- 2026-08-04: SMOOTHING PASS. The path used to be laid down segment by
-- segment straight off centreAt(), so it inherited every wobble in the
-- mainline it was tracking. That is fine for the west slip (its span is
-- a gentle 13.1deg) but ruinous for the east one: the mainline turns
-- 34.7deg across samples 539..551, with 9.2/8.0/8.2deg steps right at
-- the mouth, and the slip folded that into 58.2 and 50.1deg hinges --
-- an earthquake kink. Measured east total heading change: 216deg
-- against the west's 56.5deg, on an almost identical 709 vs 717 stud
-- chord. So: build the raw polyline FIRST, relax it, then lay parts
-- along the relaxed line. Laplacian relaxation on XZ only -- the Y
-- profile is already eased and must keep its parallel-run shape.
local raw = {}
for k = 0, STEPS do
local t = k / STEPS
local f = slip.join + span * t
local c, fwd = centreAt(f)
local right = fwd:Cross(UP)
local latT = math.clamp(t / 0.30, 0, 1)
local lat = INNER + (OFFSET - INNER) * ease(latT)
local dropT = math.clamp((t - 0.30) / (1 - 0.30), 0, 1)
local yy = DECK_TOP - (DECK_TOP - PA_TOP) * ease(dropT)
local p = c + right * (PA_SLIP_SIDE * lat)
raw[k + 1] = Vector3.new(p.X, yy - 1, p.Z)
end
-- Relax. Only the true endpoints are pinned: the mouth stays welded to
-- the deck edge and the landing stays on the apron. Pinning index 2 as
-- well (first attempt) held the outgoing tangent rigid and produced a
-- 7.5deg kink at segment 2 where the pinned run met the relaxed run --
-- the pin became its own hinge. Letting index 2 move removes it.
local LAMBDA, PASSES = 0.5, 60
for _ = 1, PASSES do
local next_ = {}
for j = 1, #raw do
if j == 1 or j == #raw then
next_[j] = raw[j]
else
local avg = (raw[j - 1] + raw[j + 1]) * 0.5
local mv = (avg - raw[j]) * LAMBDA
-- XZ only: preserve the eased vertical profile exactly.
next_[j] = Vector3.new(raw[j].X + mv.X, raw[j].Y, raw[j].Z + mv.Z)
end
end
raw = next_
end
for k = 0, STEPS do
local cur = raw[k + 1]
if prev then
local delta = cur - prev
local len = delta.Magnitude
if len > 0.5 then
local mid = (prev + cur) / 2
local segCF = CFrame.lookAt(mid, mid + delta.Unit, UP)
local deck = part({
Name = slip.name .. "_Deck_" .. k,
Size = Vector3.new(WIDTH, 2, len + 1.2),
CFrame = segCF,
Color = C_ASPHALT,
Material = Enum.Material.Asphalt,
})
deck.Parent = ramps
-- Centre line: this is a two-way connector (the drawing's
-- opposed arrows), so it gets a dividing line, not dashes.
local centreLine = part({
Name = slip.name .. "_CentreLine",
Size = Vector3.new(LANE_LINE_W, MARK_H, len + 1.2),
CFrame = segCF * CFrame.new(0, 0, 0),
Color = C_PAINT_WHITE,
Material = Enum.Material.SmoothPlastic,
})
centreLine.Parent = ramps
-- Parapet + kill wall down both edges. The kill wall is the
-- same invisible backstop the mainline uses -- without it a
-- car at Wangan speed tunnels straight through a 3.2-stud
-- concrete lip and falls 56 studs to the apron.
for _, side in ipairs({ -1, 1 }) do
local wall = part({
Name = slip.name .. "_Wall_" .. k,
Size = Vector3.new(1.2, 3.2, len + 1.2),
CFrame = segCF * CFrame.new(side * (WIDTH / 2 - 0.6), 2.6, 0),
Color = C_PARAPET,
Material = Enum.Material.Concrete,
})
wall.Parent = ramps
local kill = part({
Name = "KillWall",
Size = Vector3.new(2, WALL_HEIGHT, len + 6),
CFrame = segCF * CFrame.new(side * (WIDTH / 2 + 1), WALL_HEIGHT / 2, 0),
Transparency = 1,
CanCollide = true,
})
kill.Parent = ramps
end
-- Pillars from the deck underside down to the apron, every
-- 5th step. Skipped where the gap is small (near the top of
-- the run the slip is still tucked under the mainline).
if k % 5 == 0 then
local h = mid.Y - 1 - PA_TOP
if h > 4 then
local col = part({
Name = slip.name .. "_Pillar_" .. k,
Size = Vector3.new(7, h, 7),
CFrame = CFrame.new(mid.X, PA_TOP + h / 2, mid.Z),
Color = C_CONCRETE,
Material = Enum.Material.Concrete,
})
col.Parent = ramps
end
end
end
end
prev = cur
end
end
end
function MapBuilder.build(): Folder function MapBuilder.build(): Folder
local existing = Workspace:FindFirstChild("Map") local existing = Workspace:FindFirstChild("Map")
if existing then if existing then
@@ -2917,6 +3455,7 @@ function MapBuilder.build(): Folder
buildRoad(map) buildRoad(map)
buildJunctionSigns(map) buildJunctionSigns(map)
buildPA(map) buildPA(map)
buildPARamps(map)
buildLandmarks(map) buildLandmarks(map)
buildSkyline(map) buildSkyline(map)
buildBridges(map) buildBridges(map)
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Capture the speed-change (aux) lanes ANCHORED to the parts themselves.
Hardcoded camera offsets are how you end up photographing empty air after the
geometry moves. Every shot here is framed from the LIVE position of the aux
lane / slip parts queried out of the DataModel at capture time, so the camera
cannot drift away from the thing under test.
"""
import shutil
import sys
import time
sys.path.insert(0, "/tmp")
import rbx # noqa: E402
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
# Ask Studio where the aux lanes actually are, then frame from that.
# Returns "x y z | x y z ..." anchors: west aux mid, east aux mid, and the
# two slip mouths.
ANCHOR_CODE = """
local map = workspace:FindFirstChild("Map")
if not map then return "NOMAP" end
local aux = {}
local mouths = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local ai = d.Name:match("^AuxLane_(%d+)$")
if ai then table.insert(aux, {i = tonumber(ai), p = d.Position}) end
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k and tonumber(k) <= 1 then mouths[s] = d.Position end
end
end
end
if #aux == 0 then return "NOAUX" end
table.sort(aux, function(a, b) return a.i < b.i end)
-- Split the aux segments into two clusters (west run and east run) by index gap.
local groups, cur = {}, {aux[1]}
for k = 2, #aux do
if aux[k].i - aux[k - 1].i > 3 then
table.insert(groups, cur); cur = {}
end
table.insert(cur, aux[k])
end
table.insert(groups, cur)
local parts = {}
for _, g in ipairs(groups) do
local mid = g[math.max(1, math.floor(#g / 2))].p
table.insert(parts, ("%.1f %.1f %.1f"):format(mid.X, mid.Y, mid.Z))
end
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local m = mouths[s]
if m then table.insert(parts, ("%.1f %.1f %.1f"):format(m.X, m.Y, m.Z)) end
end
return table.concat(parts, " | ")
"""
CAM_TPL = """
local cam = workspace.CurrentCamera
cam.CameraType = Enum.CameraType.Scriptable
cam.FieldOfView = {fov}
cam.CFrame = CFrame.lookAt(
Vector3.new({px}, {py}, {pz}),
Vector3.new({tx}, {ty}, {tz})
)
return "cam ok"
"""
def grab(name, px, py, pz, tx, ty, tz, fov=55):
code = CAM_TPL.format(fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz)
r = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": code})
if isinstance(r, dict) and r.get("isError"):
print(f"{name}: CAM FAIL {r}")
return None
time.sleep(1.5)
cap = rbx.call("screen_capture", {"capture_id": "ScreenCapture_1"}, wait=22, timeout=300)
imgs = cap.get("images") if isinstance(cap, dict) else None
if not imgs:
print(f"{name}: CAPTURE FAIL {str(cap)[:160]}")
return None
dest = f"{OUT}/{name}.jpg"
shutil.copy(imgs[0], dest)
print(f"{name}: OK -> {dest}")
return dest
def main():
a = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": ANCHOR_CODE},
wait=40, timeout=300)
txt = a if isinstance(a, str) else str(a)
if "NOMAP" in txt or "NOAUX" in txt:
print("ANCHOR FAIL: " + txt[:200])
return
# Pull the coordinate triples out of whatever wrapper the bridge returns.
import re
trip = re.findall(r"(-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)", txt)
if not trip:
print("ANCHOR PARSE FAIL: " + txt[:300])
return
pts = [tuple(float(v) for v in t) for t in trip]
print("anchors: " + str(pts))
made = []
for n, (x, y, z) in enumerate(pts[:2], start=1):
# Oblique from outside the deck, looking back at the merge.
d = grab(f"aux{n}_oblique", x + 260, y + 150, z + 260, x, y, z, fov=50)
if d:
made.append(d)
# Top-down: the merge taper reads clearest from directly above.
d = grab(f"aux{n}_topdown", x + 3, y + 430, z + 3, x, y, z, fov=42)
if d:
made.append(d)
# Driver's-eye down the lane.
d = grab(f"aux{n}_driver", x - 190, y + 16, z - 60, x + 120, y + 8, z + 40, fov=68)
if d:
made.append(d)
print("\nMADE " + str(len(made)) + " shots")
for m in made:
print(m)
if __name__ == "__main__":
main()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Capture the two spots the user circled in their screenshot.
Their view looks along the mainline with the ramp descending on the left; the
two marks sit at the suppression boundary (mainline samples ~525 and ~551),
where the aux lane ends and the parapet transitions back to the deck edge.
Anchor the camera to those SAMPLES, live, so the shot cannot drift.
left oval -> sample 525 area: was a 4-sample hole with no barrier
right circle -> sample 551 area: was a 26-stud protruding fence stub
"""
import re
import shutil
import sys
import time
sys.path.insert(0, "/tmp")
import rbx # noqa: E402
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
ANCHOR_CODE = """
local map = workspace:FindFirstChild("Map")
if not map then return "NOMAP" end
local deck = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
if k then deck[tonumber(k)] = d end
end
end
local out = {}
for _, i in ipairs({520, 525, 538, 551, 556}) do
local d = deck[i]
if d then
table.insert(out, ("%.1f %.1f %.1f"):format(d.Position.X, d.Position.Y, d.Position.Z))
end
end
if #out < 5 then return "NOSAMP" end
return table.concat(out, " | ")
"""
CAM_TPL = """
local cam = workspace.CurrentCamera
cam.CameraType = Enum.CameraType.Scriptable
cam.FieldOfView = {fov}
cam.CFrame = CFrame.lookAt(
Vector3.new({px}, {py}, {pz}),
Vector3.new({tx}, {ty}, {tz})
)
return "cam ok"
"""
def grab(name, px, py, pz, tx, ty, tz, fov=55):
code = CAM_TPL.format(fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz)
r = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": code})
if isinstance(r, dict) and r.get("isError"):
print(f"{name}: CAM FAIL {r}")
return None
time.sleep(1.5)
cap = rbx.call("screen_capture", {"capture_id": "ScreenCapture_1"}, wait=22, timeout=300)
imgs = cap.get("images") if isinstance(cap, dict) else None
if not imgs:
print(f"{name}: CAPTURE FAIL {str(cap)[:160]}")
return None
dest = f"{OUT}/{name}.jpg"
shutil.copy(imgs[0], dest)
print(f"{name}: OK -> {dest}")
return dest
def main():
a = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": ANCHOR_CODE},
wait=40, timeout=300)
txt = a if isinstance(a, str) else str(a)
if "NOMAP" in txt or "NOSAMP" in txt:
print("ANCHOR FAIL: " + txt[:200])
return
trip = re.findall(r"(-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)", txt)
if len(trip) < 5:
print("ANCHOR PARSE FAIL: " + txt[:300])
return
p = [tuple(float(v) for v in t) for t in trip]
s520, s525, s538, s551, s556 = p
print("anchors: " + str(p))
made = []
# LEFT OVAL: the formerly-bare run. Look along the road from upstream so the
# barrier line is seen edge-on -- a gap would read as an obvious break.
made.append(grab("fix_left_oval",
s520[0] - 90, s520[1] + 55, s520[2] - 90,
s538[0], s538[1], s538[2], fov=55))
# RIGHT CIRCLE: the transition where the fence used to jut out 26 studs.
made.append(grab("fix_right_circle",
s556[0] + 120, s556[1] + 50, s556[2] + 120,
s551[0], s551[1], s551[2], fov=50))
# Top-down over each boundary: a jut shows instantly as a step in the line.
made.append(grab("fix_boundary_525_top",
s525[0], s525[1] + 300, s525[2],
s525[0], s525[1], s525[2], fov=55))
made.append(grab("fix_boundary_551_top",
s551[0], s551[1] + 300, s551[2],
s551[0], s551[1], s551[2], fov=55))
# Wide overview matching the user's framing.
made.append(grab("fix_overview",
s538[0] - 420, s538[1] + 240, s538[2] - 420,
s538[0], s538[1], s538[2], fov=60))
made = [m for m in made if m]
print("\nMADE " + str(len(made)) + " shots")
for m in made:
print(m)
if __name__ == "__main__":
main()
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Capture the PA slip ramps from the user's own viewpoint, LIVE-ANCHORED.
The user's screenshot looks along the mainline from above the deck with the
ramp descending on the left. Reproduce that framing from the actual ramp
geometry so the shot cannot drift onto empty air after an edit.
"""
import re
import shutil
import sys
import time
sys.path.insert(0, "/tmp")
import rbx # noqa: E402
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
# Anchors: for each slip -> mouth (first deck seg), mid-span, landing (last seg).
ANCHOR_CODE = """
local map = workspace:FindFirstChild("Map")
if not map then return "NOMAP" end
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
segs[s] = segs[s] or {}
table.insert(segs[s], {i = tonumber(k), p = d.Position})
end
end
end
end
local out = {}
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local g = segs[s]
if g and #g > 2 then
table.sort(g, function(a, b) return a.i < b.i end)
local mouth = g[1].p
local mid = g[math.floor(#g / 2)].p
local land = g[#g].p
table.insert(out, ("%.1f %.1f %.1f"):format(mouth.X, mouth.Y, mouth.Z))
table.insert(out, ("%.1f %.1f %.1f"):format(mid.X, mid.Y, mid.Z))
table.insert(out, ("%.1f %.1f %.1f"):format(land.X, land.Y, land.Z))
end
end
if #out == 0 then return "NOSLIP" end
return table.concat(out, " | ")
"""
CAM_TPL = """
local cam = workspace.CurrentCamera
cam.CameraType = Enum.CameraType.Scriptable
cam.FieldOfView = {fov}
cam.CFrame = CFrame.lookAt(
Vector3.new({px}, {py}, {pz}),
Vector3.new({tx}, {ty}, {tz})
)
return "cam ok"
"""
def grab(name, px, py, pz, tx, ty, tz, fov=55):
code = CAM_TPL.format(fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz)
r = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": code})
if isinstance(r, dict) and r.get("isError"):
print(f"{name}: CAM FAIL {r}")
return None
time.sleep(1.5)
cap = rbx.call("screen_capture", {"capture_id": "ScreenCapture_1"}, wait=22, timeout=300)
imgs = cap.get("images") if isinstance(cap, dict) else None
if not imgs:
print(f"{name}: CAPTURE FAIL {str(cap)[:160]}")
return None
dest = f"{OUT}/{name}.jpg"
shutil.copy(imgs[0], dest)
print(f"{name}: OK -> {dest}")
return dest
def main():
a = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": ANCHOR_CODE},
wait=40, timeout=300)
txt = a if isinstance(a, str) else str(a)
if "NOMAP" in txt or "NOSLIP" in txt:
print("ANCHOR FAIL: " + txt[:200])
return
trip = re.findall(r"(-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)", txt)
if len(trip) < 6:
print("ANCHOR PARSE FAIL: " + txt[:300])
return
pts = [tuple(float(v) for v in t) for t in trip]
print("anchors: " + str(pts))
made = []
for n, base in enumerate((0, 3), start=1):
mouth, mid, land = pts[base], pts[base + 1], pts[base + 2]
tag = ["west", "east"][n - 1]
# 1. The user's view: high, behind the mouth, looking down the ramp.
made.append(grab(
f"ramp_{tag}_userview",
mouth[0] - 300, mouth[1] + 120, mouth[2] - 300,
land[0], land[1], land[2], fov=60))
# 2. Side elevation: shows the descent profile against the deck.
made.append(grab(
f"ramp_{tag}_side",
mid[0] + 420, mid[1] + 90, mid[2],
mid[0], mid[1], mid[2], fov=48))
# 3. Top-down over mid-span: is it parallel to the deck or cutting away?
made.append(grab(
f"ramp_{tag}_topdown",
mid[0], mid[1] + 560, mid[2],
mid[0], mid[1], mid[2], fov=55))
made = [m for m in made if m]
print("\nMADE " + str(len(made)) + " shots")
for m in made:
print(m)
if __name__ == "__main__":
main()
+73
View File
@@ -0,0 +1,73 @@
-- The rebuild was interrupted mid-flight; its effect is UNKNOWN. Establish
-- ground truth before touching anything else.
-- (a) Did the build run? part count vs 16714 (user-edited) / 16820 (source).
-- (b) Is the aux lane back? user's edited workspace had 30 aux parts,
-- source produces 52 -- so they deleted ~22.
-- (c) Did the smoothing pass actually land on the east slip?
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "workspace.Map MISSING" end
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d (16714=user-edited, 16820=source rebuild)"):format(parts))
-- Aux lane inventory, with index range + position so I can tell WHICH lane.
local aux = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^AuxLane_(%d+)$")
if k then table.insert(aux, { i = tonumber(k), p = d.Position, inst = d }) end
end
end
table.sort(aux, function(a, b) return a.i < b.i end)
say(("AuxLane parts=%d (30=after user deletion, 52=source)"):format(#aux))
if #aux > 0 then
-- group into contiguous runs so each physical lane shows up separately
local runs, cur = {}, nil
for _, a in ipairs(aux) do
if cur and a.i - cur.last <= 2 then cur.last = a.i; cur.n += 1
else
if cur then table.insert(runs, cur) end
cur = { first = a.i, last = a.i, n = 1, p = a.p }
end
end
if cur then table.insert(runs, cur) end
for _, r in ipairs(runs) do
say((" aux run idx %d..%d n=%d at (%.0f,%.0f,%.0f)"):format(
r.first, r.last, r.n, r.p.X, r.p.Y, r.p.Z))
end
end
-- Slip smoothness: did the relaxation take effect?
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then say(s .. ": ABSENT") continue end
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local d = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += d
if d > worst then worst = d; worstAt = segs[j].i end
if d > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
say(("%s: segs=%d total=%.1fdeg worst=%.1f@%d over5=%d level=%d"):format(
s, #segs, total, worst, worstAt, over, level))
end
return table.concat(out, "\n")
+55
View File
@@ -0,0 +1,55 @@
local HttpService = game:GetService("HttpService")
local RS = game:GetService("ReplicatedStorage")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local map = workspace:FindFirstChild("Map")
if not map then return "no Map" end
-- Aggregate PA parts EXCLUDING the slips we just built, so we can see the lot.
local agg = { xMin = math.huge, xMax = -math.huge, zMin = math.huge, zMax = -math.huge }
local names = {}
local biggest, biggestArea = nil, 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:sub(1, 2) == "PA" and not d.Name:find("Slip") then
names[d.Name] = (names[d.Name] or 0) + 1
agg.xMin = math.min(agg.xMin, d.Position.X - d.Size.X / 2)
agg.xMax = math.max(agg.xMax, d.Position.X + d.Size.X / 2)
agg.zMin = math.min(agg.zMin, d.Position.Z - d.Size.Z / 2)
agg.zMax = math.max(agg.zMax, d.Position.Z + d.Size.Z / 2)
local area = d.Size.X * d.Size.Z
if area > biggestArea then
biggestArea = area
biggest = ("%s size=(%d,%d,%d) pos=(%d,%d,%d) yTop=%.1f"):format(
d.Name, d.Size.X, d.Size.Y, d.Size.Z,
d.Position.X, d.Position.Y, d.Position.Z,
d.Position.Y + d.Size.Y / 2)
end
end
end
local nameList = {}
for n, c in pairs(names) do
table.insert(nameList, n .. "x" .. c)
end
table.sort(nameList)
-- cap the list so the payload can't truncate
local head = {}
for i = 1, math.min(#nameList, 40) do head[i] = nameList[i] end
local s = Circuit.samples
local function probe(i)
local a, b = s[i], s[i + 1]
local d = (b.pos - a.pos)
local fwd = Vector3.new(d.X, 0, d.Z).Unit
local right = fwd:Cross(Vector3.yAxis)
return ("s%d pos=(%d,%d) fwd=(%.2f,%.2f) right=(%.2f,%.2f)"):format(
i, a.pos.X, a.pos.Z, fwd.X, fwd.Z, right.X, right.Z)
end
return HttpService:JSONEncode({
pa_nonslip_bounds = { x = { math.floor(agg.xMin), math.floor(agg.xMax) }, z = { math.floor(agg.zMin), math.floor(agg.zMax) } },
biggest_pa_part = biggest,
distinct_pa_names = #nameList,
sample_names = head,
pa_center = tostring(Circuit.PA_CENTER),
joins = { probe(525), probe(537), probe(551), probe(539) },
})
+134
View File
@@ -0,0 +1,134 @@
-- Two questions, measured rather than eyeballed:
-- (1) WHERE exactly is the mainline missing barriers? Walk the deck edge and
-- report contiguous runs with no Barrier/EdgeRail within reach.
-- (User deleted ~106 parts by hand, so gaps are wherever they cut.)
-- (2) HOW does the south on-ramp differ from the north off-ramp? Compare the
-- two slips feature-by-feature so "make it look like the other" is a
-- concrete list of deltas, not a guess.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
-- Collect barrier-ish parts into a coarse spatial hash for fast lookup.
local CELL = 60
local grid = {}
local function key(x, z) return math.floor(x / CELL) .. ":" .. math.floor(z / CELL) end
local barrierCount = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and (d.Name == "Barrier" or d.Name == "EdgeRail") then
barrierCount += 1
local k = key(d.Position.X, d.Position.Z)
grid[k] = grid[k] or {}
table.insert(grid[k], d)
end
end
say(("indexed %d Barrier/EdgeRail parts"):format(barrierCount))
local function nearestBarrier(p)
local best = math.huge
local cx, cz = math.floor(p.X / CELL), math.floor(p.Z / CELL)
for dx = -1, 1 do
for dz = -1, 1 do
local c = grid[(cx + dx) .. ":" .. (cz + dz)]
if c then
for _, b in ipairs(c) do
local d2 = (Vector3.new(b.Position.X, 0, b.Position.Z) - Vector3.new(p.X, 0, p.Z)).Magnitude
-- only count barriers at roughly this height
if math.abs(b.Position.Y - p.Y) < 14 and d2 < best then best = d2 end
end
end
end
end
return best
end
-- Walk the mainline deck parts to build an ordered centreline.
local deck = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
or d.Name:match("^RoadDeck_(%d+)$")
if k then table.insert(deck, { i = tonumber(k), p = d.Position, cf = d.CFrame, sz = d.Size }) end
end
end
table.sort(deck, function(a, b) return a.i < b.i end)
say(("mainline deck parts found: %d"):format(#deck))
if #deck > 0 then
local UP = Vector3.new(0, 1, 0)
local runs = {}
local cur = nil
for j, s in ipairs(deck) do
local fwd = s.cf.LookVector
local right = fwd:Cross(UP).Unit
local half = s.sz.X / 2
-- probe just outboard of each edge
local gapL = nearestBarrier(s.p - right * (half + 2)) > 12
local gapR = nearestBarrier(s.p + right * (half + 2)) > 12
local bad = gapL or gapR
if bad then
local side = (gapL and gapR) and "BOTH" or (gapL and "L" or "R")
if cur and cur.side == side and s.i - cur.last <= 2 then
cur.last = s.i; cur.n += 1
else
if cur then table.insert(runs, cur) end
cur = { first = s.i, last = s.i, n = 1, side = side, pos = s.p }
end
else
if cur then table.insert(runs, cur); cur = nil end
end
end
if cur then table.insert(runs, cur) end
table.sort(runs, function(a, b) return a.n > b.n end)
say(("UNBARRIERED RUNS: %d total"):format(#runs))
for j = 1, math.min(#runs, 14) do
local r = runs[j]
say((" samples %d..%d (%d) side=%s at (%.0f,%.0f,%.0f)"):format(
r.first, r.last, r.n, r.side, r.pos.X, r.pos.Y, r.pos.Z))
end
end
-- (2) Compare the two slips.
say("--- SLIP COMPARISON ---")
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs, walls, lines, other = {}, 0, 0, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^" .. s) then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
table.insert(segs, { i = tonumber(k), p = d.Position, sz = d.Size, cf = d.CFrame })
elseif d.Name:match("_Wall_") then walls += 1
elseif d.Name:match("[Ll]ine") then lines += 1
else
local nm = d.Name:gsub("^" .. s .. "_", ""):gsub("_%d+$", "")
other[nm] = (other[nm] or 0) + 1
end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
local ol = {}
for k, v in pairs(other) do table.insert(ol, k .. "=" .. v) end
table.sort(ol)
if #segs > 0 then
-- heading of first and last segment: which way does it run?
local h1 = segs[1].cf.LookVector
local hN = segs[#segs].cf.LookVector
-- curvature: total heading change
local turn = 0
for j = 2, #segs do
local a = segs[j - 1].cf.LookVector
local b = segs[j].cf.LookVector
turn += math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
end
say(("%s: segs=%d walls=%d lines=%d startY=%.1f endY=%.1f"):format(s, #segs, walls, lines, segs[1].p.Y, segs[#segs].p.Y))
say((" heading first=(%.2f,%.2f) last=(%.2f,%.2f) totalTurn=%.1fdeg"):format(h1.X, h1.Z, hN.X, hN.Z, turn))
say((" start=(%.0f,%.0f,%.0f) end=(%.0f,%.0f,%.0f)"):format(
segs[1].p.X, segs[1].p.Y, segs[1].p.Z, segs[#segs].p.X, segs[#segs].p.Y, segs[#segs].p.Z))
say(" other parts: " .. (#ol > 0 and table.concat(ol, " ") or "none"))
end
end
return table.concat(out, "\n")
+55
View File
@@ -0,0 +1,55 @@
-- The require sweep passing only proves the modules LOAD. What runs on Play is
-- MapBuilder actually building. Execute it for real and capture the error.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local serverScript = SS:FindFirstChild("Server")
local mb
for _, d in ipairs(SS:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "MapBuilder" then mb = d end
end
say("MapBuilder module: " .. (mb and mb:GetFullName() or "NOT FOUND"))
if not mb then return table.concat(out, "\n") end
local ok, mod = pcall(require, mb)
say("require: " .. (ok and "ok" or ("FAIL " .. tostring(mod))))
if not ok then return table.concat(out, "\n") end
say("exports: " .. type(mod))
if type(mod) == "table" then
local keys = {}
for k, v in pairs(mod) do table.insert(keys, ("%s(%s)"):format(tostring(k), type(v))) end
table.sort(keys)
say(" " .. table.concat(keys, ", "))
end
-- Clear any existing map so we build from scratch, exactly like a fresh run.
local existing = workspace:FindFirstChild("Map")
say("existing Map in workspace: " .. (existing and "yes" or "no"))
local buildFn = type(mod) == "table" and (mod.Build or mod.build or mod.BuildMap)
if type(buildFn) ~= "function" then
say("NO BUILD FUNCTION EXPORTED -- cannot test build path")
return table.concat(out, "\n")
end
if existing then existing:Destroy() end
local t0 = os.clock()
local bok, berr = pcall(buildFn)
local dt = os.clock() - t0
say(("build pcall: %s (%.2fs)"):format(bok and "OK" or "FAIL", dt))
if not bok then
say("BUILD ERROR: " .. tostring(berr):sub(1, 600))
return table.concat(out, "\n")
end
local m = workspace:FindFirstChild("Map")
say("Map after build: " .. (m and "present" or "MISSING"))
if m then
local n = #m:GetDescendants()
say("descendants: " .. n)
if n == 0 then say("CONTROL FAIL: map built but empty") end
end
return table.concat(out, "\n")
+106
View File
@@ -0,0 +1,106 @@
-- Re-verify after un-pinning index 2, and settle the 4 "bare" samples:
-- 525L/526L/549L/550L sit at the ramp MOUTHS (west join=525, east join=551).
-- An open parapet at a mouth is correct -- that is the doorway cars drive
-- through, and the ramp deck's own walls take over. So check whether ramp deck
-- + ramp wall actually cover those samples. If yes: not a hole. If no: a hole.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mod = SS:FindFirstChild("MapBuilder", true)
if not mod then return "MapBuilder NOT FOUND" end
local fresh = mod:Clone()
fresh.Name = "MB_fresh_" .. tostring(math.random(1e6))
fresh.Parent = SS
local ok, MB = pcall(require, fresh)
if not ok then fresh:Destroy() return "require FAILED: " .. tostring(MB) end
local ok2, err = pcall(function() MB.build() end)
fresh:Destroy()
if not ok2 then return "build ERROR: " .. tostring(err) end
say("build OK")
local map = workspace:FindFirstChild("Map")
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d"):format(parts))
local deck, bars, slipAll = {}, {}, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
if k then deck[tonumber(k)] = d end
if d.Name == "Barrier" or d.Name == "EdgeRail" then table.insert(bars, d) end
if d.Name:match("^PA[EW][ae]stSlip") then table.insert(slipAll, d) end
end
end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local function nearest(list, p, ytol)
local best = math.huge
for _, b in ipairs(list) do
if math.abs(b.Position.Y - p.Y) < (ytol or 14) then
local dd = (flat(b.Position) - flat(p)).Magnitude
if dd < best then best = dd end
end
end
return best
end
say("--- edge audit 520..555 (L side) ---")
local realHoles = {}
for i = 520, 555 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local edge = d.Position - right * (d.Size.X / 2 + 2)
local bd = nearest(bars, edge)
local sd = nearest(slipAll, edge, 20)
local covered = bd < 12 or sd < 30
if not covered then table.insert(realHoles, i) end
if bd >= 12 then
say((" %d: barrier=%.1f slipStruct=%.1f -> %s"):format(
i, bd, sd, covered and "OK (ramp mouth)" or "HOLE"))
end
end
end
say(("REAL holes: %d %s"):format(#realHoles,
#realHoles > 0 and table.concat(realHoles, ",") or "(none)"))
say("--- smoothness ---")
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local dth = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += dth
if dth > worst then worst = dth; worstAt = segs[j].i end
if dth > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
say(("%s: total=%.1fdeg worst=%.1f@%d over5=%d level=%d"):format(
s, total, worst, worstAt, over, level))
say((" start=(%.0f,%.0f,%.0f) end=(%.0f,%.0f,%.0f)"):format(
segs[1].p.X, segs[1].p.Y, segs[1].p.Z, segs[#segs].p.X, segs[#segs].p.Y, segs[#segs].p.Z))
end
local aux = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^AuxLane_%d+$") then aux += 1 end
end
say(("AuxLane total=%d (want 30)"):format(aux))
return table.concat(out, "\n")
+123
View File
@@ -0,0 +1,123 @@
-- Studio caches required modules, which is why the previous rebuild silently
-- ran the OLD code (east slip still measured 216.0deg, unchanged). Force a
-- fresh load: clone the ModuleScript so require() sees a new instance and
-- recompiles from current source.
--
-- Verifies three things at once:
-- (1) smoothing landed -> east total heading change << 216deg, zero >5deg kinks
-- (2) aux lane stays deleted -> AuxLane run 527..548 ABSENT, total ~30 not 52
-- (3) barrier gap closed -> parapet present along 527..548
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mod = SS:FindFirstChild("MapBuilder", true)
if not mod then return "MapBuilder NOT FOUND" end
if not mod:IsA("ModuleScript") then return "MapBuilder is " .. mod.ClassName end
local fresh = mod:Clone()
fresh.Name = "MapBuilder_fresh_" .. tostring(math.random(1e6))
fresh.Parent = SS
local ok, MB = pcall(require, fresh)
if not ok then
fresh:Destroy()
return "require FAILED: " .. tostring(MB)
end
local t0 = os.clock()
local ok2, err = pcall(function() MB.build() end)
local dt = os.clock() - t0
fresh:Destroy()
if not ok2 then return "build ERROR: " .. tostring(err) end
say(("build OK (%.2fs) via fresh module"):format(dt))
local map = workspace:FindFirstChild("Map")
if not map then return "Map missing" end
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d"):format(parts))
-- (2) aux lane
local aux = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^AuxLane_(%d+)$")
if k then table.insert(aux, tonumber(k)) end
end
end
table.sort(aux)
local inRange = 0
for _, i in ipairs(aux) do if i >= 527 and i <= 548 then inRange += 1 end end
say(("AuxLane total=%d inside 527..548 = %d (want 0)"):format(#aux, inRange))
-- (3) barrier coverage across the formerly-bare run
local deck, bars = {}, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
if k then deck[tonumber(k)] = d end
if d.Name == "Barrier" or d.Name == "EdgeRail" then table.insert(bars, d) end
end
end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local bare = {}
for i = 525, 550 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
for _, sgn in ipairs({ -1, 1 }) do
local edge = d.Position + right * sgn * (d.Size.X / 2 + 2)
local best = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - edge.Y) < 14 then
local dd = (flat(b.Position) - flat(edge)).Magnitude
if dd < best then best = dd end
end
end
if best > 12 then table.insert(bare, i .. (sgn < 0 and "L" or "R")) end
end
end
end
say(("bare edge samples in 525..550: %d %s"):format(#bare,
#bare > 0 and ("-> " .. table.concat(bare, ",")) or "(want 0)"))
-- (1) smoothness
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then say(s .. ": ABSENT") continue end
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local dth = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += dth
if dth > worst then worst = dth; worstAt = segs[j].i end
if dth > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
local lmin, lmax = math.huge, -math.huge
for j = 2, #segs do
local L = (segs[j].p - segs[j - 1].p).Magnitude
lmin = math.min(lmin, L); lmax = math.max(lmax, L)
end
say(("%s: total=%.1fdeg worst=%.1f@%d over5=%d level=%d spacing=%.1f..%.1f"):format(
s, total, worst, worstAt, over, level, lmin, lmax))
say((" start=(%.0f,%.0f,%.0f) end=(%.0f,%.0f,%.0f)"):format(
segs[1].p.X, segs[1].p.Y, segs[1].p.Z, segs[#segs].p.X, segs[#segs].p.Y, segs[#segs].p.Z))
end
return table.concat(out, "\n")
+135
View File
@@ -0,0 +1,135 @@
-- Rebuild with AUX_SUPPRESS widened to 525..550, then verify BOTH marked
-- defects are gone:
-- (A) left oval -> zero samples with no left barrier
-- (B) right circle -> no 26-stud lateral snap at the suppression boundary
-- The 6.3-stud steps at 512/513/563 are the aux lane's own smooth taper
-- (width 26 -> 19 -> 13 -> 7 -> 0) and are CORRECT; the parapet rides the
-- taper. Only steps much larger than that are real juts.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mod = SS:FindFirstChild("MapBuilder", true)
if not mod then return "MapBuilder NOT FOUND" end
local fresh = mod:Clone()
fresh.Name = "MB_fresh_" .. tostring(math.random(1e6))
fresh.Parent = SS
local ok, MB = pcall(require, fresh)
if not ok then fresh:Destroy() return "require FAILED: " .. tostring(MB) end
local ok2, err = pcall(function() MB.build() end)
fresh:Destroy()
if not ok2 then return "build ERROR: " .. tostring(err) end
say("build OK")
local map = workspace:FindFirstChild("Map")
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d"):format(parts))
local UP = Vector3.new(0, 1, 0)
local deck, aux, bars = {}, {}, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
if k then deck[tonumber(k)] = d end
local a = d.Name:match("^AuxLane_(%d+)$")
if a then aux[tonumber(a)] = d end
if d.Name == "Barrier" then table.insert(bars, d) end
end
end
say("sample | auxW | left barrier lat | step")
local prevLat, jogs, gaps = nil, {}, {}
for i = 505, 570 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local c = d.Position
local best, bestLat = math.huge, nil
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - c.Y) < 14 then
local rel = b.Position - c
local along = math.abs(rel:Dot(d.CFrame.LookVector))
local lat = rel:Dot(right)
if along < 22 and lat < 0 and along < best then best = along; bestLat = lat end
end
end
local auxW = aux[i] and aux[i].Size.X or 0
if bestLat then
local step = prevLat and math.abs(math.abs(bestLat) - math.abs(prevLat)) or 0
if step > 10 then table.insert(jogs, ("%d(%.1f)"):format(i, step)) end
if step > 10 or i % 5 == 0 then
say((" %d | aux=%.0f | lat=%.1f | step=%.1f%s"):format(
i, auxW, bestLat, step, step > 10 and " <<< JOG" or ""))
end
prevLat = bestLat
else
table.insert(gaps, i)
say((" %d | aux=%.0f | NO LEFT BARRIER <<< GAP"):format(i, auxW))
prevLat = nil
end
end
end
say(("BIG JOGS (>10): %d %s"):format(#jogs, #jogs > 0 and table.concat(jogs, " ") or "(none)"))
say(("GAPS: %d %s"):format(#gaps, #gaps > 0 and table.concat(gaps, ",") or "(none)"))
-- full-loop unguarded sweep, same rule as before
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local allBars = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and (d.Name == "Barrier" or d.Name == "EdgeRail") then
table.insert(allBars, d)
end
end
local slipParts = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("Slip") then table.insert(slipParts, d) end
end
local holes, swept = {}, 0
for i, d in pairs(deck) do
swept += 1
local right = d.CFrame.LookVector:Cross(UP).Unit
for _, sgn in ipairs({ -1, 1 }) do
local edge = d.Position + right * sgn * (d.Size.X / 2 + 2)
local bd = math.huge
for _, b in ipairs(allBars) do
if math.abs(b.Position.Y - edge.Y) < 14 then
local dd = (flat(b.Position) - flat(edge)).Magnitude
if dd < bd then bd = dd end
end
end
if bd >= 12 then
local ok2b = false
local a = aux[i]
if a then
local outer = a.Position + right * sgn * (a.Size.X / 2 + 2)
local bd2 = math.huge
for _, b in ipairs(allBars) do
if math.abs(b.Position.Y - outer.Y) < 14 then
local dd = (flat(b.Position) - flat(outer)).Magnitude
if dd < bd2 then bd2 = dd end
end
end
if bd2 < 14 then ok2b = true end
end
if not ok2b then
local sd = math.huge
for _, p in ipairs(slipParts) do
local dd = (flat(p.Position) - flat(edge)).Magnitude
if dd < sd then sd = dd end
end
if sd > 30 then table.insert(holes, i .. (sgn < 0 and "L" or "R")) end
end
end
end
end
say(("FULL SWEEP: %d samples, unguarded=%d %s"):format(swept, #holes,
#holes > 0 and table.concat(holes, ",") or "(none)"))
local auxN = 0
for _ in pairs(aux) do auxN += 1 end
say(("AuxLane total=%d (want 30)"):format(auxN))
return table.concat(out, "\n")
+97
View File
@@ -0,0 +1,97 @@
-- The 50-sample "unbarriered run" (513..562, LEFT) overlaps the aux-lane region
-- (510..565) exactly. In that stretch the parapet was deliberately RELOCATED
-- outboard, to the aux lane's outer edge -- so a 12-stud probe off the mainline
-- edge would report "missing" when the barrier is simply further out.
-- Do not claim a hole until this is disambiguated. Report ACTUAL distances.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local bars = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and (d.Name == "Barrier" or d.Name == "EdgeRail"
or d.Name == "KillWall" or d.Name:match("Parapet")) then
table.insert(bars, d)
end
end
say(("barrier-ish parts: %d"):format(#bars))
local aux = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^AuxLane_(%d+)$") then
table.insert(aux, d)
end
end
say(("aux lane parts: %d"):format(#aux))
local deck = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
or d.Name:match("^RoadDeck_(%d+)$")
if k then deck[tonumber(k)] = d end
end
end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
-- For a probe point, distance to nearest barrier AND nearest aux part.
local function nearest(list, p, ymax)
local best, bi = math.huge, nil
for _, b in ipairs(list) do
if math.abs(b.Position.Y - p.Y) < (ymax or 16) then
local d2 = (flat(b.Position) - flat(p)).Magnitude
if d2 < best then best = d2; bi = b end
end
end
return best, bi
end
say("sample | edgeDist->barrier | auxHere | verdict")
local realGaps = {}
for i = 505, 570 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
-- LEFT side probe (side = -right)
local edge = d.Position - right * (d.Size.X / 2 + 2)
local bd = nearest(bars, edge)
local ad = nearest(aux, edge)
local hasAux = ad < 20
-- A barrier is "present" if within 12 of the mainline edge, OR the aux
-- lane is here and a barrier sits within 12 of the aux OUTER edge.
local ok = bd < 12
if not ok and hasAux then
local auxEdge = d.Position - right * (d.Size.X / 2 + 26)
local bd2 = nearest(bars, auxEdge)
if bd2 < 14 then ok = true end
if i % 6 == 0 then
say((" %d | %.1f | aux=%s | auxEdgeBarrier=%.1f | %s"):format(
i, bd, tostring(hasAux), bd2, ok and "OK(relocated)" or "GAP"))
end
elseif i % 6 == 0 then
say((" %d | %.1f | aux=%s | %s"):format(i, bd, tostring(hasAux), ok and "OK" or "GAP"))
end
if not ok then table.insert(realGaps, i) end
end
end
say(("REAL gaps after accounting for relocated parapet: %d samples"):format(#realGaps))
if #realGaps > 0 then
local runs, cur = {}, nil
for _, i in ipairs(realGaps) do
if cur and i - cur.last <= 2 then cur.last = i
else if cur then table.insert(runs, cur) end cur = { first = i, last = i } end
end
if cur then table.insert(runs, cur) end
for _, r in ipairs(runs) do
local d = deck[r.first]
say((" GAP RUN %d..%d at (%.0f,%.0f,%.0f)"):format(
r.first, r.last, d.Position.X, d.Position.Y, d.Position.Z))
end
end
return table.concat(out, "\n")
+93
View File
@@ -0,0 +1,93 @@
-- The 10 flagged samples all read barrier=22.9 -- the exact signature of the
-- RELOCATED parapet (aux lane still exists at 510..524 and 551..565, and its
-- outer edge carries the rail). probe_final's audit did not re-probe the aux
-- outer edge, so it mislabelled them. Settle it: for each flagged sample,
-- measure distance from the AUX OUTER edge to the nearest barrier.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local deck, bars, aux = {}, {}, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
if k then deck[tonumber(k)] = d end
if d.Name == "Barrier" or d.Name == "EdgeRail" then table.insert(bars, d) end
local a = d.Name:match("^AuxLane_(%d+)$")
if a then aux[tonumber(a)] = d end
end
end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local function nearest(p, ytol)
local best = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - p.Y) < (ytol or 14) then
local dd = (flat(b.Position) - flat(p)).Magnitude
if dd < best then best = dd end
end
end
return best
end
local flagged = { 520, 521, 522, 523, 524, 551, 552, 553, 554, 555 }
local trueHoles = {}
say("sample | auxHere | barrier@auxOuterEdge | verdict")
for _, i in ipairs(flagged) do
local d, a = deck[i], aux[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local verdict, dist
if a then
-- probe just outboard of the AUX lane, where the rail was moved to
local outer = a.Position - right * (a.Size.X / 2 + 2)
dist = nearest(outer)
verdict = dist < 14 and "OK (parapet relocated to aux edge)" or "HOLE"
else
local edge = d.Position - right * (d.Size.X / 2 + 2)
dist = nearest(edge)
verdict = dist < 12 and "OK" or "HOLE"
end
if verdict == "HOLE" then table.insert(trueHoles, i) end
say((" %d | aux=%s | %.1f | %s"):format(i, a and "yes" or "no", dist, verdict))
end
end
say(("TRUE UNGUARDED SAMPLES: %d %s"):format(#trueHoles,
#trueHoles > 0 and table.concat(trueHoles, ",") or "(none)"))
-- Full-loop sweep so nothing elsewhere is missed.
local swept, holes = 0, {}
for i, d in pairs(deck) do
swept += 1
local right = d.CFrame.LookVector:Cross(UP).Unit
for _, sgn in ipairs({ -1, 1 }) do
local edge = d.Position + right * sgn * (d.Size.X / 2 + 2)
if nearest(edge) >= 12 then
local a = aux[i]
local ok = false
if a then
local outer = a.Position + right * sgn * (a.Size.X / 2 + 2)
if nearest(outer) < 14 then ok = true end
end
if not ok then
-- ramp mouth? slip structure nearby counts as guarded
local sd = math.huge
for _, p in ipairs(map:GetDescendants()) do
if p:IsA("BasePart") and p.Name:match("Slip") then
local dd = (flat(p.Position) - flat(edge)).Magnitude
if dd < sd then sd = dd end
end
end
if sd > 30 then table.insert(holes, i .. (sgn < 0 and "L" or "R")) end
end
end
end
end
say(("FULL SWEEP: %d deck samples, unguarded=%d %s"):format(swept, #holes,
#holes > 0 and ("-> " .. table.concat(holes, ",")) or "(none)"))
return table.concat(out, "\n")
+107
View File
@@ -0,0 +1,107 @@
-- Slip walls measured CLEAN (0 missing, 0 deviation, 0.00 lateral step), so
-- neither marked defect is on the ramp decks themselves. Prime suspect is the
-- suppression boundary I introduced: across 527..548 the parapet is forced back
-- to the mainline edge, while 510..526 and 549..565 still carry it relocated
-- OUT on the aux lane's outer edge (~26 studs further). At samples 527 and 549
-- that is a ~26-stud lateral JOG -- a barrier that visibly sticks out and ends
-- in a stub. And the gore between mainline barrier and ramp wall start may be
-- open -- the missing-barrier oval.
--
-- Measure signed lateral offset of every Barrier from the mainline centreline,
-- walk 505..570, and report steps + gaps.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local deck, aux = {}, {}
local bars = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
if k then deck[tonumber(k)] = d end
local a = d.Name:match("^AuxLane_(%d+)$")
if a then aux[tonumber(a)] = d end
if d.Name == "Barrier" then table.insert(bars, d) end
end
end
-- For each mainline sample, find the barrier nearest to that sample's plane on
-- the LEFT side, and report its lateral offset from the centreline.
say("sample | auxW | barrier lateral offset from centreline | step")
local prevLat = nil
local jogs, gaps = {}, {}
for i = 505, 570 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local c = d.Position
-- gather barriers within half a sample spacing along the road axis
local best, bestLat = math.huge, nil
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - c.Y) < 14 then
local rel = b.Position - c
local along = math.abs(rel:Dot(d.CFrame.LookVector))
local lat = rel:Dot(right)
if along < 22 and lat < 0 then -- LEFT side only
if along < best then best = along; bestLat = lat end
end
end
end
local auxW = aux[i] and aux[i].Size.X or 0
if bestLat then
local step = prevLat and math.abs(math.abs(bestLat) - math.abs(prevLat)) or 0
if step > 6 then
table.insert(jogs, ("%d(step %.1f)"):format(i, step))
end
if i % 4 == 0 or step > 6 then
say((" %d | aux=%.0f | lat=%.1f | step=%.1f%s"):format(
i, auxW, bestLat, step, step > 6 and " <<< JOG" or ""))
end
prevLat = bestLat
else
table.insert(gaps, i)
say((" %d | aux=%.0f | NO BARRIER FOUND on left <<< GAP"):format(i, auxW))
prevLat = nil
end
end
end
say(("JOGS (lateral step >6): %d %s"):format(#jogs, #jogs > 0 and table.concat(jogs, " ") or "(none)"))
say(("GAPS (no left barrier): %d %s"):format(#gaps, #gaps > 0 and table.concat(gaps, ",") or "(none)"))
-- Gore check: distance from each slip's FIRST wall to the nearest mainline
-- barrier. A large value means an open mouth corner -- the missing-barrier oval.
say("--- gore / mouth junction ---")
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local first, firstK = nil, math.huge
local last, lastK = nil, -1
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local wk = d.Name:match("^" .. s .. "_Wall_(%d+)$")
if wk then
local k = tonumber(wk)
if k < firstK then firstK = k; first = d end
if k > lastK then lastK = k; last = d end
end
end
end
if first then
local bd = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - first.Position.Y) < 16 then
local dd = (flat(b.Position) - flat(first.Position)).Magnitude
if dd < bd then bd = dd end
end
end
say(("%s: first wall seg %d at (%.0f,%.0f,%.0f) -> nearest mainline barrier %.1f"):format(
s, firstK, first.Position.X, first.Position.Y, first.Position.Z, bd))
say((" last wall seg %d at (%.0f,%.0f,%.0f)"):format(
lastK, last.Position.X, last.Position.Y, last.Position.Z))
end
end
return table.concat(out, "\n")
+73
View File
@@ -0,0 +1,73 @@
-- PAEastSlip turns 216deg vs PAWestSlip's 56.5deg -- nearly 4x. That is the
-- "looks terrible" the user means. Find WHERE the turn concentrates: is it a
-- smooth long curve, or a few violent kinks? Per-segment heading deltas tell us.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame, sz = d.Size }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then say(s .. ": absent") continue end
local deltas = {}
for j = 2, #segs do
local a = segs[j - 1].cf.LookVector
local b = segs[j].cf.LookVector
local dth = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
table.insert(deltas, { i = segs[j].i, d = dth, p = segs[j].p })
end
-- worst kinks
local sorted = {}
for _, v in ipairs(deltas) do table.insert(sorted, v) end
table.sort(sorted, function(x, y) return x.d > y.d end)
local sum = 0
for _, v in ipairs(deltas) do sum += v.d end
say(("%s: total=%.1fdeg mean=%.2fdeg/seg n=%d"):format(s, sum, sum / #deltas, #deltas))
local worst = {}
for j = 1, math.min(8, #sorted) do
table.insert(worst, ("seg%d=%.1f"):format(sorted[j].i, sorted[j].d))
end
say(" worst kinks: " .. table.concat(worst, " "))
-- how many segments exceed a smooth-curve threshold?
local over = 0
for _, v in ipairs(deltas) do if v.d > 5 then over += 1 end end
say((" segments turning >5deg: %d"):format(over))
-- segment length consistency (uneven lengths also read as ugly)
local lmin, lmax, lsum = math.huge, -math.huge, 0
for j = 2, #segs do
local L = (segs[j].p - segs[j - 1].p).Magnitude
lmin = math.min(lmin, L); lmax = math.max(lmax, L); lsum += L
end
say((" seg spacing: min=%.1f max=%.1f mean=%.1f totalRun=%.0f"):format(
lmin, lmax, lsum / (#segs - 1), lsum))
-- vertical profile: where does it drop?
local topY = segs[1].p.Y
local level = 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
local steep = 0
for j = 2, #segs do
local dy = math.abs(segs[j].p.Y - segs[j - 1].p.Y)
local dxz = (Vector3.new(segs[j].p.X, 0, segs[j].p.Z) - Vector3.new(segs[j - 1].p.X, 0, segs[j - 1].p.Z)).Magnitude
if dxz > 0.1 and (dy / dxz) > 0.12 then steep += 1 end
end
say((" level-run=%d segments steeper than 12%%: %d"):format(level, steep))
end
return table.concat(out, "\n")
+61
View File
@@ -0,0 +1,61 @@
-- East slip kinks hard at segs 6,7,11,12 (27.8/17.5/58.2/50.1 deg). The slip
-- follows the mainline via centreAt(f), so either (a) the mainline bends
-- viciously at those samples, or (b) the slip's own path math misbehaves.
-- East runs join=551 -> land=539 (backwards, span=-12); west runs 525 -> 537.
-- Sample the mainline heading across BOTH spans to tell which.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local deck = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
or d.Name:match("^RoadDeck_(%d+)$")
if k then table.insert(deck, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(deck, function(a, b) return a.i < b.i end)
local byIdx = {}
for _, d in ipairs(deck) do byIdx[d.i] = d end
say(("mainline parts=%d idx %d..%d"):format(#deck, deck[1].i, deck[#deck].i))
local function report(lo, hi, label)
say(("--- mainline heading, samples %d..%d (%s) ---"):format(lo, hi, label))
local prev, acc = nil, 0
local worst, worstAt = 0, -1
for i = lo, hi do
local d = byIdx[i]
if d then
local h = d.cf.LookVector
if prev then
local dth = math.deg(math.acos(math.clamp(prev:Dot(h), -1, 1)))
acc += dth
if dth > worst then worst = dth; worstAt = i end
if dth > 4 then
say((" sample %d: turn %.1fdeg at (%.0f,%.0f,%.0f)"):format(i, dth, d.p.X, d.p.Y, d.p.Z))
end
end
prev = h
end
end
say((" total=%.1fdeg worst=%.1fdeg@%d"):format(acc, worst, worstAt))
end
report(525, 537, "WEST slip span - smooth one")
report(539, 551, "EAST slip span - ugly one")
-- Also: does the east span cross the sample-wrap boundary or reverse direction?
say("--- endpoint geometry ---")
for _, pair in ipairs({ { 525, 537, "west" }, { 551, 539, "east" } }) do
local a, b = byIdx[pair[1]], byIdx[pair[2]]
if a and b then
local straight = (Vector3.new(a.p.X, 0, a.p.Z) - Vector3.new(b.p.X, 0, b.p.Z)).Magnitude
say(("%s: join@%d=(%.0f,%.0f) land@%d=(%.0f,%.0f) straightDist=%.0f"):format(
pair[3], pair[1], a.p.X, a.p.Z, pair[2], b.p.X, b.p.Z, straight))
end
end
return table.concat(out, "\n")
+103
View File
@@ -0,0 +1,103 @@
-- READ-ONLY inventory of the Daikoku Futo PA area so the capture camera can be
-- anchored to real geometry instead of guessed coordinates.
--
-- Wanted:
-- * what top-level containers/models exist for the PA
-- * every distinct part-name PREFIX in the PA region, with counts
-- * the PA's world-space bounding box + centre (for framing)
-- * key landmarks: slip ramps, apron/parking deck, any building/structure
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
-- 1. Top-level structure of Map
say("=== Map children ===")
for _, c in ipairs(map:GetChildren()) do
local n = 0
for _, d in ipairs(c:GetDescendants()) do
if d:IsA("BasePart") then n += 1 end
end
say((" %s (%s) parts=%d"):format(c.Name, c.ClassName, n))
end
-- 2. Anything whose name mentions PA / Daikoku / parking
say("=== named PA-ish objects (models/folders) ===")
local seen = 0
for _, d in ipairs(map:GetDescendants()) do
if not d:IsA("BasePart") then
local nm = d.Name
if nm:match("PA") or nm:lower():match("daikoku") or nm:lower():match("park")
or nm:lower():match("apron") or nm:lower():match("rest") then
local n = 0
for _, x in ipairs(d:GetDescendants()) do
if x:IsA("BasePart") then n += 1 end
end
say((" %s (%s) parts=%d parent=%s"):format(d.Name, d.ClassName, n, d.Parent.Name))
seen += 1
if seen > 40 then say(" ...(truncated)") break end
end
end
end
if seen == 0 then say(" (none found by name)") end
-- 3. Locate the PA by its slip ramps: the apron is where the ramps land.
local land = {}
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local best, bestK = nil, -1
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k and tonumber(k) > bestK then bestK = tonumber(k); best = d end
end
end
if best then
land[s] = best.Position
say(("%s landing: (%.0f, %.0f, %.0f)"):format(s, best.Position.X, best.Position.Y, best.Position.Z))
end
end
local anchor
if land.PAWestSlip and land.PAEastSlip then
anchor = (land.PAWestSlip + land.PAEastSlip) / 2
elseif land.PAWestSlip then
anchor = land.PAWestSlip
elseif land.PAEastSlip then
anchor = land.PAEastSlip
end
if not anchor then return table.concat(out, "\n") .. "\nNO SLIP LANDINGS -> cannot anchor" end
say(("PA anchor (between landings): (%.0f, %.0f, %.0f)"):format(anchor.X, anchor.Y, anchor.Z))
-- 4. Every part within 1400 studs of the anchor, grouped by name prefix.
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local counts, mins, maxs = {}, {}, {}
local lo = Vector3.new(math.huge, math.huge, math.huge)
local hi = Vector3.new(-math.huge, -math.huge, -math.huge)
local total = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
if (flat(d.Position) - flat(anchor)).Magnitude < 1400 then
total += 1
-- prefix = name with trailing _<digits> stripped
local pre = d.Name:gsub("_%d+$", "")
counts[pre] = (counts[pre] or 0) + 1
lo = Vector3.new(math.min(lo.X, d.Position.X), math.min(lo.Y, d.Position.Y), math.min(lo.Z, d.Position.Z))
hi = Vector3.new(math.max(hi.X, d.Position.X), math.max(hi.Y, d.Position.Y), math.max(hi.Z, d.Position.Z))
end
end
end
say(("=== %d parts within 1400 studs of PA anchor ==="):format(total))
local keys = {}
for k in pairs(counts) do table.insert(keys, k) end
table.sort(keys, function(a, b) return counts[a] > counts[b] end)
for idx, k in ipairs(keys) do
if idx > 30 then say((" ...and %d more prefixes"):format(#keys - 30)) break end
say((" %-28s %d"):format(k, counts[k]))
end
say(("bbox: (%.0f,%.0f,%.0f) .. (%.0f,%.0f,%.0f)"):format(lo.X, lo.Y, lo.Z, hi.X, hi.Y, hi.Z))
say(("bbox size: %.0f x %.0f x %.0f"):format(hi.X - lo.X, hi.Y - lo.Y, hi.Z - lo.Z))
local ctr = (lo + hi) / 2
say(("bbox centre: (%.0f, %.0f, %.0f)"):format(ctr.X, ctr.Y, ctr.Z))
return table.concat(out, "\n")
+36
View File
@@ -0,0 +1,36 @@
-- Minimal PA locator. The full inventory probe returned an empty error, so
-- start small and confirm the basics work before adding anything back.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
say("=== Map children ===")
for _, c in ipairs(map:GetChildren()) do
local n = 0
for _, d in ipairs(c:GetDescendants()) do
if d:IsA("BasePart") then n += 1 end
end
say((" %s (%s) parts=%d"):format(c.Name, c.ClassName, n))
end
local land = {}
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local best, bestK = nil, -1
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k and tonumber(k) > bestK then
bestK = tonumber(k)
best = d
end
end
end
if best then
land[s] = best.Position
say(("%s landing: (%.0f, %.0f, %.0f)"):format(s, best.Position.X, best.Position.Y, best.Position.Z))
end
end
return table.concat(out, "\n")
+80
View File
@@ -0,0 +1,80 @@
-- The crux of the user's request: does the ramp run PARALLEL to the highway
-- (travelling far along it while climbing), or does it dive off the side?
-- Measures longitudinal progress (in mainline samples) vs vertical drop vs
-- lateral drift, plus the actual grade. Also finds what the ramp passes over.
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local samples = Circuit.samples
local map = ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local out = {}
local function say(s) table.insert(out, s) end
local ramps = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local name, idx = d.Name:match("^(PA%a+Slip)_Deck_(%d+)$")
if name then
ramps[name] = ramps[name] or {}
table.insert(ramps[name], { i = tonumber(idx), p = d })
end
end
end
local function nearestSample(p)
local bd, bi = math.huge, -1
for i = 1, #samples do
local s = samples[i]
local dd = (Vector3.new(p.X, 0, p.Z) - Vector3.new(s.pos.X, 0, s.pos.Z)).Magnitude
if dd < bd then bd, bi = dd, i end
end
return bi, bd
end
for _, name in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = ramps[name]
if not segs then say(name .. ": MISSING") continue end
table.sort(segs, function(x, y) return x.i < y.i end)
local first, last = segs[1].p, segs[#segs].p
local si = nearestSample(first.Position)
local ei = nearestSample(last.Position)
-- 3D path length along the ramp centreline.
local plen = 0
for k = 2, #segs do
plen += (segs[k].p.Position - segs[k - 1].p.Position).Magnitude
end
local drop = first.Position.Y - last.Position.Y
local flat = (Vector3.new(first.Position.X, 0, first.Position.Z)
- Vector3.new(last.Position.X, 0, last.Position.Z)).Magnitude
-- Sample spacing, to convert sample counts into studs.
local spacing = (samples[2].pos - samples[1].pos).Magnitude
local dsamples = math.abs(ei - si)
say(("=== %s ==="):format(name))
say((" segments: %d, seg length ~%.0f studs, path length %.0f studs")
:format(#segs, plen / math.max(1, #segs - 1), plen))
say((" mouth at sample %d -> end near sample %d (travels %d samples = %.0f studs ALONG the highway)")
:format(si, ei, dsamples, dsamples * spacing))
say((" vertical drop: %.1f studs horizontal run: %.0f studs grade: %.1f%%")
:format(drop, flat, 100 * drop / math.max(1, flat)))
say((" PARALLEL-ness: %.0f studs along-highway per %.0f studs of drop -> %s")
:format(dsamples * spacing, drop,
(dsamples * spacing) > drop * 4 and "runs alongside" or "DIVES OFF THE SIDE"))
end
-- What is underneath the ramp mid-span? (is it over water/air, or over the lot?)
local names = {}
for _, d in ipairs(ws:GetDescendants()) do
if d:IsA("BasePart") then
names[d.Name:gsub("_%d+$", "")] = true
end
end
local cats = {}
for k in pairs(names) do table.insert(cats, k) end
table.sort(cats)
say("part categories in world: " .. #cats)
say(" " .. table.concat(cats, ", "):sub(1, 900))
return table.concat(out, "\n")
+40
View File
@@ -0,0 +1,40 @@
-- Which Studio is the bridge actually driving, and can that place be PLAYED?
local out = {}
local function say(s) table.insert(out, s) end
say("place name: " .. tostring(game.Name))
say("PlaceId: " .. tostring(game.PlaceId))
say("GameId: " .. tostring(game.GameId))
local RS = game:GetService("RunService")
say("IsEdit: " .. tostring(RS:IsEdit()) .. " IsRunning: " .. tostring(RS:IsRunning()) ..
" IsClient: " .. tostring(RS:IsClient()) .. " IsServer: " .. tostring(RS:IsServer()))
-- Is the map present, and is there anything to actually spawn onto?
local m = workspace:FindFirstChild("Map")
say("workspace.Map: " .. (m and (#m:GetDescendants() .. " descendants") or "MISSING"))
say("SpawnLocations: ")
local spawns = 0
for _, d in ipairs(workspace:GetDescendants()) do
if d:IsA("SpawnLocation") then
spawns += 1
say((" %s at %s"):format(d:GetFullName(), tostring(d.Position)))
end
end
if spawns == 0 then say(" NONE -- a playtest has nowhere to spawn the character") end
-- StarterPlayer/StarterGui presence (a play session needs these to not error)
for _, svc in ipairs({ "StarterPlayer", "StarterGui", "StarterPack", "Players", "Lighting" }) do
local ok, s = pcall(function() return game:GetService(svc) end)
say(("service %s: %s"):format(svc, ok and "ok" or "FAIL"))
end
-- Rojo connection state: is the synced source actually in this place?
local SS = game:GetService("ServerScriptService")
local srv = SS:FindFirstChild("Server")
say("ServerScriptService.Server: " .. (srv and srv.ClassName or "MISSING"))
if srv then
local mb = srv:FindFirstChild("MapBuilder")
say(" MapBuilder present: " .. tostring(mb ~= nil))
end
return table.concat(out, "\n")
+104
View File
@@ -0,0 +1,104 @@
-- What do the slip ramps ACTUALLY do? Elevation + lateral profile per segment,
-- and do they intersect the mainline deck?
-- The user says: "on and off ramps go to the bottom and through the highway".
-- Measure that claim directly rather than assuming.
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local samples = Circuit.samples
local map = ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local out = {}
local function say(s) table.insert(out, s) end
-- Collect ramp deck segments in build order.
local ramps = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local name, idx = d.Name:match("^(PA%a+Slip)_Deck_(%d+)$")
if name then
ramps[name] = ramps[name] or {}
table.insert(ramps[name], { i = tonumber(idx), p = d })
end
end
end
-- Nearest mainline sample + signed lateral offset for a world point.
local function nearestSample(p)
local bd, bi = math.huge, -1
for i = 1, #samples do
local s = samples[i]
local dd = (Vector3.new(p.X, 0, p.Z) - Vector3.new(s.pos.X, 0, s.pos.Z)).Magnitude
if dd < bd then bd, bi = dd, i end
end
return bi, bd
end
local function lateralAt(i, p)
local n = #samples
local a = samples[((i - 1) % n) + 1]
local b = samples[(i % n) + 1]
local f = b.pos - a.pos
f = Vector3.new(f.X, 0, f.Z).Unit
local r = Vector3.new(-f.Z, 0, f.X)
local rel = Vector3.new(p.X - a.pos.X, 0, p.Z - a.pos.Z)
return rel:Dot(r), a.pos.Y
end
for _, name in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = ramps[name]
if not segs then
say(name .. ": MISSING")
else
table.sort(segs, function(x, y) return x.i < y.i end)
say(("=== %s (%d segs) ==="):format(name, #segs))
say("seg | rampY | deckY | dY(ramp-deck) | lateral | note")
local under = 0
for k, s in ipairs(segs) do
if k == 1 or k % 6 == 0 or k == #segs then
local si = nearestSample(s.p.Position)
local lat, deckY = lateralAt(si, s.p.Position)
local dy = s.p.Position.Y - deckY
local note = ""
-- |lateral| < 30 means it is horizontally UNDER/INSIDE the deck slab.
if math.abs(lat) < 30 then
note = (math.abs(dy) < 8) and "*** THROUGH DECK ***" or "under/over deck footprint"
end
say(("%3d | %6.1f | %6.1f | %+7.1f | %+7.1f | %s")
:format(s.i, s.p.Position.Y, deckY, dy, lat, note))
end
local si = nearestSample(s.p.Position)
local lat = lateralAt(si, s.p.Position)
if math.abs(lat) < 30 then under += 1 end
end
say(("%s: %d/%d segments sit within the deck footprint (|lateral| < 30)")
:format(name, under, #segs))
end
end
-- Does any ramp part actually overlap a Road part in 3D?
local roads = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^Road_%d+$") then
table.insert(roads, d)
end
end
local clashes, tested = {}, 0
for _, name in ipairs({ "PAWestSlip", "PAEastSlip" }) do
for _, s in ipairs(ramps[name] or {}) do
for _, r in ipairs(roads) do
tested += 1
local d = s.p.Position - r.Position
local sx = (s.p.Size.X + r.Size.X) / 2
local sy = (s.p.Size.Y + r.Size.Y) / 2
local sz = (s.p.Size.Z + r.Size.Z) / 2
if math.abs(d.X) < sx and math.abs(d.Y) < sy and math.abs(d.Z) < sz then
table.insert(clashes, ("%s x %s (dY=%.1f)"):format(s.p.Name, r.Name, d.Y))
end
end
end
end
say(("ramp-vs-road AABB tests: %d, overlaps: %d"):format(tested, #clashes))
for k = 1, math.min(#clashes, 12) do say(" " .. clashes[k]) end
if tested == 0 then say("CONTROL FAIL: nothing tested") end
return table.concat(out, "\n")
+62
View File
@@ -0,0 +1,62 @@
-- Rebuild the map from current source and report what actually landed.
-- The workspace snapshot predates today's edits; this regenerates it.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mb
for _, d in ipairs(SS:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "MapBuilder" then mb = d end
end
if not mb then return "MapBuilder NOT FOUND" end
local okReq, M = pcall(require, mb)
if not okReq then return "require failed: " .. tostring(M) end
local old = workspace:FindFirstChild("Map")
if old then
old:Destroy()
say("destroyed stale Map")
end
local t0 = os.clock()
local okB, err = pcall(function() return M.build() end)
say(("build: %s (%.2fs)"):format(okB and "OK" or ("FAILED -> " .. tostring(err)), os.clock() - t0))
if not okB then return table.concat(out, "\n") end
local map = workspace:FindFirstChild("Map")
if not map then return table.concat(out, "\n") .. "\nMap MISSING after build" end
local parts, slip, aux = 0, {}, 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
parts += 1
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
slip[s] = slip[s] or {}
table.insert(slip[s], { i = tonumber(k), y = d.Position.Y })
end
end
if d.Name:match("^AuxLane_%d+$") then aux += 1 end
end
end
say(("parts=%d auxLaneSegs=%d"):format(parts, aux))
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local g = slip[s]
if not g or #g == 0 then
say(s .. ": ABSENT")
else
table.sort(g, function(a, b) return a.i < b.i end)
local topY, level = g[1].y, 0
for _, seg in ipairs(g) do
if math.abs(seg.y - topY) < 0.5 then level += 1 else break end
end
say(("%s: segs=%d startY=%.1f endY=%.1f level-run=%d/%d %s"):format(
s, #g, topY, g[#g].y, level, #g,
level >= 12 and "<- PARALLEL ASCENT PRESENT" or "<- old diving profile"))
end
end
return table.concat(out, "\n")
+151
View File
@@ -0,0 +1,151 @@
-- READ-ONLY REGRESSION CHECK. Does NOT build. Inspects whatever is currently
-- in Studio and compares against every property we had verified as good, so we
-- can tell whether the last edits preserved the good version or damaged it.
--
-- Known-good baseline (measured earlier this session):
-- ramp PAEastSlip total bend 30.7deg, worst 1.3, over5=0, level run 20
-- ramp PAWestSlip total bend 34.4deg, worst 1.4, over5=0, level run 20
-- AuxLane parts = 30 (user's hand-edited count, NOT the source's 52)
-- unguarded deck edges = 0 across all 823 samples
-- parts ~= 16788
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING -- nothing built" end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local parts = 0
local deck, aux, bars, slipParts = {}, {}, {}, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
parts += 1
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
if k then deck[tonumber(k)] = d end
local a = d.Name:match("^AuxLane_(%d+)$")
if a then aux[tonumber(a)] = d end
if d.Name == "Barrier" or d.Name == "EdgeRail" then table.insert(bars, d) end
if d.Name:match("Slip") then table.insert(slipParts, d) end
end
end
say(("parts = %d (baseline ~16788)"):format(parts))
-- 1. RAMP SMOOTHNESS -- the thing the user cares most about right now
local rampOK = true
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then
say(s .. ": ABSENT <<< RAMP GONE")
rampOK = false
else
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local dth = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += dth
if dth > worst then worst = dth; worstAt = segs[j].i end
if dth > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
if over > 0 then rampOK = false end
say(("%s: segs=%d total=%.1fdeg worst=%.1f@%d over5=%d level=%d %s"):format(
s, #segs, total, worst, worstAt, over, level,
over == 0 and "OK" or "<<< KINKED"))
end
end
say(rampOK and ">> RAMPS STILL SMOOTH" or ">> RAMP REGRESSION")
-- 2. USER'S DELETED LANE -- must stay deleted
local auxN, inRange = 0, 0
for i in pairs(aux) do
auxN += 1
if i >= 525 and i <= 550 then inRange += 1 end
end
say(("AuxLane = %d (baseline 30) | inside deleted run 525..550 = %d (want 0) %s"):format(
auxN, inRange, (auxN == 30 and inRange == 0) and "OK" or "<<< REGRESSION"))
-- 3. BARRIER COVERAGE -- full loop
local holes = {}
local swept = 0
for i, d in pairs(deck) do
swept += 1
local right = d.CFrame.LookVector:Cross(UP).Unit
for _, sgn in ipairs({ -1, 1 }) do
local edge = d.Position + right * sgn * (d.Size.X / 2 + 2)
local bd = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - edge.Y) < 14 then
local dd = (flat(b.Position) - flat(edge)).Magnitude
if dd < bd then bd = dd end
end
end
if bd >= 12 then
local okb = false
local a = aux[i]
if a then
local outer = a.Position + right * sgn * (a.Size.X / 2 + 2)
local bd2 = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - outer.Y) < 14 then
local dd = (flat(b.Position) - flat(outer)).Magnitude
if dd < bd2 then bd2 = dd end
end
end
if bd2 < 14 then okb = true end
end
if not okb then
local sd = math.huge
for _, p in ipairs(slipParts) do
local dd = (flat(p.Position) - flat(edge)).Magnitude
if dd < sd then sd = dd end
end
if sd > 30 then table.insert(holes, i .. (sgn < 0 and "L" or "R")) end
end
end
end
end
say(("barrier sweep: %d samples, unguarded=%d %s"):format(swept, #holes,
#holes == 0 and "OK" or ("<<< " .. table.concat(holes, ","))))
-- 4. FENCE JOG at the suppression boundary
local prevLat, jogs = nil, {}
for i = 505, 570 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local best, bestLat = math.huge, nil
for _, b in ipairs(bars) do
if b.Name == "Barrier" and math.abs(b.Position.Y - d.Position.Y) < 14 then
local rel = b.Position - d.Position
local along = math.abs(rel:Dot(d.CFrame.LookVector))
local lat = rel:Dot(right)
if along < 22 and lat < 0 and along < best then best = along; bestLat = lat end
end
end
if bestLat then
if prevLat then
local step = math.abs(math.abs(bestLat) - math.abs(prevLat))
if step > 10 then table.insert(jogs, ("%d(%.1f)"):format(i, step)) end
end
prevLat = bestLat
else
prevLat = nil
end
end
end
say(("fence jogs >10 studs: %d %s"):format(#jogs,
#jogs == 0 and "OK" or ("<<< " .. table.concat(jogs, " "))))
return table.concat(out, "\n")
+74
View File
@@ -0,0 +1,74 @@
-- "Changes were reverted" -- WHERE? Distinguish three separate states that can
-- each independently be stale:
-- (a) the MapBuilder SOURCE inside Studio (Rojo sync state)
-- (b) the BUILT geometry sitting in workspace (may predate the source)
-- (c) the place file itself
local out = {}
local function say(s) table.insert(out, s) end
say("place: " .. tostring(game.Name))
-- (a) SOURCE state in Studio
local SS = game:GetService("ServerScriptService")
local mb
for _, d in ipairs(SS:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "MapBuilder" then mb = d end
end
if not mb then
say("SOURCE: MapBuilder NOT FOUND in ServerScriptService")
else
local ok, src = pcall(function() return mb.Source end)
if ok and type(src) == "string" then
say(("SOURCE: len=%d PARALLEL_RUN=%s PA_DEC_SPAN=%s PA_SLIP=%s"):format(
#src,
tostring(src:find("PARALLEL_RUN") ~= nil),
tostring(src:find("PA_DEC_SPAN") ~= nil),
tostring(src:find("local PA_SLIP") ~= nil)))
else
say("SOURCE: unreadable " .. tostring(src))
end
end
-- (b) BUILT geometry actually in the workspace right now
local map = workspace:FindFirstChild("Map")
if not map then
say("BUILT: workspace.Map MISSING")
return table.concat(out, "\n")
end
local slipSegs, auxSegs = {}, 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
slipSegs[s] = slipSegs[s] or {}
table.insert(slipSegs[s], { i = tonumber(k), p = d.Position })
end
end
if d.Name:match("^AuxLane_%d+$") then auxSegs += 1 end
end
end
say("BUILT: aux lane segments = " .. auxSegs .. " (expect ~52 with decel+accel)")
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local g = slipSegs[s]
if not g or #g == 0 then
say(("BUILT: %s -- NO DECK SEGMENTS (ramp absent)"):format(s))
else
table.sort(g, function(a, b) return a.i < b.i end)
-- How many segments hold deck height before the descent starts?
local topY = g[1].p.Y
local level = 0
for _, seg in ipairs(g) do
if math.abs(seg.p.Y - topY) < 0.5 then level += 1 else break end
end
local drop = topY - g[#g].p.Y
say(("BUILT: %s segs=%d startY=%.1f endY=%.1f drop=%.1f level-run=%d/%d %s"):format(
s, #g, topY, g[#g].p.Y, drop, level, #g,
level >= 12 and "<- PARALLEL FIX PRESENT" or "<- OLD DIVING PROFILE"))
end
end
return table.concat(out, "\n")
+61
View File
@@ -0,0 +1,61 @@
-- Does the game actually RUN? Exercise the real server startup path and report
-- the first genuine error, rather than guessing from static analysis.
local RS = game:GetService("ReplicatedStorage")
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
-- 1. Are the Rojo-synced trees actually present in the DataModel?
local shared = RS:FindFirstChild("Shared")
say("ReplicatedStorage.Shared: " .. (shared and "present" or "MISSING"))
if shared then
local names = {}
for _, c in ipairs(shared:GetChildren()) do table.insert(names, c.Name) end
table.sort(names)
say(" modules: " .. table.concat(names, ", "))
end
local server = SS:FindFirstChild("Server") or SS:FindFirstChild("TS")
say("ServerScriptService children:")
for _, c in ipairs(SS:GetChildren()) do
say((" %s (%s)"):format(c.Name, c.ClassName))
end
-- 2. Try requiring every shared + server module. Report the FIRST failure with
-- its message -- this is what actually stops a playtest.
local function tryRequire(inst)
local ok, err = pcall(function() return require(inst) end)
return ok, err
end
say("--- require sweep ---")
local failures = 0
local tested = 0
for _, root in ipairs({ shared, server }) do
if root then
for _, m in ipairs(root:GetDescendants()) do
if m:IsA("ModuleScript") then
tested += 1
local ok, err = tryRequire(m)
if not ok then
failures += 1
say(("FAIL %s: %s"):format(m:GetFullName(), tostring(err):sub(1, 300)))
end
end
end
end
end
say(("required %d modules, %d failed"):format(tested, failures))
if tested == 0 then say("CONTROL FAIL: nothing was required -- wrong scope") end
-- 3. Script instances that would run on play, and whether any are disabled.
say("--- scripts ---")
for _, d in ipairs(game:GetDescendants()) do
if (d:IsA("Script") or d:IsA("LocalScript")) and not d:IsDescendantOf(workspace) then
if d.Name:match("init") or d:IsDescendantOf(SS) then
say((" %s (%s) disabled=%s"):format(d:GetFullName(), d.ClassName, tostring(d.Disabled)))
end
end
end
return table.concat(out, "\n")
+88
View File
@@ -0,0 +1,88 @@
-- Which side of the deck does each slip + aux lane sit on?
-- Answers: are both existing slips on the SAME carriageway (so "the opposite"
-- means the other carriageway), or is one an entry and one an exit?
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local map = ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local samples = Circuit.samples
local out = {}
local function say(s) table.insert(out, s) end
-- Signed lateral offset of a world point from the mainline at sample i.
-- Positive = one side, negative = the other. Uses the deck's own right vector.
local function sideOf(i, p)
local n = #samples
local a = samples[((i - 1) % n) + 1]
local b = samples[(i % n) + 1]
local fwd = (b.pos - a.pos)
fwd = Vector3.new(fwd.X, 0, fwd.Z).Unit
local right = Vector3.new(-fwd.Z, 0, fwd.X)
local rel = Vector3.new(p.X - a.pos.X, 0, p.Z - a.pos.Z)
return rel:Dot(right)
end
local SLIPS = { { "PAWestSlip", 525, 537 }, { "PAEastSlip", 551, 539 } }
for _, s in ipairs(SLIPS) do
local name, join, land = s[1], s[2], s[3]
local first, last, nseg = nil, nil, 0
local maxI = -1
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = tonumber(d.Name:match("^" .. name .. "_Deck_(%d+)$") or "")
if k then
nseg += 1
if k <= 1 then first = d end
if k > maxI then maxI, last = k, d end
end
end
end
if not first then
say(name .. ": FAIL no deck parts")
else
local lat = sideOf(join, first.Position)
say(("%s: segs=%d mouth lateral %+.1f (%s side) mouthY=%.1f landY=%.1f -> %s")
:format(name, nseg, lat, lat > 0 and "RIGHT" or "LEFT",
first.Position.Y, last and last.Position.Y or -1,
(last and last.Position.Y < first.Position.Y) and "DESCENDS (exit/off-ramp)"
or "CLIMBS (entry/on-ramp)"))
end
end
-- Aux lanes: which side, grouped by contiguous sample index.
local aux = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local ai = tonumber(d.Name:match("^AuxLane_(%d+)$") or "")
if ai then table.insert(aux, { i = ai, p = d.Position }) end
end
end
table.sort(aux, function(x, y) return x.i < y.i end)
if #aux == 0 then
say("AUX: FAIL none built")
else
local groups, cur = {}, { aux[1] }
for k = 2, #aux do
if aux[k].i - aux[k - 1].i > 3 then
table.insert(groups, cur); cur = {}
end
table.insert(cur, aux[k])
end
table.insert(groups, cur)
for gi, g in ipairs(groups) do
local mid = g[math.max(1, math.floor(#g / 2))]
local lat = sideOf(mid.i, mid.p)
say(("AUX group %d: samples %d..%d (%d segs) lateral %+.1f (%s side)")
:format(gi, g[1].i, g[#g].i, #g, lat, lat > 0 and "RIGHT" or "LEFT"))
end
end
-- Does the deck carry traffic both ways, and is there a median?
local med = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^Median") then med += 1 end
end
say(("median parts: %d (deck is one slab split into two carriageways)"):format(med))
return table.concat(out, "\n")
+109
View File
@@ -0,0 +1,109 @@
-- Measure the envelope a SIMPLE straight slip-ramp must live in.
-- User's drawing: short stubs peeling off the mainline at a shallow angle,
-- running roughly PARALLEL to the highway in the strip between the mainline
-- edge and the PA lot's north edge, descending an S onto the apron.
--
-- We need to know, in real numbers:
-- 1. Where the mainline actually runs across the PA frontage (samples,
-- heading, deck Y) -- is it straight enough for a parallel slip?
-- 2. Where the lot's north edge is, and how wide the clear strip is
-- between the mainline's south edge and that lot edge.
-- 3. Deck Y vs apron Y (the height the S-curve must lose).
-- 4. What is already sitting in that strip that a ramp would hit.
local RS = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local samples = Circuit.samples
local n = #samples
local c = Circuit.PA_CENTER
-- Nearest mainline sample to the PA centre.
local bestIdx, bestD = 1, math.huge
for i = 1, n do
local p = samples[i].pos
local d = (Vector3.new(p.X, 0, p.Z) - Vector3.new(c.X, 0, c.Z)).Magnitude
if d < bestD then bestD, bestIdx = d, i end
end
-- Walk a band around the PA frontage and report heading + deck Y, so we can
-- see how straight the mainline is and where a parallel slip could sit.
local band = {}
for k = -40, 40, 5 do
local i = ((bestIdx - 1 + k) % n) + 1
local s = samples[i].pos
local nx = samples[(i % n) + 1].pos
local hdg = math.deg(math.atan2(nx.Z - s.Z, nx.X - s.X))
local d = (Vector3.new(s.X, 0, s.Z) - Vector3.new(c.X, 0, c.Z)).Magnitude
table.insert(band, {
k = k, idx = i,
x = math.floor(s.X), y = math.floor(s.Y + 0.5), z = math.floor(s.Z),
hdg = math.floor(hdg + 0.5),
distPA = math.floor(d),
})
end
-- The apron / lot floor: bounds and surface height.
local map = workspace:FindFirstChild("Map")
local floor = map and map:FindFirstChild("PAFloor", true)
local lot = nil
if floor then
lot = {
name = floor.Name,
surfaceY = floor.Position.Y + floor.Size.Y / 2,
x_min = math.floor(floor.Position.X - floor.Size.X / 2),
x_max = math.floor(floor.Position.X + floor.Size.X / 2),
z_min = math.floor(floor.Position.Z - floor.Size.Z / 2),
z_max = math.floor(floor.Position.Z + floor.Size.Z / 2),
}
end
-- What already occupies the strip between the mainline and the lot's north
-- edge? Anything here is a collision candidate for a parallel slip ramp.
-- Strip = north third of the lot bbox, i.e. Z above (z_max - 220).
local obstacles = {}
if lot then
local stripZ = lot.z_max - 220
for _, d in ipairs(workspace:GetDescendants()) do
if d:IsA("BasePart") then
local p = d.Position
if p.Z >= stripZ and p.Z <= lot.z_max + 160
and p.X >= lot.x_min - 60 and p.X <= lot.x_max + 60 then
local key = d.Name
local e = obstacles[key]
if not e then
e = { n = 0, x_min = 1e9, x_max = -1e9, z_min = 1e9,
z_max = -1e9, y_max = -1e9 }
obstacles[key] = e
end
e.n += 1
e.x_min = math.min(e.x_min, p.X); e.x_max = math.max(e.x_max, p.X)
e.z_min = math.min(e.z_min, p.Z); e.z_max = math.max(e.z_max, p.Z)
e.y_max = math.max(e.y_max, p.Y + d.Size.Y / 2)
end
end
end
end
local obsOut = {}
for k, e in pairs(obstacles) do
table.insert(obsOut, {
name = k, n = e.n,
x = math.floor(e.x_min) .. ".." .. math.floor(e.x_max),
z = math.floor(e.z_min) .. ".." .. math.floor(e.z_max),
ytop = math.floor(e.y_max),
})
end
table.sort(obsOut, function(a, b) return a.n > b.n end)
while #obsOut > 25 do table.remove(obsOut) end
return HttpService:JSONEncode({
bestIdx = bestIdx,
nearestDist = math.floor(bestD),
pa_center = { math.floor(c.X), math.floor(c.Y), math.floor(c.Z) },
pa_radius = Circuit.PA_RADIUS,
band = band,
lot = lot,
strip_obstacles = obsOut,
})
+69
View File
@@ -0,0 +1,69 @@
-- Rebuild with the smoothing pass, then re-measure the SAME metrics as before
-- so the before/after is directly comparable. Target: east total heading change
-- down from 216deg toward the west's ~56deg, and ZERO segments turning >5deg
-- (the 58.2/50.1deg hinges must be gone). Also verify the parallel run and
-- the endpoints did not move -- smoothing must not detach the mouth.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mod = SS:FindFirstChild("MapBuilder", true)
if not mod then return "MapBuilder module NOT FOUND" end
local ok, MB = pcall(require, mod)
if not ok then return "require failed: " .. tostring(MB) end
local t0 = os.clock()
local ok2, err = pcall(function() MB.build() end)
if not ok2 then return "build ERROR: " .. tostring(err) end
say(("build OK (%.2fs)"):format(os.clock() - t0))
local map = workspace:FindFirstChild("Map")
if not map then return "Map missing after build" end
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d"):format(parts))
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then say(s .. ": ABSENT") continue end
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local d = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += d
if d > worst then worst = d; worstAt = segs[j].i end
if d > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
local lmin, lmax = math.huge, -math.huge
for j = 2, #segs do
local L = (segs[j].p - segs[j - 1].p).Magnitude
lmin = math.min(lmin, L); lmax = math.max(lmax, L)
end
say(("%s: segs=%d total=%.1fdeg mean=%.2f worst=%.1f@%d over5=%d"):format(
s, #segs, total, total / (#segs - 1), worst, worstAt, over))
say((" level-run=%d startY=%.1f endY=%.1f spacing=%.1f..%.1f"):format(
level, segs[1].p.Y, segs[#segs].p.Y, lmin, lmax))
say((" start=(%.0f,%.0f,%.0f) end=(%.0f,%.0f,%.0f)"):format(
segs[1].p.X, segs[1].p.Y, segs[1].p.Z, segs[#segs].p.X, segs[#segs].p.Y, segs[#segs].p.Z))
end
return table.concat(out, "\n")
+56
View File
@@ -0,0 +1,56 @@
-- What is actually on screen right now? Don't assume "missing" means empty --
-- distinguish (a) no Map at all, (b) Map present but stripped, (c) Map fine.
local out = {}
local function say(s) table.insert(out, s) end
say("place: " .. tostring(game.Name))
local map = workspace:FindFirstChild("Map")
if not map then
say("workspace.Map: MISSING ENTIRELY")
else
local parts, total = 0, 0
for _, d in ipairs(map:GetDescendants()) do
total += 1
if d:IsA("BasePart") then parts += 1 end
end
say(("workspace.Map: present descendants=%d parts=%d"):format(total, parts))
local kids = {}
for _, c in ipairs(map:GetChildren()) do
local n = 0
for _, _d in ipairs(c:GetDescendants()) do n += 1 end
table.insert(kids, ("%s(%d)"):format(c.Name, n))
end
say("Map children: " .. (#kids > 0 and table.concat(kids, ", ") or "NONE"))
end
-- Is anything else sitting loose in workspace?
local loose = {}
for _, c in ipairs(workspace:GetChildren()) do
if c:IsA("BasePart") or c:IsA("Model") or c:IsA("Folder") then
table.insert(loose, c.Name)
end
if #loose > 15 then break end
end
say("workspace top-level: " .. table.concat(loose, ", "))
-- Where is the camera looking? If the map exists but the camera is parked in
-- empty space, "gone from my screen" means a camera problem, not a build one.
local cam = workspace.CurrentCamera
if cam then
say(("camera pos=(%.0f, %.0f, %.0f)"):format(cam.CFrame.Position.X, cam.CFrame.Position.Y, cam.CFrame.Position.Z))
end
-- Spawn / reference point so we know where the map SHOULD be.
local ref
for _, d in ipairs(workspace:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("Deck") then ref = d break end
end
if ref then
say(("sample deck part '%s' at (%.0f, %.0f, %.0f)"):format(ref.Name, ref.Position.X, ref.Position.Y, ref.Position.Z))
else
say("no Deck part found anywhere in workspace")
end
return table.concat(out, "\n")
+39
View File
@@ -0,0 +1,39 @@
-- Is Studio actually CONNECTED to the Rojo server, or is it showing a stale
-- snapshot? A place can contain MapBuilder from an old sync and still be
-- disconnected, which is how you end up testing yesterday's geometry.
local out = {}
local function say(s) table.insert(out, s) end
-- Rojo's plugin leaves session state in the DataModel; look for its markers.
local found = {}
for _, d in ipairs(game:GetDescendants()) do
local n = d.Name
if n:match("Rojo") or n:match("ROJO") then
table.insert(found, ("%s (%s)"):format(d:GetFullName(), d.ClassName))
end
if #found > 12 then break end
end
say("Rojo-named instances: " .. (#found > 0 and table.concat(found, ", ") or "NONE FOUND"))
-- Does the in-place MapBuilder source contain the fix I just made? This is the
-- real test of whether the running place is current.
local SS = game:GetService("ServerScriptService")
local mb
for _, d in ipairs(SS:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "MapBuilder" then mb = d end
end
if mb then
local ok, src = pcall(function() return mb.Source end)
if ok and type(src) == "string" then
say("MapBuilder source length: " .. #src)
say("has PARALLEL_RUN (today's ramp fix): " .. tostring(src:find("PARALLEL_RUN") ~= nil))
say("has PA_AUX_DECEL (decel lanes): " .. tostring(src:find("PA_AUX_DECEL") ~= nil))
else
say("could not read MapBuilder.Source (permission): " .. tostring(src))
end
else
say("MapBuilder NOT FOUND")
end
say("place: " .. tostring(game.Name))
return table.concat(out, "\n")
+78
View File
@@ -0,0 +1,78 @@
-- The user hand-edited ramps in Studio. Their work exists ONLY in the live
-- workspace, not in MapBuilder source. Before touching anything, measure what
-- is actually there now and how it differs from what source would generate.
local out = {}
local function say(s) table.insert(out, s) end
say("place: " .. tostring(game.Name))
local map = workspace:FindFirstChild("Map")
if not map then return "workspace.Map MISSING" end
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("Map parts=%d (source rebuild produced 16820)"):format(parts))
-- Slip deck geometry as it stands RIGHT NOW.
local slip = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
slip[s] = slip[s] or {}
table.insert(slip[s], {
i = tonumber(k), pos = d.Position, size = d.Size,
cf = d.CFrame, inst = d,
})
end
end
end
end
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local g = slip[s]
if not g or #g == 0 then
say(s .. ": ABSENT")
else
table.sort(g, function(a, b) return a.i < b.i end)
local topY, level = g[1].pos.Y, 0
for _, seg in ipairs(g) do
if math.abs(seg.pos.Y - topY) < 0.5 then level += 1 else break end
end
-- widths and gaps tell us if the user reshaped it
local wmin, wmax = math.huge, -math.huge
local maxGap, gapAt = 0, -1
for j, seg in ipairs(g) do
wmin = math.min(wmin, seg.size.X); wmax = math.max(wmax, seg.size.X)
if j > 1 then
local d2 = (seg.pos - g[j - 1].pos).Magnitude
if d2 > maxGap then maxGap = d2; gapAt = seg.i end
end
end
say(("%s: segs=%d idx[%d..%d] startY=%.1f endY=%.1f level=%d width=%.1f..%.1f maxGap=%.1f@%d"):format(
s, #g, g[1].i, g[#g].i, topY, g[#g].pos.Y, level, wmin, wmax, maxGap, gapAt))
end
end
-- What barrier-ish parts exist, and on what? Name survey first so I know the
-- real naming scheme before I go hunting for "missing" ones.
local kinds = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local n = d.Name:gsub("_%d+$", "_#")
if n:lower():match("barrier") or n:lower():match("parapet") or n:lower():match("rail")
or n:lower():match("wall") or n:lower():match("guard") or n:lower():match("kerb")
or n:lower():match("curb") or n:lower():match("fence") then
kinds[n] = (kinds[n] or 0) + 1
end
end
end
local list = {}
for k, v in pairs(kinds) do table.insert(list, ("%s=%d"):format(k, v)) end
table.sort(list)
say("BARRIER-LIKE PART NAMES: " .. (#list > 0 and table.concat(list, " ") or "NONE"))
return table.concat(out, "\n")
+120
View File
@@ -0,0 +1,120 @@
-- User's picture marks TWO defects on the slip ramps:
-- (A) left tall oval -> a stretch of ramp edge with NO barrier at all
-- (B) right circle -> the OUTER barrier "sticks out" / juts, not smooth
-- Earlier measurement hinted at (A): PAEastSlip had walls=119 where west had
-- 120, i.e. one wall segment short. Find the truth for both slips now.
--
-- Method:
-- 1. Index every <slip>_Wall_<k> by k and by side (left/right of centreline).
-- Missing k on a side = a hole in that parapet.
-- 2. For each wall, compute SIGNED lateral offset from its own deck segment
-- centre. Walls are built at +/-(WIDTH/2 - 0.6); any outlier is a jut.
-- 3. Measure end-to-end gaps between consecutive walls on the same side --
-- a protruding corner shows as a large lateral step between neighbours.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local UP = Vector3.new(0, 1, 0)
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local decks, walls = {}, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local dk = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if dk then decks[tonumber(dk)] = d end
local wk = d.Name:match("^" .. s .. "_Wall_(%d+)$")
if wk then
local k = tonumber(wk)
walls[k] = walls[k] or {}
table.insert(walls[k], d)
end
end
end
local idx = {}
for k in pairs(decks) do table.insert(idx, k) end
table.sort(idx)
if #idx == 0 then say(s .. ": no decks") continue end
say(("=== %s: decks=%d (idx %d..%d) ==="):format(s, #idx, idx[1], idx[#idx]))
-- (1) missing walls
local missing, oneSided = {}, {}
for _, k in ipairs(idx) do
local w = walls[k]
if not w then
table.insert(missing, k)
elseif #w < 2 then
table.insert(oneSided, k)
end
end
say((" walls: missing-both=%d %s | only-one-side=%d %s"):format(
#missing, #missing > 0 and table.concat(missing, ",") or "-",
#oneSided, #oneSided > 0 and table.concat(oneSided, ",") or "-"))
-- (2) signed lateral offset of each wall from its deck centre
local offs = {}
local worstOut, worstAt, worstSide = 0, -1, ""
for _, k in ipairs(idx) do
local dk = decks[k]
local w = walls[k]
if dk and w then
local right = dk.CFrame.LookVector:Cross(UP).Unit
for _, wp in ipairs(w) do
local rel = wp.Position - dk.Position
local lat = rel:Dot(right)
table.insert(offs, { k = k, lat = lat, side = lat < 0 and "L" or "R" })
end
end
end
-- expected magnitude = median |lat|
local mags = {}
for _, o in ipairs(offs) do table.insert(mags, math.abs(o.lat)) end
table.sort(mags)
local med = mags[math.max(1, math.floor(#mags / 2))] or 0
say((" median |lateral offset| = %.2f (walls sit this far off centre)"):format(med))
local juts = {}
for _, o in ipairs(offs) do
local dev = math.abs(o.lat) - med
if dev > 1.5 then
table.insert(juts, ("%d%s+%.1f"):format(o.k, o.side, dev))
if dev > worstOut then worstOut = dev; worstAt = o.k; worstSide = o.side end
end
end
say((" walls deviating >1.5 from median: %d %s"):format(#juts,
#juts > 0 and table.concat(juts, " ") or "(none)"))
if worstAt > 0 then
say((" worst jut: seg %d side %s, +%.1f studs outboard"):format(worstAt, worstSide, worstOut))
end
-- (3) lateral step between consecutive walls on the SAME side
local bySide = { L = {}, R = {} }
for _, o in ipairs(offs) do table.insert(bySide[o.side], o) end
for _, sd in ipairs({ "L", "R" }) do
local a = bySide[sd]
table.sort(a, function(x, y) return x.k < y.k end)
local bigStep, stepAt = 0, -1
for j = 2, #a do
local st = math.abs(math.abs(a[j].lat) - math.abs(a[j - 1].lat))
if st > bigStep then bigStep = st; stepAt = a[j].k end
end
say((" side %s: n=%d largest lateral step=%.2f at seg %d"):format(sd, #a, bigStep, stepAt))
end
-- world position of any missing/one-sided spot so I can match it to the pic
for _, k in ipairs(oneSided) do
local dk = decks[k]
if dk then
local w = walls[k][1]
local right = dk.CFrame.LookVector:Cross(UP).Unit
local sd = (w.Position - dk.Position):Dot(right) < 0 and "L" or "R"
say((" >> seg %d has ONLY side %s, at (%.0f,%.0f,%.0f)"):format(
k, sd, dk.Position.X, dk.Position.Y, dk.Position.Z))
end
end
end
return table.concat(out, "\n")
+123
View File
@@ -0,0 +1,123 @@
-- Is the fused aux run genuine continuous pavement (a weaving section), or
-- overlapping/duplicated parts? Checks per-sample coverage, duplicate indices,
-- and the width profile across both mouths.
local ws = game:GetService("Workspace")
local map = ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local seen, dupes = {}, 0
local byIdx = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local i = tonumber(d.Name:match("^AuxLane_(%d+)$") or "")
if i then
if seen[i] then dupes += 1 end
seen[i] = true
byIdx[i] = d
end
end
end
local out = {}
local function say(s) table.insert(out, s) end
say(("duplicate AuxLane indices: %d -> %s"):format(dupes, dupes == 0 and "PASS" or "FAIL"))
-- Walk 505..570 and report which samples have lane and how wide.
local missing = {}
local prof = {}
for i = 505, 570 do
local p = byIdx[i]
if p then
table.insert(prof, ("%d:%.0f"):format(i, p.Size.X))
else
table.insert(missing, i)
table.insert(prof, ("%d:--"):format(i))
end
end
say("width profile (sample:width, -- = no lane):")
say(" " .. table.concat(prof, " "))
say("gaps in 505..570: " .. (#missing > 0 and table.concat(missing, ",") or "none"))
-- The only gaps should be the ramp noses (the ramp deck covers those).
-- Derive coverage from the ACTUAL ramp deck parts, but test the point where
-- the AUX LANE would sit (deck edge + half lane width) -- NOT the mainline
-- centreline. Testing proximity to the centreline marked 26 samples as "nose"
-- because the ramp spirals past them at other heights, which would mask any
-- real hole and make this check vacuously pass. The nose test must also match
-- in HEIGHT: a ramp 50 studs below the deck does not cover the deck's edge.
local samples = require(game:GetService("ReplicatedStorage")
:WaitForChild("Shared"):WaitForChild("Circuit")).samples
local decks = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("Slip_Deck_") then
table.insert(decks, d)
end
end
local HALF, AUXW, SIDE = 30, 26, -1
local function auxPointAt(i)
local n = #samples
local a = samples[((i - 1) % n) + 1]
local b = samples[(i % n) + 1]
local f = b.pos - a.pos
f = Vector3.new(f.X, 0, f.Z).Unit
local r = Vector3.new(-f.Z, 0, f.X)
return a.pos + r * (SIDE * (HALF + AUXW / 2)), a.pos.Y
end
local NOSE, noseWhy = {}, {}
for _, i in ipairs(missing) do
local p, deckY = auxPointAt(i)
for _, d in ipairs(decks) do
local flat = (Vector3.new(d.Position.X, 0, d.Position.Z)
- Vector3.new(p.X, 0, p.Z)).Magnitude
local dy = math.abs(d.Position.Y - deckY)
-- Must overlap the lane footprint horizontally AND be at deck level.
if flat <= AUXW and dy <= 6 then
NOSE[i] = true
noseWhy[i] = ("%s flat=%.0f dy=%.0f"):format(d.Name, flat, dy)
break
end
end
end
local noseList = {}
for i in pairs(NOSE) do table.insert(noseList, i) end
table.sort(noseList)
say(("ramp-nose-covered gaps (%d): %s"):format(#noseList, table.concat(noseList, ",")))
for _, i in ipairs(noseList) do
say((" %d covered by %s"):format(i, noseWhy[i]))
end
local lo, hi = math.huge, -1
for k in pairs(byIdx) do
lo = math.min(lo, k); hi = math.max(hi, k)
end
local bad = {}
for _, i in ipairs(missing) do
-- Samples outside the lane's own extent are not holes; only flag gaps that
-- sit strictly between the first and last aux segment.
if i > lo and i < hi and not NOSE[i] then table.insert(bad, i) end
end
say(("lane run extent: %d..%d"):format(lo, hi))
say(("non-nose gaps inside the lane run: %s -> %s"):format(#bad > 0 and table.concat(bad, ",") or "none",
#bad == 0 and "PASS (only ramp noses uncovered)" or "FAIL (hole in pavement)"))
-- Between the two mouths, is the lane continuously full width (a real weaving
-- section) rather than tapering out and back in?
local weaveMin = math.huge
local weaveTested = 0
for i = 528, 548 do
local p = byIdx[i]
if p then
weaveTested += 1
weaveMin = math.min(weaveMin, p.Size.X)
end
end
if weaveTested == 0 then
say("weave section: FAIL nothing tested")
else
say(("weave section 528..548: %d segs, min width %.1f -> %s")
:format(weaveTested, weaveMin,
weaveMin >= 24 and "PASS (continuous full-width weaving lane)" or "TAPERS"))
end
return table.concat(out, "\n")
+194
View File
@@ -0,0 +1,194 @@
-- Verify the speed-change (auxiliary) lanes and that the slips no longer cut
-- across mainline traffic.
--
-- Rebuilds from a CLONED module first (require() cache bypass), then measures.
-- Every check below can FAIL -- and asserts it actually tested something, so a
-- lookup returning nothing reports FAIL rather than vacuously passing.
local sss = game:GetService("ServerScriptService")
local ss = game:GetService("ServerStorage")
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local realMod = sss:FindFirstChild("MapBuilder", true)
if not realMod then return "MapBuilder not found" end
local bust = ss:FindFirstChild("_cachebust")
if not bust then
bust = Instance.new("Folder")
bust.Name = "_cachebust"
bust.Parent = ss
end
for _, ch in ipairs(bust:GetChildren()) do
ch:Destroy()
end
local clone = realMod:Clone()
clone.Name = "MapBuilder_aux_" .. tostring(math.random(1e6, 9e6))
clone.Parent = bust
local mb = require(clone)
local old = ws:FindFirstChild("Map")
if old then old:Destroy() end
local ok, err = pcall(function() mb.build() end)
if not ok then return "BUILD ERROR: " .. tostring(err) end
local map = ws:FindFirstChild("Map")
if not map then return "no Map after build" end
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local out = {}
local function say(s) table.insert(out, s) end
-- Collect the aux lane decks and the slip decks.
local aux = {}
local decks = { PAWestSlip = {}, PAEastSlip = {} }
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local ai = d.Name:match("^AuxLane_(%d+)$")
if ai then
table.insert(aux, { i = tonumber(ai), p = d.Position, s = d.Size, cf = d.CFrame })
end
for slipName, list in pairs(decks) do
local idx = d.Name:match("^" .. slipName .. "_Deck_(%d+)$")
if idx then
table.insert(list, { i = tonumber(idx), p = d.Position, s = d.Size, cf = d.CFrame })
end
end
end
end
-- 1. The aux lanes must exist at all.
if #aux == 0 then
say("FAIL: no AuxLane_* parts were built")
return table.concat(out, "\n")
end
say(("aux lane segments built: %d"):format(#aux))
-- 2. Every aux segment must sit OUTSIDE the mainline deck, not on top of it.
-- Measure the gap between the mainline centreline and the aux centreline,
-- and compare against that sample's own half-width.
local samples = Circuit.samples
local minClear, minAt = math.huge, -1
local tested = 0
for _, a in ipairs(aux) do
local s = samples[a.i]
if s then
tested += 1
local d = (Vector3.new(a.p.X, 0, a.p.Z) - Vector3.new(s.pos.X, 0, s.pos.Z)).Magnitude
-- inner edge of aux lane relative to mainline centre
local innerEdge = d - a.s.X / 2
if innerEdge < minClear then
minClear, minAt = innerEdge, a.i
end
end
end
if tested == 0 then
say("FAIL: no aux segment matched a circuit sample (harness looked up nothing)")
else
say(("aux inner edge vs mainline centre: min %.1f studs at sample %d (tested %d)")
:format(minClear, minAt, tested))
end
-- 3. The slip decks must START at the aux lane offset, not on the centreline.
-- This is the actual bug the user reported: lat began at 0.
for slipName, list in pairs(decks) do
table.sort(list, function(x, y) return x.i < y.i end)
if #list < 50 then
say(("%s FAIL: only %d deck segments"):format(slipName, #list))
else
local first = list[1]
-- Find the nearest mainline sample to the slip's first segment.
local bestD, bestI = math.huge, -1
for i = 1, #samples do
local d = (Vector3.new(first.p.X, 0, first.p.Z) - Vector3.new(samples[i].pos.X, 0, samples[i].pos.Z)).Magnitude
if d < bestD then bestD, bestI = d, i end
end
local halfW = 0
do
-- Deck half-width at that sample, read off the built road itself.
local road = map:FindFirstChild("Road_" .. bestI, true)
halfW = road and road.Size.X / 2 or 0
end
local innerEdge = bestD - first.s.X / 2
local verdict = (halfW > 0 and innerEdge >= halfW - 1.5) and "PASS" or "FAIL"
say(("%s start: centre %.1f from mainline centre, inner edge %.1f, deck half-width %.1f -> %s")
:format(slipName, bestD, innerEdge, halfW, verdict))
end
end
-- 4. Continuity: the slip mouth and the nearest aux segment must be adjacent
-- (end-to-end), not overlapping and not separated by a hole.
for slipName, list in pairs(decks) do
if #list >= 50 then
local first = list[1]
local bestGap, bestI = math.huge, -1
for _, a in ipairs(aux) do
local g = (a.p - first.p).Magnitude
if g < bestGap then bestGap, bestI = g, a.i end
end
local verdict = bestGap <= 90 and "PASS" or "FAIL"
say(("%s mouth -> nearest aux segment: %.1f studs (sample %d) -> %s")
:format(slipName, bestGap, bestI, verdict))
end
end
-- 5. The aux lane must be level with the mainline deck at the mouth.
local maxStep, stepAt = 0, -1
for _, a in ipairs(aux) do
local road = map:FindFirstChild("Road_" .. a.i, true)
if road then
local step = math.abs((a.p.Y + a.s.Y / 2) - (road.Position.Y + road.Size.Y / 2))
if step > maxStep then maxStep, stepAt = step, a.i end
end
end
say(("aux/mainline surface step: max %.2f studs at sample %d -> %s")
:format(maxStep, stepAt, maxStep <= 0.05 and "PASS" or "FAIL"))
-- 6. The taper must actually taper: widths should fall to near zero at the end.
local wmin, wmax = math.huge, 0
for _, a in ipairs(aux) do
wmin = math.min(wmin, a.s.X)
wmax = math.max(wmax, a.s.X)
end
say(("aux width range: %.1f .. %.1f studs -> %s")
:format(wmin, wmax, (wmax >= 25 and wmin <= 8) and "PASS (full lane + taper)" or "CHECK"))
-- 7. DECELERATION LANES. Each mouth must now have lane on BOTH sides: one
-- upstream (decel, for traffic leaving) and one downstream (accel, for
-- traffic joining). Group the aux segments into runs and check each slip
-- mouth has a run on either side of it.
table.sort(aux, function(x, y) return x.i < y.i end)
local runs, cur = {}, { aux[1] }
for k = 2, #aux do
if aux[k].i - aux[k - 1].i > 3 then
table.insert(runs, cur); cur = {}
end
table.insert(cur, aux[k])
end
table.insert(runs, cur)
say(("aux runs: %d (expect 4 = 2 mouths x {decel, accel})"):format(#runs))
for ri, r in ipairs(runs) do
local wlo, whi = math.huge, 0
for _, a in ipairs(r) do
wlo = math.min(wlo, a.s.X); whi = math.max(whi, a.s.X)
end
say((" run %d: samples %d..%d (%d segs) width %.1f..%.1f")
:format(ri, r[1].i, r[#r].i, #r, wlo, whi))
end
-- Each slip join must sit between a decel run and an accel run.
for _, slip in ipairs({ { "PAWestSlip", 525 }, { "PAEastSlip", 551 } }) do
local name, join = slip[1], slip[2]
local before, after = false, false
for _, r in ipairs(runs) do
if r[#r].i < join and join - r[#r].i <= 6 then before = true end
if r[1].i > join and r[1].i - join <= 6 then after = true end
if r[1].i <= join and r[#r].i >= join then
-- run spans the join: counts as adjacent on both sides
before = true; after = true
end
end
say(("%s join=%d: decel-side run %s, accel-side run %s -> %s")
:format(name, join, tostring(before), tostring(after),
(before and after) and "PASS" or "FAIL"))
end
return table.concat(out, "\n")
+112
View File
@@ -0,0 +1,112 @@
-- Verify the linear slips against assertions that can FAIL.
-- Fixes the earlier harness bug: FindFirstChild must be RECURSIVE (PAFloor is
-- nested), otherwise `floor` is nil and every apron test vacuously reports
-- "does not reach" regardless of the real geometry.
local HttpService = game:GetService("HttpService")
local map = workspace:FindFirstChild("Map")
if not map then return "no Map -- run the rebuild first" end
local ramps = map:FindFirstChild("PARamps", true)
if not ramps then return "PARamps folder missing" end
local floor = map:FindFirstChild("PAFloor", true) -- RECURSIVE
if not floor then return "PAFloor missing" end
local fTop = floor.Position.Y + floor.Size.Y / 2
local fx0, fx1 = floor.Position.X - floor.Size.X / 2, floor.Position.X + floor.Size.X / 2
local fz0, fz1 = floor.Position.Z - floor.Size.Z / 2, floor.Position.Z + floor.Size.Z / 2
-- Collect deck segments per slip, ordered by their index suffix so we can find
-- the true LOW end rather than guessing from a bounding box.
local decks = { PAWestSlip = {}, PAEastSlip = {} }
local counts = {}
for _, d in ipairs(ramps:GetDescendants()) do
if d:IsA("BasePart") then
counts[d.Name:gsub("_%d+$", "")] = (counts[d.Name:gsub("_%d+$", "")] or 0) + 1
for slipName, list in pairs(decks) do
local idx = d.Name:match("^" .. slipName .. "_Deck_(%d+)$")
if idx then
table.insert(list, { i = tonumber(idx), p = d.Position, s = d.Size, cf = d.CFrame })
end
end
end
end
local verdict, report = {}, {}
for slipName, list in pairs(decks) do
table.sort(list, function(a, b) return a.i < b.i end)
if #list < 50 then
table.insert(verdict, slipName .. " FAIL: only " .. #list .. " deck segments")
else
local first, last = list[1], list[#list]
local yTopFirst = first.p.Y + first.s.Y / 2
local yTopLast = last.p.Y + last.s.Y / 2
-- 1. Does the LOW end sit inside the apron footprint?
local inX = last.p.X >= fx0 and last.p.X <= fx1
local inZ = last.p.Z >= fz0 and last.p.Z <= fz1
local lands = inX and inZ
-- 2. Is the low end FLUSH with the apron surface? A lip catches cars.
local lip = yTopLast - fTop
-- 3. Is the high end flush with the mainline deck (Y 103)?
local mouthErr = yTopFirst - 103
-- 4. Peak grade over any single segment.
local maxGrade = 0
for k = 2, #list do
local a, b = list[k - 1], list[k]
local dy = math.abs((b.p.Y) - (a.p.Y))
local dxz = (Vector3.new(b.p.X, 0, b.p.Z) - Vector3.new(a.p.X, 0, a.p.Z)).Magnitude
if dxz > 0.5 then maxGrade = math.max(maxGrade, dy / dxz) end
end
report[slipName] = {
segs = #list,
mouth = { x = math.floor(first.p.X), z = math.floor(first.p.Z), yTop = math.floor(yTopFirst * 10) / 10 },
landing = { x = math.floor(last.p.X), z = math.floor(last.p.Z), yTop = math.floor(yTopLast * 10) / 10 },
landsInApron = lands,
lipStuds = math.floor(lip * 100) / 100,
mouthErrStuds = math.floor(mouthErr * 100) / 100,
peakGradePct = math.floor(maxGrade * 1000) / 10,
}
if not lands then
table.insert(verdict, ("%s FAIL: landing (%d,%d) outside apron x[%d..%d] z[%d..%d]")
:format(slipName, last.p.X, last.p.Z, fx0, fx1, fz0, fz1))
end
if math.abs(lip) > 1.5 then
table.insert(verdict, ("%s FAIL: %.2f stud lip at apron"):format(slipName, lip))
end
if math.abs(mouthErr) > 1.5 then
table.insert(verdict, ("%s FAIL: %.2f stud step at mainline mouth"):format(slipName, mouthErr))
end
if maxGrade > 0.16 then
table.insert(verdict, ("%s FAIL: peak grade %.1f%%"):format(slipName, maxGrade * 100))
end
end
end
-- 5. Does the mainline parapet actually open at each mouth? Count Barrier parts
-- within 160 studs of each mouth on the lot side; a sealed mouth = ramp is
-- decorative.
local road = map
local mouthBlocked = {}
for slipName, r in pairs(report) do
local mp = Vector3.new(r.mouth.x, 103, r.mouth.z)
local n = 0
for _, d in ipairs(road:GetDescendants()) do
if d:IsA("BasePart") and (d.Name == "Barrier" or d.Name == "Parapet") then
if (Vector3.new(d.Position.X, 103, d.Position.Z) - mp).Magnitude < 150 then n += 1 end
end
end
mouthBlocked[slipName] = n
end
return HttpService:JSONEncode({
apron = { top = fTop, x = { math.floor(fx0), math.floor(fx1) }, z = { math.floor(fz0), math.floor(fz1) } },
slips = report,
partCounts = counts,
barriersNearMouth = mouthBlocked,
verdict = #verdict == 0 and "ALL PASS" or table.concat(verdict, " | "),
})