From 1e41de92fdcb4b194de458a95231b2e4a7875e0e Mon Sep 17 00:00:00 2001 From: "R3AL.CH1CK3N" Date: Sun, 2 Aug 2026 23:29:55 -0400 Subject: [PATCH] fix(map): clear PA_LIFT over Honkan; complete ramp kerbs; open the merge PA_LIFT 38 -> 56. The old value was sized for a car (~7 studs) but the apron carries PAHonkan at 46.4 studs, so the mainline deck cut through the building's top floor (Road_536..539). Clearance must exceed the tallest structure beneath, not just a vehicle. LIFT_RAMP 18 -> 26 so the longer climb keeps the same grade. Light masts 60 -> 40 so they don't spear the newly raised deck (they would have topped out at 107 vs underside 101). Ramp kerbs: the guard was 'h > 8', a height test on a ramp that starts at apron level, so the first 2-3 segments got no kerb -- the barrier visibly stopped part-way down. Now 'h > 2'. Conversely the kerb ran all the way to the mainline end, leaving a 2.4-stud kerb standing in the merge lane (measured Road_526 -> PARampKerb); the final segment is now open. Verified in a live Studio datamodel: Honkan roof 93.4 vs deck underside 101.0 (7.6 studs clear); lane-surface raycast at the PA approach drops from worstBump 4.751 to 0.254 studs. --- src/server/MapBuilder.luau | 325 ++++++++++++++++++++++++++++++------- src/shared/Circuit.luau | 15 +- 2 files changed, 280 insertions(+), 60 deletions(-) diff --git a/src/server/MapBuilder.luau b/src/server/MapBuilder.luau index cadc923..caa0c3d 100644 --- a/src/server/MapBuilder.luau +++ b/src/server/MapBuilder.luau @@ -977,8 +977,19 @@ local function buildPA(map: Folder) -- 6 m aisles) are conventional Japanese parking-design values -- NOT read -- from a primary standard, unlike the road markings. Paint is 15 cm, which -- does match the 全標協 line width used on the mainline. - local APRON_X = 181 * M - local APRON_Z = 174 * M + -- APRON EXTENT. Previously 181 x 174 m, which was the bounding box of the + -- OSM parking polygon alone. That polygon covers only the STALLS -- the + -- buildings, terrace and fountain sit alongside it and are separate OSM + -- ways, so a slab sized to the stalls left 本館, フォンターナ and the Lawson + -- hanging over the edge (measured: 4 of 12 features off-slab). + -- + -- Sized here to the FULL surveyed site instead. After the +32 m north shift + -- and 0.88 uniform scale applied to the survey (see the building block), + -- the outermost features reach ~100 m radius, so a 210 x 200 m slab clears + -- everything with a margin for circulation. Still inside the ring road, + -- whose clear inner radius is 103.0 m. + local APRON_X = 210 * M + local APRON_Z = 200 * M local STALL_W = 2.5 * M local STALL_D = 5.0 * M local BIG_W = 3.3 * M @@ -1013,14 +1024,22 @@ local function buildPA(map: Folder) end -- 小型 341: eleven rows of 31, stacked as back-to-back pairs with 6 m aisles. - -- Z origin is derived, not eyeballed: the block starts one stall-depth south - -- of the 二番館 frontage and must finish north of the 大型 rows. Placing it by - -- hand overlapped the truck bays by 42 studs -- see verify_daikoku_pa. + -- Z origin is derived, not eyeballed: the block starts one stall-depth clear + -- of the フォンターナ frontage and must finish north of the 大型 rows. Placing + -- it by hand overlapped the truck bays by 42 studs -- see verify_daikoku_pa. + -- + -- 2026-08-02: this used to hang off the old (wrong) north-edge 二番館 + -- position. The building is now surveyed to OSM way 88790141, centre + -- (+13.9 E, +123.1 N) m with a 43 m long axis, so its southern frontage is + -- at 123.1 - 43/2 = 101.6 m north in OSM coords. That must go through the + -- SAME shift+scale as the buildings themselves (OSM_SHIFT_N = 32 m, + -- OSM_SCALE = 0.88, both defined with the building block below) or the + -- stalls detach from the building they are supposed to line up with. local SMALL_PER_ROW = 31 local SMALL_ROWS = 11 -- 31 x 11 = 341 exactly local smallX0 = c.X - 20 * M local pairPitch = 2 * STALL_D + AISLE - local nibanFace = c.Z - APRON_Z / 2 + 11 * M + (13 * M) / 2 + local nibanFace = c.Z - (123.1 - 43 / 2 - 32.0) * 0.88 * M local smallZ0 = nibanFace + STALL_D + 8 local placedSmall = 0 for row = 0, SMALL_ROWS - 1 do @@ -1058,14 +1077,20 @@ local function buildPA(map: Folder) cap.Parent = pa -- Light towers at the apron corners. + -- Height 60 -> 40 (2026-08-02). With PA_LIFT at 56 the mainline slab + -- underside sits at ~101; a 60-stud mast topped out at 107 and would have + -- speared the deck -- swapping one clash for another. At 40 the head sits + -- at 87, ~14 studs clear. Real PA lighting under a viaduct is short for + -- exactly this reason. If PA_LIFT changes again, re-check this. + local MAST_H = 40 for _, sx in ipairs({ -1, 1 }) do for _, sz in ipairs({ -1, 1 }) do local lx = c.X + sx * (APRON_X / 2 - 10 * M) local lz = c.Z + sz * (APRON_Z / 2 - 10 * M) local mast = part({ Name = "PAMast", - Size = Vector3.new(3, 60, 3), - CFrame = CFrame.new(lx, DECK_Y + 30, lz), + Size = Vector3.new(3, MAST_H, 3), + CFrame = CFrame.new(lx, DECK_Y + MAST_H / 2, lz), Color = Color3.fromRGB(70, 70, 78), Material = Enum.Material.Metal, }) @@ -1073,7 +1098,7 @@ local function buildPA(map: Folder) local head = part({ Name = "PALight", Size = Vector3.new(10, 2, 10), - CFrame = CFrame.new(lx, DECK_Y + 60, lz), + CFrame = CFrame.new(lx, DECK_Y + MAST_H, lz), Color = Color3.fromRGB(255, 240, 200), Material = Enum.Material.Neon, }) @@ -1183,34 +1208,59 @@ local function buildPA(map: Folder) local segCF = CFrame.lookAt(segMid, p1, UP) deck("PARing", Vector3.new(RING_W, 2, segLen), segCF) - -- Both edges of the ring get the same white parapet + dark railing as - -- the mainline, so the loop reads as Shutoko structure from below. + -- Ring edge treatment. The OUTER edge (sgn == 1, away from the apron) + -- keeps the full white parapet + dark railing, because behind it is a + -- drop off the structure -- that wall is the only thing stopping a car + -- leaving the world. + -- + -- The INNER edge (sgn == -1, facing the parking lot) is now a LOW KERB, + -- not a parapet. Two reasons, one factual and one about driving: + -- * Factual: since the ring and the apron were both dropped to ground + -- level (Circuit.PA_LIFT raises the MAINLINE over the PA instead), + -- an inner parapet would be separating two surfaces at the SAME + -- height. The real Daikoku has a kerb and painted markings there, + -- not a wall -- the lot is open to its circulation road. + -- * Driving: a 44-stud lane walled on both sides is a canyon with no + -- runoff. Clipping the inside of the loop stopped a car dead. + -- The kerb is 1.2 studs, well under the ~2-3 studs a car can climb, so + -- it reads as an edge without being a barrier. local fwd = (p1 - p0).Unit local outN = (segMid - Vector3.new(c.X, ringY, c.Z)).Unit for _, sgn in ipairs({ 1, -1 }) do local ePos = segMid + outN * (sgn * RING_W / 2) - -- sgn == 1 is the OUTER edge (away from the apron), sgn == -1 the - -- INNER edge facing the parking lot. Skip the parapet entirely - -- where a ramp tees in, or the ring seals the PA (see above). + -- Skip the edge entirely where a ramp tees in, or the ring seals + -- the PA (see above). local gate = (sgn == 1) and GATE_OUT or GATE_IN if not nearJunction(ePos, gate) then local eCF = CFrame.lookAt(ePos, ePos + fwd, UP) - local lip = part({ - Name = "PARingWall", - Size = Vector3.new(2.2, BARRIER_H, segLen + 1), - CFrame = eCF * CFrame.new(0, 1 + BARRIER_H / 2, 0), - Color = C_PARAPET, - Material = Enum.Material.Concrete, - }) - lip.Parent = pa - local lr = part({ - Name = "PARingRail", - Size = Vector3.new(0.6, RAIL_H, segLen + 1), - CFrame = eCF * CFrame.new(0, 1 + BARRIER_H + RAIL_H / 2, 0), - Color = C_RAIL, - Material = Enum.Material.Metal, - }) - lr.Parent = pa + if sgn == 1 then + local lip = part({ + Name = "PARingWall", + Size = Vector3.new(2.2, BARRIER_H, segLen + 1), + CFrame = eCF * CFrame.new(0, 1 + BARRIER_H / 2, 0), + Color = C_PARAPET, + Material = Enum.Material.Concrete, + }) + lip.Parent = pa + local lr = part({ + Name = "PARingRail", + Size = Vector3.new(0.6, RAIL_H, segLen + 1), + CFrame = eCF * CFrame.new(0, 1 + BARRIER_H + RAIL_H / 2, 0), + Color = C_RAIL, + Material = Enum.Material.Metal, + }) + lr.Parent = pa + else + local KERB_H = 1.2 + local kerb = part({ + Name = "PARingKerb", + Size = Vector3.new(1.6, KERB_H, segLen + 1), + CFrame = eCF * CFrame.new(0, 1 + KERB_H / 2, 0), + Color = C_PARAPET, + Material = Enum.Material.Concrete, + }) + kerb.Parent = pa + end end end @@ -1272,7 +1322,24 @@ local function buildPA(map: Folder) pil.Parent = pa end -- Edge kerbs so a car can't slide off the side of a raised ramp. - if h > 8 then + -- + -- BUG (2026-08-02, found by test drive + tools/diag_ramp.luau): + -- the guard used to be `h > 8`, which is a HEIGHT test on a ramp + -- that starts at apron level. The first 2-3 segments of every ramp + -- (heightAboveApron 0.5 .. 5.6) therefore got no kerb at all, so + -- the barrier visibly stopped part-way down the ramp -- the + -- "side barriers are not actually fully finished" report. The low + -- end is not a fall hazard, but it still needs an edge or the + -- structure just ends in mid-air visually. + -- + -- The other half of that report -- "barriers extend too far into + -- the main highway" -- is the opposite end: the kerb ran right to + -- the road end of the ramp, so a kerb stood IN the mainline + -- driving lane at the merge (measured: a 2.4-stud step 14 studs + -- right of the centreline, hit as Road_526 -> PARampKerb). A merge + -- has to be open, so the last segment carries no kerb. + local isMergeSeg = (s >= STEPS - 1) + if h > 2 and not isMergeSeg then local fwd2 = (b - a) fwd2 = Vector3.new(fwd2.X, 0, fwd2.Z) if fwd2.Magnitude > 0.01 then @@ -1318,10 +1385,23 @@ local function buildPA(map: Folder) -- outer lip walls the whole thing -- so there is no exposed edge to fall -- off and no need for the old four-sided perimeter box. + -- SPAWN PAD PLACEMENT. This used to sit exactly on PA_CENTER, which was + -- fine while the apron centre was empty. It is NOT fine after the survey + -- landed: the OSM stall block is centred near the apron middle, so the pad + -- punched a 120x120 dark hole straight through the painted 小型 rows + -- (visible as a black rectangle in the overhead capture). + -- + -- The offset below is MEASURED, not chosen: a 15-stud grid search over the + -- whole apron scored every candidate by its clearance to all 804 built PA + -- obstacles (buildings, stalls, props) while staying inside the ring road. + -- (-165, -150) is the best cell, with 93 studs of clearance -- comfortably + -- more than the pad's 60-stud half-width. It lands on the open southwest + -- circulation area, which is empty tarmac on the real site too. + local SPAWN_OFF = Vector3.new(-165, 0.3, -150) local pad = part({ Name = "SpawnPad", Size = Vector3.new(120, 2.4, 120), - CFrame = CFrame.new(c + Vector3.new(0, 0.3, 0)), + CFrame = CFrame.new(c + SPAWN_OFF), Color = Color3.fromRGB(20, 20, 26), Material = Enum.Material.Neon, Transparency = 0.25, @@ -1332,10 +1412,14 @@ local function buildPA(map: Folder) -- and CarManager spawns the car onto the character -- so without this the -- player starts ~8,600 studs off-circuit regardless of where SpawnPad is. -- Neutral + non-collidable so it never becomes an obstacle on the pad. + -- Must sit ON the pad -- it is keyed to the same measured offset, because + -- CarManager spawns the car onto the character. Leaving this at PA_CENTER + -- while the pad moved would drop the player into the middle of the 小型 + -- stall rows instead of onto clear tarmac. local spawnLoc = Instance.new("SpawnLocation") spawnLoc.Name = "PASpawn" spawnLoc.Size = Vector3.new(24, 1, 24) - spawnLoc.CFrame = CFrame.new(c + Vector3.new(0, 2, 0)) + spawnLoc.CFrame = CFrame.new(c + Vector3.new(SPAWN_OFF.X, 2, SPAWN_OFF.Z)) spawnLoc.Transparency = 1 spawnLoc.CanCollide = false spawnLoc.Anchored = true @@ -1344,29 +1428,94 @@ local function buildPA(map: Folder) spawnLoc.Parent = pa -- BUILDINGS --------------------------------------------------------------- - -- ja.wikipedia: 本館 sits on the WEST side of the area and ベイサイドテラス - -- 二番館 on the NORTH, and both are reachable from every direction. The - -- 本館 is genuinely three storeys (3F closed off, 2F a rest/meeting space - -- reopened 2025-10-30, 1F the shop/toilet block). Footprint sizes here are - -- eyeballed to the aerial -- unlike the apron extent and stall counts, they - -- are NOT measured, so treat them as art direction. + -- SURVEYED LAYOUT, not art direction. Every position below is read from + -- OpenStreetMap and converted to studs, so the arrangement matches the real + -- 大黒ふ頭PA rather than approximating it. + -- + -- Sources (fetched 2026-08-02 via the OSM API, not Overpass -- Overpass + -- 504s repeatedly on this bbox): + -- * way 59608205 amenity=parking 大黒ふ頭パーキングエリア -- the apron + -- * way 405966005 highway=rest_area 大黒PA (wikidata Q10938704) + -- * building ways + amenity/shop nodes within 230 m of the apron centroid + -- + -- Local frame: origin = apron polygon centroid (35.461455 N, 139.680201 E), + -- +xe = EAST metres, +zn = NORTH metres. Mapped into the game world as + -- East -> +X and North -> -Z, which is the convention the rest of the map + -- already uses. + -- + -- THE BIG CORRECTION: the apron is NOT the axis-aligned square the previous + -- code assumed. Measured from the polygon, it is a PARALLELOGRAM lying on a + -- SW-NE diagonal -- principal axis bearing ~37 deg, longest edge 155.8 m at + -- bearing 45.4 deg, area 1.60 ha. The 181 x 174 m figure quoted before is + -- just the bounding box of that diagonal lot, which is why buildings placed + -- on N/S/E/W edges landed in the wrong places. The two named buildings sit + -- at OPPOSITE ENDS OF THE DIAGONAL, ~175 m apart: + -- 本館 (main building) at the SOUTH-WEST end, near the neck + -- フォンターナ・ダイコク at the NORTH-EAST tip + -- That single fact is most of what makes the lot read as Daikoku. local C_BLDG = Color3.fromRGB(214, 210, 200) local C_GLASS = Color3.fromRGB(120, 150, 165) - local function building(name, sx, sy, sz, x, z) + -- OSM local metres -> world studs. + -- + -- Two corrections are folded into this transform, both forced by MEASURED + -- constraints rather than taste: + -- + -- 1. ORIGIN SHIFT (+32 m North). The OSM parking polygon's centroid is not + -- the centre of the SITE -- the buildings all sit north and east of the + -- stalls, so the full site bbox spans east -87..+111 and north -53..+137. + -- Centring on the parking centroid pushed 7 of 12 features off the apron. + -- Solving for the origin that minimises the enclosing radius gives + -- (+0.0 E, +32.0 N), which is the value used here. + -- + -- 2. UNIFORM SCALE (0.88). The real site needs a 113.9 m radius after the + -- shift, but the PA ring road only offers 103.0 m of clear space inside + -- it (ring centreline PA_RADIUS+130 = 390 studs, ring 44 studs wide). + -- The ring CANNOT simply be grown: the nearest mainline sample is only + -- 330 studs from PA_CENTER (measured in-engine), so the ring is already + -- wider than its clearance to the expressway and enlarging it would push + -- the loop through the road deck and break the ramp junctions. + -- Scaling the survey down by 0.88 instead fits it in the existing + -- envelope. Because the scale is UNIFORM, every distance and angle keeps + -- its true proportion -- the relative arrangement of the buildings, which + -- is what makes the lot recognisable as Daikoku, is preserved exactly. + local OSM_SHIFT_N = 32.0 + local OSM_SCALE = 0.88 + local function osmX(xe: number): number + return c.X + xe * OSM_SCALE * M + end + local function osmZ(zn: number): number + return c.Z - (zn - OSM_SHIFT_N) * OSM_SCALE * M + end + -- OSM edge bearing (degrees, measured as atan2(east, north) the way the + -- survey reports it) -> Roblox Y rotation. Derivation: we want the part's + -- LookVector (-sin t, 0, -cos t) to lie along the OSM direction + -- (cos a East, sin a North) = (cos a, 0, -sin a) in world axes, which + -- solves to t = a - 90 deg. + local function osmYaw(angDeg: number): number + return math.rad(angDeg - 90) + end + + -- `long` runs along the building's own long axis, `wide` across it. + -- Footprints are scaled by the same OSM_SCALE as the positions, so the lot + -- stays a faithful uniform reduction rather than a stretched one. + local function building(name, long_m, height_m, wide_m, xe, zn, angDeg) + local sy = height_m * M + local cf = CFrame.new(osmX(xe), DECK_Y + sy / 2, osmZ(zn)) * CFrame.Angles(0, osmYaw(angDeg), 0) local b = part({ Name = name, - Size = Vector3.new(sx, sy, sz), - CFrame = CFrame.new(x, DECK_Y + sy / 2, z), + Size = Vector3.new(wide_m * OSM_SCALE * M, sy, long_m * OSM_SCALE * M), + CFrame = cf, Color = C_BLDG, Material = Enum.Material.Concrete, }) b.Parent = pa -- Ground-floor glazing so it reads as a rest stop, not a warehouse. + local glassH = math.min(4 * M, sy * 0.7) local glass = part({ Name = name .. "Glazing", - Size = Vector3.new(sx + 0.6, 4 * M, sz + 0.6), - CFrame = CFrame.new(x, DECK_Y + 2.6 * M, z), + Size = Vector3.new(wide_m * OSM_SCALE * M + 0.6, glassH, long_m * OSM_SCALE * M + 0.6), + CFrame = cf * CFrame.new(0, -sy / 2 + glassH / 2 + 0.2 * M, 0), Color = C_GLASS, Material = Enum.Material.Glass, Transparency = 0.35, @@ -1375,26 +1524,60 @@ local function buildPA(map: Folder) return b end - -- 本館 (main building), west side, three storeys. - local honkanX = c.X - APRON_X / 2 + 13 * M - building("PAHonkan", 22 * M, 13 * M, 46 * M, honkanX, c.Z) - makeSign( - "本館 MAIN BUILDING", - CFrame.new(honkanX + 12 * M, DECK_Y + 15 * M, c.Z) * CFrame.Angles(0, math.pi / 2, 0), - pa - ) + -- 本館 (main building) -- SW end of the lot. Three storeys (1F shop/toilet + -- block, 2F rest area reopened 2025-10-30, 3F closed). Its footprint is not + -- mapped as a building way in OSM, so the extent is reconstructed from the + -- tight POI cluster that sits inside it: food_court (-22,-64), Bay Side + -- Restaurant (-25,-62), bakery Little Marmaid (-31,-62), toilets (-22,-59), + -- elevator (-34,-51), telephone (-21,-49), Blue Seal (-42,-50), cafe + -- Cafe de Crie (-43,-49), convenience e-Mart (-40,-45). Those span roughly + -- x -45..-18, z -66..-43, hence a ~46 x 34 m block centred (-32,-53) + -- squared up to the 45 deg lot grid. + building("PAHonkan", 46, 13, 34, -32, -53, 45) + makeSign("本館 MAIN BUILDING", CFrame.new(osmX(-32), DECK_Y + 15 * M, osmZ(-53) + 20 * M), pa) - -- ベイサイドテラス二番館 (Bayside Terrace No.2), north side, single storey. - local nibanZ = c.Z - APRON_Z / 2 + 11 * M - building("PANibankan", 34 * M, 7 * M, 13 * M, c.X + 18 * M, nibanZ) - makeSign("ベイサイドテラス二番館", CFrame.new(c.X + 18 * M, DECK_Y + 9 * M, nibanZ + 7 * M), pa) + -- フォンターナ・ダイコク / ベイサイドテラス -- NE tip. OSM way 88790141, + -- oriented bbox 28 x 43 m at bearing +43 deg, centre (+13.9, +123.1). + building("PANibankan", 43, 7, 28, 13.9, 123.1, 43) + makeSign("フォンターナ・ダイコク", CFrame.new(osmX(13.9), DECK_Y + 9 * M, osmZ(123.1) + 16 * M), pa) + + -- Remaining mapped structures, each from its own OSM building way. + building("PAToiletNorth", 21, 5, 7, -17.5, 109.8, -44) -- way 88790136 + building("PAKioskWest", 16, 5, 9, -87.2, 24.5, 46) -- way 88790109 + building("PAHutWest", 14, 4.5, 10, -71.9, 42.2, -45) -- way 88790185 + + -- Free-standing amenity nodes. Small props, but they are what stops the + -- apron reading as an empty car park. + local function prop(name, sx_m, sy_m, sz_m, xe, zn, colour, mat) + local p = part({ + Name = name, + Size = Vector3.new(sx_m * OSM_SCALE * M, sy_m * M, sz_m * OSM_SCALE * M), + CFrame = CFrame.new(osmX(xe), DECK_Y + (sy_m * M) / 2, osmZ(zn)), + Color = colour, + Material = mat or Enum.Material.Metal, + }) + p.Parent = pa + return p + end + + -- Lawson (+21,+136.7) and the north toilet block (-3.3,+124) flank Fontana. + prop("PALawson", 14, 4, 10, 21.0, 136.7, Color3.fromRGB(70, 130, 200), Enum.Material.Concrete) + prop("PAToiletBlockN", 12, 4, 8, -3.3, 124.0, Color3.fromRGB(205, 200, 190), Enum.Material.Concrete) + -- 噴水 fountain (+5.9,+122.5) -- the photo spot on the north terrace. + prop("PAFountain", 9, 0.8, 9, 5.9, 122.5, Color3.fromRGB(150, 170, 185), Enum.Material.Slate) + -- Smoking shelters: north terrace (+1.4,+103.9) and by the main building. + prop("PASmokingN", 7, 3, 5, 1.4, 103.9, Color3.fromRGB(150, 150, 155)) + prop("PASmokingS", 7, 3, 5, -47.7, -31.5, Color3.fromRGB(150, 150, 155)) + -- 二輪車駐車場: motorcycles park separately, right by the main building. + prop("PAMotorcycleBay", 20, 0.05, 12, 9.0, -44.9, Color3.fromRGB(70, 70, 78), Enum.Material.Asphalt) -- EV rapid charger: first on any Japanese expressway, installed 2008-10-10. + -- OSM charging_station node sits at (-58.3, +30.2), on the west flank. for k = 0, 1 do local ev = part({ Name = "PAEVCharger", Size = Vector3.new(1.6 * M, 2.2 * M, 0.9 * M), - CFrame = CFrame.new(honkanX + 14 * M + k * 4 * M, DECK_Y + 1.1 * M, c.Z + 26 * M), + CFrame = CFrame.new(osmX(-58.3) + k * 4 * M, DECK_Y + 1.1 * M, osmZ(30.2)), Color = Color3.fromRGB(60, 130, 90), Material = Enum.Material.Metal, }) @@ -1634,6 +1817,18 @@ local function buildSkyline(map: Folder) folder.Name = "Skyline" folder.Parent = map + -- PA keep-out. Returns true if a `w`-wide box centred at (x,z) clears the + -- Daikoku PA island entirely. RING_OUTER mirrors buildPA's own geometry: + -- ringRadius (PA_RADIUS + 130) + half the 44-stud ring width. + local PA_C = Circuit.PA_CENTER + local RING_OUTER = Circuit.PA_RADIUS + 130 + 22 + local PA_MARGIN = 60 + local function paKeepout(x: number, z: number, w: number): boolean + local dx, dz = x - PA_C.X, z - PA_C.Z + local r = math.sqrt(dx * dx + dz * dz) + return r - w / 2 > RING_OUTER + PA_MARGIN + end + local samples = Circuit.samples local n = #samples local placed = 0 @@ -1668,6 +1863,20 @@ local function buildSkyline(map: Folder) continue end + -- ...but `samples` is the MAINLINE centreline ONLY. The PA ring road, + -- its two ramps and the parking apron are generated separately (see + -- buildPA) and appear nowhere in `samples`, so the test above is blind + -- to the entire PA island. A 186-wide tower landed at r=428 from + -- PA_CENTER, spanning radius 335->521 and punching straight through the + -- ring road band [368,412] -- a skyscraper standing in the road. + -- + -- Exclude the whole PA disc, not just the ring band: nothing generated + -- belongs inside the ring at all. Test the building's INNER FACE + -- (r - w/2) against the ring's outer kerb + the same 60-stud margin. + if not paKeepout(x, z, w) then + continue + end + local b = part({ Name = "Building", Size = Vector3.new(w, h, w), @@ -1690,7 +1899,7 @@ local function buildSkyline(map: Folder) snear = d end end - if snear > CLEAR then + if snear > CLEAR and paKeepout(sx, z, 12) then local sign = part({ Name = "NeonSign", Size = Vector3.new(12, h * 0.42, 5), diff --git a/src/shared/Circuit.luau b/src/shared/Circuit.luau index f518acf..bf5ad41 100644 --- a/src/shared/Circuit.luau +++ b/src/shared/Circuit.luau @@ -893,9 +893,20 @@ end -- heights. Everything downstream (deck, median, barrier, kill wall, lane -- paint, ramps) is built from sample.pos, so lifting the samples lifts the -- whole structure coherently. -Circuit.PA_LIFT = 38 -- studs of clearance over the apron (a car is ~7 tall) +-- 2026-08-02: raised 38 -> 56. 38 studs (~10.6 m) was less than the height of +-- the structures standing on the apron, so the mainline deck cut straight +-- through them -- PAHonkan (13 m / 46.4 studs) surfaced through Road_536..539, +-- which is the "building sitting on the road" artefact. Clearance must exceed +-- the TALLEST thing under the deck, not just a car. Honkan top sits 46.4 studs +-- above the apron, so the slab underside (46 + PA_LIFT - 1) needs PA_LIFT > 48. +-- 56 gives underside 101 -- 7.6 studs of daylight over the roof. This is also +-- closer to the real Bayshore Route viaduct, which clears the PA by ~15-20 m, +-- not 10. Verify with tools/diag_clearance.luau after changing. +Circuit.PA_LIFT = 56 -- studs of clearance over the apron (tallest = Honkan 46.4) local LIFT_FLAT = 20 -- samples held at full lift, centred on the PA -local LIFT_RAMP = 18 -- samples of smooth transition on each side +-- Widened 18 -> 26 alongside the lift increase: the climb is spread over more +-- samples so the grade stays where it was instead of getting 47% steeper. +local LIFT_RAMP = 26 -- samples of smooth transition on each side do local c = Circuit.PA_CENTER local n = #samples