diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c05ccc8..b760165 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -37,9 +37,16 @@ until this is true. ## M2 — More cars + polish (still no racing) - [ ] Import a few more real car meshes. -- [x] Better map v2: 3200-stud expressway with gantry arches, a neon-lit - tunnel, jump ramps (right lane; left stays flat), end wall, and a - procedural neon skyline flanking the route. +- [x] Better map v3 — **"Shuto Night Loop"**: closed ~7,400-stud circuit + inspired by Tokyo's Shuto expressway, with real-Tokyo landmark zones: + **Daikoku Futo PA** (drivable meet lot + painted bays + light towers — + cars spawn here), **Rainbow Bridge** (suspension towers, rainbow rail + lights), **Odaiba** beyond it (blocky Unicorn Gundam statue with glowing + psycho-frame + giant neon ferris wheel), **Yamate-style tunnel** (north + straight), **Tokyo Tower** replica (west), **Tatsumi PA** overlook, + bilingual green highway signs (大黒PA, レインボーブリッジ, お台場, 辰巳PA), + city-core + outer-ring procedural skyline. Not geographically accurate — + a themed loop, not a map of Tokyo. - [x] Day/night, weather mood: `src/server/Ambience.luau` — Atmosphere haze catching the neon, bloom + cool color-correction post effects, and a slow 22:00→03:00 clock drift (10-min ping-pong) so the night sky breathes. diff --git a/src/server/MapBuilder.luau b/src/server/MapBuilder.luau index ec0a9c5..40ac0a8 100644 --- a/src/server/MapBuilder.luau +++ b/src/server/MapBuilder.luau @@ -1,36 +1,56 @@ --!strict --[[ MapBuilder.luau (SERVER) - Builds the "Tokyo expressway" map entirely from code — ZERO manual Studio - building. v2 (M2 groundwork): the straight highway now runs through a - sequence of FEATURES so driving it is actually interesting: + v3 — "Shuto Night Loop": a Tokyo-expressway-inspired CIRCUIT, still built + 100% from code. The straight strip is gone; the road is now a closed + rounded-rectangle loop (~7,400 studs/lap) modelled on the Shuto C1 vibe, + with real-Tokyo landmark zones: - seg 0-1 open start (spawn pad) - seg 2 overhead gantry arches - seg 3 TUNNEL (enclosed, neon-striped ceiling) - seg 4 jump ramps (side-by-side launch + landing, optional — road - continues flat around them) - seg 5-6 open run to the finish line - seg 7 finish + runoff + SOUTH straight — Daikoku Futo PA: the legendary meet spot. A big + drivable parking area off the expressway with painted + bays, light towers, and the spawn pad. Cars spawn HERE. + EAST straight — Rainbow Bridge: white suspension towers, cross beams, + rainbow-lit rails. Odaiba sits beyond it: a blocky + Unicorn-Gundam-style statue + a giant neon ferris + wheel (Palette Town wheel). + NORTH straight — Yamate-style tunnel: long enclosed bore with strip + lights (the C1/Yamate underground feel). + WEST straight — City skyline run past a Tokyo Tower replica + (orange/white, twin decks, antenna). + Everywhere — procedural neon skyline inside and outside the loop, + Tatsumi-PA-style overlook pad (visual, NE). - Neon skyline blocks flank the whole route outside the guardrails for the - night-city mood. Everything is still named parts (SpawnPad, FinishLine, - KillWall_*) so gameplay code keeps working when this is replaced by a - modeled map later. + NOT geographically accurate — it's a themed driving loop, not a map of + Tokyo. Gameplay parts keep their contract names (SpawnPad, FinishLine, + KillWall_*) so nothing else changes. ]] local Workspace = game:GetService("Workspace") local MapBuilder = {} +-- Loop geometry ---------------------------------------------------------------- local ROAD_WIDTH = 60 -local SEGMENT_LENGTH = 400 -local NUM_SEGMENTS = 8 -- total length = 3200 studs of highway +local HX = 1200 -- half-extent X +local HZ = 800 -- half-extent Z +local CORNER_R = 300 +local STEP = 60 -- straight sample spacing +local ARC_STEPS = 12 -- samples per 90-degree corner local WALL_HEIGHT = 26 local NEON_PINK = Color3.fromRGB(255, 40, 120) local NEON_BLUE = Color3.fromRGB(40, 160, 255) local NEON_TEAL = Color3.fromRGB(40, 230, 200) +local RAINBOW = { + Color3.fromRGB(255, 60, 60), + Color3.fromRGB(255, 160, 40), + Color3.fromRGB(250, 230, 60), + Color3.fromRGB(80, 230, 90), + Color3.fromRGB(60, 150, 255), + Color3.fromRGB(170, 90, 255), +} + +local UP = Vector3.new(0, 1, 0) local function part(props: { [string]: any }): Part local p = Instance.new("Part") @@ -49,232 +69,582 @@ local function det(seed: number): number return x - math.floor(x) end --- Road deck ------------------------------------------------------------------ -local function buildRoad(parent: Folder) - for i = 0, NUM_SEGMENTS - 1 do - local z = -(i * SEGMENT_LENGTH) - SEGMENT_LENGTH / 2 +-- Green highway sign with bilingual text --------------------------------------- +local function makeSign(text: string, cf: CFrame, parent: Instance) + local board = part({ + Name = "Sign", + Size = Vector3.new(30, 9, 1), + CFrame = cf, + Color = Color3.fromRGB(18, 90, 60), + Material = Enum.Material.SmoothPlastic, + }) + board.Parent = parent + local gui = Instance.new("SurfaceGui") + gui.Face = Enum.NormalId.Front + gui.CanvasSize = Vector2.new(600, 180) + gui.Parent = board + local label = Instance.new("TextLabel") + label.Size = UDim2.fromScale(1, 1) + label.BackgroundTransparency = 1 + label.Font = Enum.Font.GothamBold + label.TextColor3 = Color3.new(1, 1, 1) + label.TextScaled = true + label.Text = text + label.Parent = gui +end + +-- Centerline samples ------------------------------------------------------------ +type Sample = { pos: Vector3, piece: string } + +local function genSamples(): { Sample } + local samples: { Sample } = {} + local sx = HX - CORNER_R + local sz = HZ - CORNER_R + + local function push(pos: Vector3, piece: string) + table.insert(samples, { pos = pos, piece = piece }) + end + local function arc(cx: number, cz: number, a0: number, piece: string) + for i = 1, ARC_STEPS do + local a = a0 + (i / ARC_STEPS) * (math.pi / 2) + push(Vector3.new(cx + CORNER_R * math.sin(a), 0, cz + CORNER_R * math.cos(a)), piece) + end + end + + -- South straight (Daikoku side), heading +X + local x = -sx + while x <= sx do + push(Vector3.new(x, 0, HZ), "south") + x += STEP + end + arc(sx, sz, 0, "corner_se") + -- East straight (Rainbow Bridge), heading -Z + local z = sz - STEP + while z >= -sz do + push(Vector3.new(HX, 0, z), "east") + z -= STEP + end + arc(sx, -sz, math.pi / 2, "corner_ne") + -- North straight (Yamate tunnel), heading -X + x = sx - STEP + while x >= -sx do + push(Vector3.new(x, 0, -HZ), "north") + x -= STEP + end + arc(-sx, -sz, math.pi, "corner_nw") + -- West straight (Tokyo Tower run), heading +Z + z = -sz + STEP + while z <= sz do + push(Vector3.new(-HX, 0, z), "west") + z += STEP + end + arc(-sx, sz, 3 * math.pi / 2, "corner_sw") + + return samples +end + +-- Daikoku PA entrance gap on the south straight's OUTER side ------------------- +local DAIKOKU_GAP_HALF = 270 + +local function isDaikokuGap(mid: Vector3, piece: string, outer: boolean): boolean + return piece == "south" and outer and math.abs(mid.X) < DAIKOKU_GAP_HALF +end + +-- The loop road itself ---------------------------------------------------------- +local function buildLoop(map: Folder, samples: { Sample }) + local roadFolder = Instance.new("Folder") + roadFolder.Name = "Expressway" + roadFolder.Parent = map + + local n = #samples + for i = 1, n do + local a = samples[i] + local b = samples[(i % n) + 1] + local delta = b.pos - a.pos + local len = delta.Magnitude + 2 + local mid = (a.pos + b.pos) / 2 + local dir = delta.Unit + local right = dir:Cross(UP) -- always points OUTWARD on this loop + local cf = CFrame.lookAt(mid, mid + dir) + local road = part({ Name = "Road_" .. i, - Size = Vector3.new(ROAD_WIDTH, 2, SEGMENT_LENGTH), - Position = Vector3.new(0, 0, z), + Size = Vector3.new(ROAD_WIDTH, 2, len), + CFrame = cf, Color = Color3.fromRGB(28, 28, 34), Material = Enum.Material.Asphalt, }) - road.Parent = parent + road.Parent = roadFolder local line = part({ Name = "Lane_" .. i, - Size = Vector3.new(1, 2.1, SEGMENT_LENGTH), - Position = Vector3.new(0, 0.1, z), + Size = Vector3.new(1, 2.1, len), + CFrame = cf * CFrame.new(0, 0.1, 0), Color = Color3.fromRGB(230, 200, 60), Material = Enum.Material.Neon, }) - line.Parent = parent + line.Parent = roadFolder + + local inTunnel = a.piece == "north" + for _, side in ipairs({ -1, 1 }) do + local outer = side == 1 + if isDaikokuGap(mid, a.piece, outer) then + continue + end + local off = right * (side * (ROAD_WIDTH / 2)) + local rail = part({ + Name = "Guardrail", + Size = Vector3.new(1.5, 4, len), + CFrame = CFrame.lookAt(mid + off, mid + off + dir) * CFrame.new(0, 2, 0), + Color = Color3.fromRGB(180, 180, 190), + Material = Enum.Material.Metal, + }) + rail.Parent = map + + local woff = right * (side * (ROAD_WIDTH / 2 + 2)) + local wall = part({ + Name = "KillWall", + Size = Vector3.new(2, WALL_HEIGHT, len + 6), + CFrame = CFrame.lookAt(mid + woff, mid + woff + dir) * CFrame.new(0, WALL_HEIGHT / 2, 0), + Transparency = 1, + CanCollide = true, + }) + wall.Parent = map + + -- Rainbow rail lights on the bridge; neon pylons elsewhere (not in tunnel) + if a.piece == "east" then + local lamp = part({ + Name = "BridgeLight", + Size = Vector3.new(1.2, 1.2, 1.2), + CFrame = CFrame.new(mid + off + Vector3.new(0, 4.6, 0)), + Color = RAINBOW[(i % #RAINBOW) + 1], + Material = Enum.Material.Neon, + }) + lamp.Parent = map + elseif not inTunnel and i % 3 == 0 then + local pylon = part({ + Name = "Pylon", + Size = Vector3.new(1, 14, 1), + CFrame = CFrame.new(mid + right * (side * (ROAD_WIDTH / 2 + 3)) + Vector3.new(0, 7, 0)), + Color = outer and NEON_PINK or NEON_BLUE, + Material = Enum.Material.Neon, + }) + pylon.Parent = map + end + end + + -- Yamate tunnel bore over the north straight ----------------------------- + if inTunnel then + local h = 22 + for _, side in ipairs({ -1, 1 }) do + local woff = right * (side * (ROAD_WIDTH / 2 + 1.5)) + local wallT = part({ + Name = "TunnelWall", + Size = Vector3.new(3, h, len + 2), + CFrame = CFrame.lookAt(mid + woff, mid + woff + dir) * CFrame.new(0, h / 2, 0), + Color = Color3.fromRGB(38, 38, 46), + Material = Enum.Material.Concrete, + }) + wallT.Parent = map + end + local ceiling = part({ + Name = "TunnelCeiling", + Size = Vector3.new(ROAD_WIDTH + 6, 2, len + 2), + CFrame = cf * CFrame.new(0, h + 1, 0), + Color = Color3.fromRGB(30, 30, 38), + Material = Enum.Material.Concrete, + }) + ceiling.Parent = map + if i % 2 == 0 then + local strip = part({ + Name = "TunnelLight", + Size = Vector3.new(ROAD_WIDTH * 0.5, 0.6, 2), + CFrame = cf * CFrame.new(0, h - 0.5, 0), + Color = i % 4 == 0 and NEON_BLUE or NEON_TEAL, + Material = Enum.Material.Neon, + }) + strip.Parent = map + end + end end end --- Guardrails, kill walls, pylons ---------------------------------------------- -local function buildBarriers(map: Folder, totalLength: number) - for _, side in ipairs({ -1, 1 }) do - local x = side * (ROAD_WIDTH / 2) - local rail = part({ - Name = "Guardrail_" .. (side == -1 and "L" or "R"), - Size = Vector3.new(1.5, 4, totalLength), - Position = Vector3.new(x, 2, -totalLength / 2), - Color = Color3.fromRGB(180, 180, 190), +-- Daikoku Futo PA ---------------------------------------------------------------- +local function buildDaikoku(map: Folder) + local folder = Instance.new("Folder") + folder.Name = "DaikokuPA" + folder.Parent = map + + local z0 = HZ + ROAD_WIDTH / 2 -- pad starts at the road's outer edge + local depth = 220 + local halfW = 260 + + local pad = part({ + Name = "DaikokuPad", + Size = Vector3.new(halfW * 2, 2, depth), + CFrame = CFrame.new(0, 0, z0 + depth / 2), + Color = Color3.fromRGB(34, 34, 40), + Material = Enum.Material.Asphalt, + }) + pad.Parent = folder + + -- Painted parking bays (two rows, like the real lot's herringbone rows) + for row = 0, 1 do + local zRow = z0 + 70 + row * 90 + for bx = -5, 5 do + local lineP = part({ + Name = "BayLine", + Size = Vector3.new(1, 2.1, 22), + CFrame = CFrame.new(bx * 42, 0.1, zRow), + Color = Color3.fromRGB(230, 230, 235), + Material = Enum.Material.SmoothPlastic, + }) + lineP.Parent = folder + end + end + + -- Light towers (the tall lot floods) + perimeter kill walls + for _, corner in ipairs({ + { -halfW + 20, z0 + 30 }, + { halfW - 20, z0 + 30 }, + { -halfW + 20, z0 + depth - 20 }, + { halfW - 20, z0 + depth - 20 }, + }) do + local px, pz = corner[1], corner[2] + local pole = part({ + Name = "LightPole", + Size = Vector3.new(2, 34, 2), + CFrame = CFrame.new(px, 17, pz), + Color = Color3.fromRGB(120, 120, 130), Material = Enum.Material.Metal, }) - rail.Parent = map + pole.Parent = folder + local head = part({ + Name = "LightHead", + Size = Vector3.new(8, 1.5, 8), + CFrame = CFrame.new(px, 34.5, pz), + Color = Color3.fromRGB(255, 240, 200), + Material = Enum.Material.Neon, + }) + head.Parent = folder + end - local wall = part({ - Name = "KillWall_" .. (side == -1 and "L" or "R"), - Size = Vector3.new(2, WALL_HEIGHT, totalLength), - Position = Vector3.new(x + side * 2, WALL_HEIGHT / 2, -totalLength / 2), + -- Perimeter walls (invisible) on the three non-road sides + local wallDefs = { + { Vector3.new(halfW * 2 + 4, WALL_HEIGHT, 2), CFrame.new(0, WALL_HEIGHT / 2, z0 + depth + 1) }, + { Vector3.new(2, WALL_HEIGHT, depth + 4), CFrame.new(-halfW - 1, WALL_HEIGHT / 2, z0 + depth / 2) }, + { Vector3.new(2, WALL_HEIGHT, depth + 4), CFrame.new(halfW + 1, WALL_HEIGHT / 2, z0 + depth / 2) }, + } + for _, def in ipairs(wallDefs) do + local w = part({ + Name = "KillWall_PA", + Size = def[1], + CFrame = def[2], Transparency = 1, CanCollide = true, }) - wall.Parent = map - - for i = 0, math.floor(totalLength / 200) do - local pylon = part({ - Name = "Pylon", - Size = Vector3.new(1, 14, 1), - Position = Vector3.new(x + side * 3, 7, -(i * 200)), - Color = side == -1 and NEON_PINK or NEON_BLUE, - Material = Enum.Material.Neon, - }) - pylon.Parent = map - end + w.Parent = folder end + + -- Spawn pad (cars appear here — Daikoku is the meet) ----------------------- + local spawn = part({ + Name = "SpawnPad", + Size = Vector3.new(120, 1, 60), + CFrame = CFrame.new(0, 1.2, z0 + 130), + Color = Color3.fromRGB(20, 220, 120), + Material = Enum.Material.Neon, + Transparency = 0.4, + }) + spawn.Parent = folder + + makeSign("大黒PA DAIKOKU PA", CFrame.new(0, 12, z0 + 4) * CFrame.Angles(0, math.pi, 0), folder) end --- Overhead gantry arches (segment 2) ------------------------------------------- -local function buildGantries(map: Folder) +-- Rainbow Bridge towers over the east straight ----------------------------------- +local function buildRainbowBridge(map: Folder) local folder = Instance.new("Folder") - folder.Name = "Gantries" + folder.Name = "RainbowBridge" folder.Parent = map - local segStart = 2 * SEGMENT_LENGTH - for g = 0, 3 do - local z = -(segStart + 60 + g * 90) + for _, tz in ipairs({ 250, -250 }) do for _, side in ipairs({ -1, 1 }) do - local post = part({ - Name = "GantryPost", - Size = Vector3.new(2, 24, 2), - Position = Vector3.new(side * (ROAD_WIDTH / 2 - 2), 12, z), - Color = Color3.fromRGB(60, 60, 70), - Material = Enum.Material.DiamondPlate, + local tx = HX + side * (ROAD_WIDTH / 2 + 5) + local tower = part({ + Name = "BridgeTower", + Size = Vector3.new(6, 95, 6), + CFrame = CFrame.new(tx, 47.5, tz), + Color = Color3.fromRGB(235, 235, 240), + Material = Enum.Material.Concrete, }) - post.Parent = folder + tower.Parent = folder + local beacon = part({ + Name = "TowerBeacon", + Size = Vector3.new(2, 2, 2), + CFrame = CFrame.new(tx, 96, tz), + Color = Color3.fromRGB(255, 80, 80), + Material = Enum.Material.Neon, + }) + beacon.Parent = folder end local beam = part({ - Name = "GantryBeam", - Size = Vector3.new(ROAD_WIDTH, 2.5, 2.5), - Position = Vector3.new(0, 24, z), - Color = g % 2 == 0 and NEON_TEAL or NEON_PINK, - Material = Enum.Material.Neon, + Name = "TowerBeam", + Size = Vector3.new(ROAD_WIDTH + 22, 4, 5), + CFrame = CFrame.new(HX, 62, tz), + Color = Color3.fromRGB(235, 235, 240), + Material = Enum.Material.Concrete, }) beam.Parent = folder end + makeSign( + "レインボーブリッジ RAINBOW BRIDGE", + CFrame.new(HX, 14, 330) * CFrame.Angles(0, math.pi / 2, 0), + folder + ) end --- Tunnel (segment 3): walls + ceiling with neon strips --------------------------- -local function buildTunnel(map: Folder) +-- Odaiba: blocky Unicorn-Gundam-style statue + giant ferris wheel ---------------- +local function buildOdaiba(map: Folder) local folder = Instance.new("Folder") - folder.Name = "Tunnel" + folder.Name = "Odaiba" folder.Parent = map - local zCenter = -(3 * SEGMENT_LENGTH) - SEGMENT_LENGTH / 2 - local tunnelHeight = 22 + local gx, gz = HX + 150, 480 -- statue stands beyond the bridge's south end + local white = Color3.fromRGB(235, 235, 240) - for _, side in ipairs({ -1, 1 }) do - local wall = part({ - Name = "TunnelWall_" .. (side == -1 and "L" or "R"), - Size = Vector3.new(3, tunnelHeight, SEGMENT_LENGTH), - Position = Vector3.new(side * (ROAD_WIDTH / 2 + 1.5), tunnelHeight / 2, zCenter), - Color = Color3.fromRGB(38, 38, 46), - Material = Enum.Material.Concrete, + local function block(name: string, size: Vector3, cf: CFrame, color: Color3, mat: Enum.Material?) + local b = part({ + Name = name, + Size = size, + CFrame = cf, + Color = color, + Material = mat or Enum.Material.SmoothPlastic, }) - wall.Parent = folder + b.Parent = folder + return b end - local ceiling = part({ - Name = "TunnelCeiling", - Size = Vector3.new(ROAD_WIDTH + 6, 2, SEGMENT_LENGTH), - Position = Vector3.new(0, tunnelHeight + 1, zCenter), - Color = Color3.fromRGB(30, 30, 38), - Material = Enum.Material.Concrete, - }) - ceiling.Parent = folder + -- Base plinth + block( + "GundamBase", + Vector3.new(40, 4, 40), + CFrame.new(gx, 2, gz), + Color3.fromRGB(60, 60, 70), + Enum.Material.Concrete + ) + -- Legs + block("GundamLegL", Vector3.new(7, 20, 9), CFrame.new(gx - 6, 14, gz), white) + block("GundamLegR", Vector3.new(7, 20, 9), CFrame.new(gx + 6, 14, gz), white) + -- Hips + torso + block("GundamHips", Vector3.new(16, 6, 10), CFrame.new(gx, 27, gz), white) + block("GundamTorso", Vector3.new(15, 14, 10), CFrame.new(gx, 37, gz), white) + block("GundamChest", Vector3.new(9, 5, 3), CFrame.new(gx, 39, gz - 6), Color3.fromRGB(60, 90, 220)) + block("GundamCore", Vector3.new(4, 3, 2), CFrame.new(gx, 34, gz - 6), Color3.fromRGB(230, 60, 60)) + -- Shoulders + arms + block("GundamShoulderL", Vector3.new(6, 5, 7), CFrame.new(gx - 11, 42, gz), white) + block("GundamShoulderR", Vector3.new(6, 5, 7), CFrame.new(gx + 11, 42, gz), white) + block("GundamArmL", Vector3.new(4.5, 16, 5), CFrame.new(gx - 11, 32, gz), white) + block("GundamArmR", Vector3.new(4.5, 16, 5), CFrame.new(gx + 11, 32, gz), white) + -- Head with glowing eyes + unicorn horn + block("GundamHead", Vector3.new(6, 5.5, 5.5), CFrame.new(gx, 47, gz), white) + block("GundamEyes", Vector3.new(4, 1, 0.6), CFrame.new(gx, 47.5, gz - 2.8), NEON_TEAL, Enum.Material.Neon) + block( + "GundamHorn", + Vector3.new(0.8, 6, 0.8), + CFrame.new(gx, 53, gz - 1) * CFrame.Angles(math.rad(15), 0, 0), + Color3.fromRGB(250, 220, 90), + Enum.Material.Neon + ) + -- Psycho-frame glow strips on the legs/torso (the Unicorn's pink frame) + block("GundamFrameL", Vector3.new(1, 14, 1), CFrame.new(gx - 6, 14, gz - 4.6), NEON_PINK, Enum.Material.Neon) + block("GundamFrameR", Vector3.new(1, 14, 1), CFrame.new(gx + 6, 14, gz - 4.6), NEON_PINK, Enum.Material.Neon) - -- Neon ceiling strips light the tunnel (one every 50 studs, alternating). - for i = 0, math.floor(SEGMENT_LENGTH / 50) do - local z = -(3 * SEGMENT_LENGTH) - i * 50 - local strip = part({ - Name = "TunnelLight", - Size = Vector3.new(ROAD_WIDTH * 0.5, 0.6, 2), - Position = Vector3.new(0, tunnelHeight - 0.5, z), - Color = i % 2 == 0 and NEON_BLUE or NEON_TEAL, + -- Giant ferris wheel (Palette Town) ----------------------------------------- + local wx, wz = HX + 240, 120 + local radius = 55 + local hub = CFrame.new(wx, radius + 24, wz) + block( + "WheelSupportL", + Vector3.new(4, radius + 26, 4), + CFrame.new(wx - 18, (radius + 24) / 2, wz) * CFrame.Angles(0, 0, math.rad(-14)), + Color3.fromRGB(120, 120, 130), + Enum.Material.Metal + ) + block( + "WheelSupportR", + Vector3.new(4, radius + 26, 4), + CFrame.new(wx + 18, (radius + 24) / 2, wz) * CFrame.Angles(0, 0, math.rad(14)), + Color3.fromRGB(120, 120, 130), + Enum.Material.Metal + ) + block("WheelHub", Vector3.new(6, 6, 6), hub, Color3.fromRGB(200, 200, 210), Enum.Material.Metal) + for k = 1, 16 do + local ang = (k / 16) * math.pi * 2 + local cab = part({ + Name = "WheelCabin", + Size = Vector3.new(4, 4, 4), + Shape = Enum.PartType.Ball, + CFrame = hub * CFrame.new(math.cos(ang) * radius, math.sin(ang) * radius, 0), + Color = RAINBOW[(k % #RAINBOW) + 1], Material = Enum.Material.Neon, }) - strip.Parent = folder + cab.Parent = folder + if k % 2 == 0 then + local spoke = part({ + Name = "WheelSpoke", + Size = Vector3.new(0.8, radius, 0.8), + CFrame = hub * CFrame.Angles(0, 0, ang) * CFrame.new(0, radius / 2, 0), + Color = Color3.fromRGB(150, 150, 160), + Material = Enum.Material.Metal, + }) + spoke.Parent = folder + end end + + makeSign("お台場 ODAIBA", CFrame.new(HX + 60, 12, 300) * CFrame.Angles(0, math.pi / 2, 0), folder) end --- Jump ramps (segment 4): launch + landing pair on the right half --------------- -local function buildRamps(map: Folder) +-- Tokyo Tower replica on the west side -------------------------------------------- +local function buildTokyoTower(map: Folder) local folder = Instance.new("Folder") - folder.Name = "Ramps" + folder.Name = "TokyoTower" folder.Parent = map - local zStart = 4 * SEGMENT_LENGTH + 100 - local rampX = ROAD_WIDTH / 4 -- right half of the road; left half stays flat - - -- Launch ramp: a wedge sloping up in the direction of travel (-Z). - local launch = Instance.new("WedgePart") - launch.Anchored = true - launch.Name = "RampLaunch" - launch.Size = Vector3.new(ROAD_WIDTH / 2 - 4, 10, 48) - -- WedgePart slopes from its -Z (tall) to +Z (thin edge); car travels -Z, - -- so face the thin edge toward the approaching car (+Z toward car = yaw 180). - launch.CFrame = CFrame.new(rampX, 5, -zStart) * CFrame.Angles(0, math.rad(180), 0) - launch.Color = Color3.fromRGB(50, 50, 62) - launch.Material = Enum.Material.Metal - launch.TopSurface = Enum.SurfaceType.Smooth - launch.Parent = folder - - local edge = part({ - Name = "RampEdgeGlow", - Size = Vector3.new(ROAD_WIDTH / 2 - 4, 0.8, 1), - Position = Vector3.new(rampX, 10.4, -zStart - 24), - Color = NEON_PINK, + local tx, tz = -HX - 220, 0 + local orange = Color3.fromRGB(235, 100, 40) + local levels = { + { w = 64, h = 50, c = orange }, + { w = 44, h = 45, c = Color3.fromRGB(240, 240, 245) }, + { w = 28, h = 45, c = orange }, + { w = 15, h = 40, c = Color3.fromRGB(240, 240, 245) }, + } + local y = 0 + for li, lv in ipairs(levels) do + local sec = part({ + Name = "TowerSection" .. li, + Size = Vector3.new(lv.w, lv.h, lv.w), + CFrame = CFrame.new(tx, y + lv.h / 2, tz), + Color = lv.c, + Material = Enum.Material.Metal, + }) + sec.Parent = folder + y += lv.h + end + -- Observation decks + antenna + local deck1 = part({ + Name = "MainDeck", + Size = Vector3.new(36, 8, 36), + CFrame = CFrame.new(tx, 98, tz), + Color = Color3.fromRGB(250, 245, 235), Material = Enum.Material.Neon, }) - edge.Parent = folder - - -- Landing ramp: mirrored wedge further down so clean jumps land smooth. - local landing = Instance.new("WedgePart") - landing.Anchored = true - landing.Name = "RampLanding" - landing.Size = Vector3.new(ROAD_WIDTH / 2 - 4, 10, 60) - landing.CFrame = CFrame.new(rampX, 5, -(zStart + 150)) - landing.Color = Color3.fromRGB(50, 50, 62) - landing.Material = Enum.Material.Metal - landing.TopSurface = Enum.SurfaceType.Smooth - landing.Parent = folder + deck1.Parent = folder + local deck2 = part({ + Name = "TopDeck", + Size = Vector3.new(20, 6, 20), + CFrame = CFrame.new(tx, 152, tz), + Color = Color3.fromRGB(250, 245, 235), + Material = Enum.Material.Neon, + }) + deck2.Parent = folder + local antenna = part({ + Name = "Antenna", + Size = Vector3.new(2, 40, 2), + CFrame = CFrame.new(tx, 200, tz), + Color = orange, + Material = Enum.Material.Neon, + }) + antenna.Parent = folder end --- Neon skyline: procedural building blocks flanking the route ------------------- -local function buildSkyline(map: Folder, totalLength: number) +-- Tatsumi-style overlook pad (visual scenery, NE corner outside) ------------------ +local function buildTatsumi(map: Folder) + local folder = Instance.new("Folder") + folder.Name = "TatsumiPA" + folder.Parent = map + local px, pz = HX - 380, -HZ - 120 + local pad = part({ + Name = "TatsumiPad", + Size = Vector3.new(160, 2, 80), + CFrame = CFrame.new(px, 0, pz), + Color = Color3.fromRGB(34, 34, 40), + Material = Enum.Material.Asphalt, + }) + pad.Parent = folder + for i = 0, 4 do + local lamp = part({ + Name = "TatsumiLamp", + Size = Vector3.new(1, 12, 1), + CFrame = CFrame.new(px - 70 + i * 35, 6, pz - 30), + Color = Color3.fromRGB(255, 240, 200), + Material = Enum.Material.Neon, + }) + lamp.Parent = folder + end + makeSign("辰巳PA TATSUMI PA", CFrame.new(px, 11, pz + 30), folder) +end + +-- Neon skyline: towers inside the loop (city core) and outside --------------------- +local function buildSkyline(map: Folder) local folder = Instance.new("Folder") folder.Name = "Skyline" folder.Parent = map - local accents = { NEON_PINK, NEON_BLUE, NEON_TEAL, Color3.fromRGB(200, 120, 255) } - for _, side in ipairs({ -1, 1 }) do - local n = 0 - local z = -80 - while z > -totalLength do - n += 1 - local seed = side * 1000 + n - local width = 40 + det(seed) * 60 - local depth = 40 + det(seed + 1) * 60 - local height = 40 + det(seed + 2) * 160 - local gap = 30 + det(seed + 3) * 70 - local x = side * (ROAD_WIDTH / 2 + 50 + det(seed + 4) * 90 + width / 2) + local function tower(seed: number, x: number, z: number) + local width = 40 + det(seed) * 50 + local depth = 40 + det(seed + 1) * 50 + local height = 50 + det(seed + 2) * 170 + local t = part({ + Name = "Tower", + Size = Vector3.new(width, height, depth), + CFrame = CFrame.new(x, height / 2 - 10, z), + Color = Color3.fromRGB(16, 16, 24), + Material = Enum.Material.Slate, + }) + t.Parent = folder + local accent = accents[(math.floor(seed) % #accents) + 1] + local band = part({ + Name = "TowerBand", + Size = Vector3.new(width + 0.4, 2 + det(seed + 5) * 4, depth + 0.4), + CFrame = CFrame.new(x, height * (0.35 + det(seed + 6) * 0.4), z), + Color = accent, + Material = Enum.Material.Neon, + Transparency = 0.2, + }) + band.Parent = folder + local roof = part({ + Name = "TowerRoof", + Size = Vector3.new(width * 0.3, 1.5, depth * 0.3), + CFrame = CFrame.new(x, height - 9, z), + Color = accent, + Material = Enum.Material.Neon, + }) + roof.Parent = folder + end - local tower = part({ - Name = "Tower", - Size = Vector3.new(width, height, depth), - Position = Vector3.new(x, height / 2 - 10, z - depth / 2), - Color = Color3.fromRGB(16, 16, 24), - Material = Enum.Material.Slate, - }) - tower.Parent = folder - - -- Lit window band + rooftop accent (2 parts per tower, cheap). - local accent = accents[(n % #accents) + 1] - local band = part({ - Name = "TowerBand", - Size = Vector3.new(width + 0.4, 2 + det(seed + 5) * 4, depth + 0.4), - Position = Vector3.new(x, height * (0.4 + det(seed + 6) * 0.4), z - depth / 2), - Color = accent, - Material = Enum.Material.Neon, - Transparency = 0.2, - }) - band.Parent = folder - - local roof = part({ - Name = "TowerRoof", - Size = Vector3.new(width * 0.3, 1.5, depth * 0.3), - Position = Vector3.new(x, height - 9, z - depth / 2), - Color = accent, - Material = Enum.Material.Neon, - }) - roof.Parent = folder - - z = z - depth - gap + -- City core inside the loop + local seed = 1 + for gx = -2, 2 do + for gz = -1, 1 do + seed += 7 + local x = gx * 300 + (det(seed) - 0.5) * 120 + local z = gz * 300 + (det(seed + 3) - 0.5) * 120 + tower(seed, x, z) + end + end + -- Outer ring (skip landmark zones: Odaiba east, Tokyo Tower west-center, + -- Daikoku south-center) + for i = 1, 26 do + seed += 11 + local ang = (i / 26) * math.pi * 2 + local rad = 1750 + det(seed + 4) * 350 + local x = math.cos(ang) * rad + local z = math.sin(ang) * rad * 0.75 + local skip = (x > HX and math.abs(z) < 620) -- Odaiba + or (x < -HX and math.abs(z) < 260) -- Tokyo Tower + or (z > HZ and math.abs(x) < 420) -- Daikoku + if not skip then + tower(seed, x, z) end end end @@ -288,61 +658,46 @@ function MapBuilder.build(): Folder local map = Instance.new("Folder") map.Name = "Map" - local roadFolder = Instance.new("Folder") - roadFolder.Name = "Expressway" - roadFolder.Parent = map + local samples = genSamples() + buildLoop(map, samples) + buildDaikoku(map) + buildRainbowBridge(map) + buildOdaiba(map) + buildTokyoTower(map) + buildTatsumi(map) + buildSkyline(map) - local totalLength = SEGMENT_LENGTH * NUM_SEGMENTS - - buildRoad(roadFolder) - buildBarriers(map, totalLength) - buildGantries(map) - buildTunnel(map) - buildRamps(map) - buildSkyline(map, totalLength) - - -- Spawn pad (where cars appear) at the start ------------------------------ - local pad = part({ - Name = "SpawnPad", - Size = Vector3.new(ROAD_WIDTH - 6, 1, 40), - Position = Vector3.new(0, 1.5, -20), - Color = Color3.fromRGB(20, 220, 120), - Material = Enum.Material.Neon, - Transparency = 0.4, - }) - pad.Parent = map - - -- Finish line at the far end ---------------------------------------------- + -- Finish line across the south straight just past the Daikoku exit -------- local finish = part({ Name = "FinishLine", Size = Vector3.new(ROAD_WIDTH, 1, 6), - Position = Vector3.new(0, 1.5, -totalLength + 30), + CFrame = CFrame.new(420, 1.5, HZ), Color = Color3.fromRGB(240, 240, 240), Material = Enum.Material.Neon, Transparency = 0.3, }) finish.Parent = map - -- End wall so you can't sail off the far edge ------------------------------ - local endWall = part({ - Name = "KillWall_End", - Size = Vector3.new(ROAD_WIDTH + 8, WALL_HEIGHT, 2), - Position = Vector3.new(0, WALL_HEIGHT / 2, -totalLength - 1), - Transparency = 1, - CanCollide = true, - }) - endWall.Parent = map - - -- A floor far below to catch anything that clips through (safety net) ------ + -- Safety net floor under everything ---------------------------------------- local floor = part({ Name = "SafetyFloor", - Size = Vector3.new(ROAD_WIDTH * 4, 2, totalLength + 400), - Position = Vector3.new(0, -60, -totalLength / 2), + Size = Vector3.new((HX + 700) * 2, 2, (HZ + 700) * 2), + CFrame = CFrame.new(0, -60, 0), Transparency = 1, CanCollide = true, }) floor.Parent = map + -- Ground plane far below road level (the "bay" the expressway floats over) + local ground = part({ + Name = "Ground", + Size = Vector3.new((HX + 700) * 2, 2, (HZ + 700) * 2), + CFrame = CFrame.new(0, -12, 0), + Color = Color3.fromRGB(10, 12, 20), + Material = Enum.Material.Slate, + }) + ground.Parent = map + map.Parent = Workspace return map end