Add three-mode Wangan loop; rename project to Tokyo Wangan Racing
CI / quality (push) Successful in 1m18s
CI / quality (push) Successful in 1m18s
Daikoku Futo PA becomes the hub (Tokyo Xtreme Racer structure): park, pick a
mode, drive out the exit gate to start the run, pull back into the PA to end it.
Modes:
- CRUISE free roam, banks session + lifetime kilometres
- DUEL gap-based battle: hold 350 studs for 3s to win
- TIME ATTACK solo flying lap, 4 sector splits, saved personal best
New shared modules:
- Circuit.luau one source of truth for loop geometry. progressOf(pos) gives
lap distance, which drives lap detection, sectors and duel gaps
alike, so the map and gameplay can never drift apart.
- Modes.luau mode definitions + duel/time-attack tuning constants
Server-authoritative throughout: ModeManager samples the car real position on
Heartbeat and the client never reports distance, lap time or gap. Teleport and
respawn jumps are rejected from distance accumulation.
ProfileTemplate v3 adds DistanceStuds, BestLapTime, BestSectors, DuelsWon and
DuelsLost; ProfileStore Reconcile migrates existing saves.
Rename: repo, wally package, local dir and stale NAS asset paths all move off
the old fnf-sim name. NAS folder was already 0-tokyo-wangan-racing, so those
doc paths were broken before this.
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
--!strict
|
||||
--[[
|
||||
ModeUI.luau (CLIENT)
|
||||
|
||||
Everything the three-mode loop shows the player:
|
||||
|
||||
* PA panel — the mode picker. Only visible while parked at Daikoku.
|
||||
Pick CRUISE / DUEL / TIME ATTACK, then drive out the gate.
|
||||
* Run HUD — mode-specific live readout while out on the Wangan:
|
||||
CRUISE -> session + lifetime kilometres
|
||||
TIME ATTACK -> lap clock, sector splits, best lap
|
||||
DUEL -> opponent, gap bar, who's breaking away
|
||||
* Duel prompt — a rival pulled alongside: RUN IT / NOT NOW.
|
||||
* Result card — duel won/lost, lap set.
|
||||
|
||||
Purely presentational. Every number here came from the server; this module
|
||||
never decides anything.
|
||||
]]
|
||||
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Shared = ReplicatedStorage.Shared
|
||||
local Modes = require(Shared.Modes)
|
||||
local Remotes = require(Shared.Remotes)
|
||||
|
||||
local ModeUI = {}
|
||||
|
||||
local NEON_PINK = Color3.fromRGB(255, 40, 120)
|
||||
local NEON_BLUE = Color3.fromRGB(40, 160, 255)
|
||||
local NEON_GREEN = Color3.fromRGB(120, 255, 170)
|
||||
local PANEL_BG = Color3.fromRGB(16, 16, 24)
|
||||
local DIM = Color3.fromRGB(150, 150, 165)
|
||||
|
||||
local rSelectMode = Remotes.get("SelectMode") :: RemoteFunction
|
||||
local rRespondDuel = Remotes.get("RespondDuel") :: RemoteEvent
|
||||
local rAbandonRun = Remotes.get("AbandonRun") :: RemoteEvent
|
||||
local rModeUpdated = Remotes.get("ModeUpdated") :: RemoteEvent
|
||||
local rDuelOffer = Remotes.get("DuelOffer") :: RemoteEvent
|
||||
local rDuelResult = Remotes.get("DuelResult") :: RemoteEvent
|
||||
|
||||
local function make(class: string, props: { [string]: any }, parent: Instance?): any
|
||||
local inst = Instance.new(class)
|
||||
for k, v in pairs(props) do
|
||||
(inst :: any)[k] = v
|
||||
end
|
||||
if parent then
|
||||
inst.Parent = parent
|
||||
end
|
||||
return inst
|
||||
end
|
||||
|
||||
local function corner(inst: Instance, radius: number)
|
||||
make("UICorner", { CornerRadius = UDim.new(0, radius) }, inst)
|
||||
end
|
||||
|
||||
-- Format seconds as M:SS.mmm — the racing-timer convention.
|
||||
local function fmtTime(t: number?): string
|
||||
if not t or t <= 0 then
|
||||
return "--:--.---"
|
||||
end
|
||||
local m = math.floor(t / 60)
|
||||
local s = t - m * 60
|
||||
return string.format("%d:%06.3f", m, s)
|
||||
end
|
||||
|
||||
local screen: ScreenGui
|
||||
local paPanel: Frame
|
||||
local runHud: Frame
|
||||
local hudTitle: TextLabel
|
||||
local hudMain: TextLabel
|
||||
local hudSub: TextLabel
|
||||
local gapBar: Frame
|
||||
local gapFill: Frame
|
||||
local gapLabel: TextLabel
|
||||
local duelPrompt: Frame
|
||||
local duelPromptLabel: TextLabel
|
||||
local resultCard: Frame
|
||||
local resultTitle: TextLabel
|
||||
local resultSub: TextLabel
|
||||
|
||||
local lastState: string? = nil
|
||||
|
||||
-- Build -----------------------------------------------------------------------
|
||||
|
||||
function ModeUI.build(parentGui: ScreenGui)
|
||||
screen = parentGui
|
||||
|
||||
-- PA panel: the mode picker ----------------------------------------------
|
||||
paPanel = make("Frame", {
|
||||
Name = "PAPanel",
|
||||
AnchorPoint = Vector2.new(0.5, 1),
|
||||
Position = UDim2.new(0.5, 0, 1, -90),
|
||||
Size = UDim2.new(0, 720, 0, 210),
|
||||
BackgroundColor3 = PANEL_BG,
|
||||
BackgroundTransparency = 0.08,
|
||||
BorderSizePixel = 0,
|
||||
Visible = false,
|
||||
}, screen)
|
||||
corner(paPanel, 12)
|
||||
make("UIStroke", { Color = NEON_BLUE, Thickness = 1.5, Transparency = 0.4 }, paPanel)
|
||||
|
||||
make("TextLabel", {
|
||||
Text = "大黒PA — CHOOSE YOUR RUN",
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 15,
|
||||
TextColor3 = Color3.fromRGB(235, 235, 245),
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 10),
|
||||
Size = UDim2.new(1, 0, 0, 20),
|
||||
}, paPanel)
|
||||
|
||||
make("TextLabel", {
|
||||
Name = "Hint",
|
||||
Text = "Pick a mode, then drive out the yellow gate onto the Wangan.",
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 12,
|
||||
TextColor3 = DIM,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 1, -26),
|
||||
Size = UDim2.new(1, 0, 0, 18),
|
||||
}, paPanel)
|
||||
|
||||
local row = make("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 16, 0, 40),
|
||||
Size = UDim2.new(1, -32, 0, 130),
|
||||
}, paPanel)
|
||||
make("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 12),
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
}, row)
|
||||
|
||||
for i, mode in ipairs(Modes.List) do
|
||||
local card = make("TextButton", {
|
||||
Name = mode.Id,
|
||||
Text = "",
|
||||
AutoButtonColor = false,
|
||||
BackgroundColor3 = Color3.fromRGB(24, 24, 34),
|
||||
Size = UDim2.new(0, 224, 1, 0),
|
||||
LayoutOrder = i,
|
||||
BorderSizePixel = 0,
|
||||
}, row)
|
||||
corner(card, 10)
|
||||
local stroke = make("UIStroke", { Color = mode.Color, Thickness = 1.5, Transparency = 0.55 }, card)
|
||||
|
||||
make("TextLabel", {
|
||||
Text = mode.NameJP,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 13,
|
||||
TextColor3 = mode.Color,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 10),
|
||||
Size = UDim2.new(1, 0, 0, 16),
|
||||
}, card)
|
||||
make("TextLabel", {
|
||||
Text = mode.Name,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 19,
|
||||
TextColor3 = Color3.fromRGB(240, 240, 250),
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 28),
|
||||
Size = UDim2.new(1, 0, 0, 22),
|
||||
}, card)
|
||||
make("TextLabel", {
|
||||
Text = mode.Blurb,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 11,
|
||||
TextColor3 = DIM,
|
||||
TextWrapped = true,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 12, 0, 56),
|
||||
Size = UDim2.new(1, -24, 1, -66),
|
||||
}, card)
|
||||
|
||||
card.MouseEnter:Connect(function()
|
||||
TweenService:Create(stroke, TweenInfo.new(0.15), { Transparency = 0 }):Play()
|
||||
end)
|
||||
card.MouseLeave:Connect(function()
|
||||
TweenService:Create(stroke, TweenInfo.new(0.15), { Transparency = 0.55 }):Play()
|
||||
end)
|
||||
card.MouseButton1Click:Connect(function()
|
||||
local ok = rSelectMode:InvokeServer(mode.Id)
|
||||
if ok then
|
||||
ModeUI.showArmed(mode)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Run HUD ----------------------------------------------------------------
|
||||
runHud = make("Frame", {
|
||||
Name = "RunHUD",
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Position = UDim2.new(0.5, 0, 0, 16),
|
||||
Size = UDim2.new(0, 340, 0, 104),
|
||||
BackgroundColor3 = PANEL_BG,
|
||||
BackgroundTransparency = 0.15,
|
||||
BorderSizePixel = 0,
|
||||
Visible = false,
|
||||
}, screen)
|
||||
corner(runHud, 10)
|
||||
make("UIStroke", { Color = NEON_PINK, Thickness = 1.2, Transparency = 0.5 }, runHud)
|
||||
|
||||
hudTitle = make("TextLabel", {
|
||||
Text = "",
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 12,
|
||||
TextColor3 = NEON_PINK,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 8),
|
||||
Size = UDim2.new(1, 0, 0, 14),
|
||||
}, runHud)
|
||||
|
||||
hudMain = make("TextLabel", {
|
||||
Text = "",
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 30,
|
||||
TextColor3 = Color3.fromRGB(245, 245, 255),
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 24),
|
||||
Size = UDim2.new(1, 0, 0, 36),
|
||||
}, runHud)
|
||||
|
||||
hudSub = make("TextLabel", {
|
||||
Text = "",
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 12,
|
||||
TextColor3 = DIM,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 60),
|
||||
Size = UDim2.new(1, 0, 0, 16),
|
||||
}, runHud)
|
||||
|
||||
-- Duel gap bar (hidden unless duelling)
|
||||
gapBar = make("Frame", {
|
||||
Name = "GapBar",
|
||||
BackgroundColor3 = Color3.fromRGB(35, 35, 48),
|
||||
BorderSizePixel = 0,
|
||||
Position = UDim2.new(0, 20, 0, 80),
|
||||
Size = UDim2.new(1, -40, 0, 8),
|
||||
Visible = false,
|
||||
}, runHud)
|
||||
corner(gapBar, 4)
|
||||
gapFill = make("Frame", {
|
||||
BackgroundColor3 = NEON_PINK,
|
||||
BorderSizePixel = 0,
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Position = UDim2.new(0.5, 0, 0, 0),
|
||||
Size = UDim2.new(0, 4, 1, 0),
|
||||
}, gapBar)
|
||||
corner(gapFill, 4)
|
||||
gapLabel = make("TextLabel", {
|
||||
Text = "",
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 11,
|
||||
TextColor3 = DIM,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 88),
|
||||
Size = UDim2.new(1, 0, 0, 14),
|
||||
Visible = false,
|
||||
}, runHud)
|
||||
|
||||
-- Duel prompt ------------------------------------------------------------
|
||||
duelPrompt = make("Frame", {
|
||||
Name = "DuelPrompt",
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.62, 0),
|
||||
Size = UDim2.new(0, 380, 0, 120),
|
||||
BackgroundColor3 = PANEL_BG,
|
||||
BackgroundTransparency = 0.05,
|
||||
BorderSizePixel = 0,
|
||||
Visible = false,
|
||||
}, screen)
|
||||
corner(duelPrompt, 12)
|
||||
make("UIStroke", { Color = NEON_PINK, Thickness = 2, Transparency = 0.1 }, duelPrompt)
|
||||
|
||||
duelPromptLabel = make("TextLabel", {
|
||||
Text = "",
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 16,
|
||||
TextColor3 = Color3.fromRGB(245, 245, 255),
|
||||
TextWrapped = true,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 16, 0, 14),
|
||||
Size = UDim2.new(1, -32, 0, 46),
|
||||
}, duelPrompt)
|
||||
|
||||
local btnRow = make("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 16, 1, -50),
|
||||
Size = UDim2.new(1, -32, 0, 36),
|
||||
}, duelPrompt)
|
||||
make("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
Padding = UDim.new(0, 10),
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
}, btnRow)
|
||||
|
||||
local function promptBtn(text: string, color: Color3, accept: boolean)
|
||||
local b = make("TextButton", {
|
||||
Text = text,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
TextColor3 = Color3.fromRGB(10, 10, 14),
|
||||
BackgroundColor3 = color,
|
||||
Size = UDim2.new(0, 160, 1, 0),
|
||||
BorderSizePixel = 0,
|
||||
}, btnRow)
|
||||
corner(b, 8)
|
||||
b.MouseButton1Click:Connect(function()
|
||||
rRespondDuel:FireServer(accept)
|
||||
duelPrompt.Visible = false
|
||||
end)
|
||||
end
|
||||
promptBtn("RUN IT", NEON_PINK, true)
|
||||
promptBtn("NOT NOW", Color3.fromRGB(90, 90, 110), false)
|
||||
|
||||
-- Result card ------------------------------------------------------------
|
||||
resultCard = make("Frame", {
|
||||
Name = "ResultCard",
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.new(0.5, 0, 0.42, 0),
|
||||
Size = UDim2.new(0, 420, 0, 110),
|
||||
BackgroundColor3 = PANEL_BG,
|
||||
BackgroundTransparency = 0.05,
|
||||
BorderSizePixel = 0,
|
||||
Visible = false,
|
||||
}, screen)
|
||||
corner(resultCard, 12)
|
||||
make("UIStroke", { Color = NEON_GREEN, Thickness = 2, Transparency = 0.15 }, resultCard)
|
||||
resultTitle = make("TextLabel", {
|
||||
Text = "",
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 26,
|
||||
TextColor3 = NEON_GREEN,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 20),
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
}, resultCard)
|
||||
resultSub = make("TextLabel", {
|
||||
Text = "",
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 14,
|
||||
TextColor3 = DIM,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(0, 0, 0, 58),
|
||||
Size = UDim2.new(1, 0, 0, 20),
|
||||
}, resultCard)
|
||||
|
||||
ModeUI.connect()
|
||||
end
|
||||
|
||||
-- Behaviour -------------------------------------------------------------------
|
||||
|
||||
function ModeUI.showArmed(mode: Modes.ModeDef)
|
||||
local hint = paPanel:FindFirstChild("Hint") :: TextLabel?
|
||||
if hint then
|
||||
hint.Text = string.format("%s armed — drive out the yellow gate to start.", mode.Name)
|
||||
hint.TextColor3 = mode.Color
|
||||
end
|
||||
end
|
||||
|
||||
local function showResult(title: string, sub: string, color: Color3)
|
||||
resultTitle.Text = title
|
||||
resultTitle.TextColor3 = color
|
||||
resultSub.Text = sub
|
||||
resultCard.Visible = true
|
||||
task.delay(5, function()
|
||||
resultCard.Visible = false
|
||||
end)
|
||||
end
|
||||
|
||||
function ModeUI.connect()
|
||||
rModeUpdated.OnClientEvent:Connect(function(state: any)
|
||||
if typeof(state) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
local inPA = state.state == "IN_PA" or state.state == "ARMED"
|
||||
paPanel.Visible = inPA
|
||||
runHud.Visible = state.state == "RUNNING"
|
||||
|
||||
-- Reset the picker hint when we newly arrive back in the PA.
|
||||
if state.state == "IN_PA" and lastState ~= "IN_PA" then
|
||||
local hint = paPanel:FindFirstChild("Hint") :: TextLabel?
|
||||
if hint then
|
||||
hint.Text = "Pick a mode, then drive out the yellow gate onto the Wangan."
|
||||
hint.TextColor3 = DIM
|
||||
end
|
||||
end
|
||||
lastState = state.state
|
||||
|
||||
if state.state ~= "RUNNING" then
|
||||
return
|
||||
end
|
||||
|
||||
local mode = state.mode and Modes.Get(state.mode)
|
||||
if not mode then
|
||||
return
|
||||
end
|
||||
hudTitle.Text = mode.NameJP .. " " .. mode.Name
|
||||
hudTitle.TextColor3 = mode.Color
|
||||
|
||||
if state.duel then
|
||||
-- DUEL readout takes over the HUD.
|
||||
local gap = state.duel.gap or 0
|
||||
hudMain.Text = string.format("%+.0f", gap)
|
||||
hudMain.TextColor3 = gap >= 0 and NEON_GREEN or NEON_PINK
|
||||
hudSub.Text =
|
||||
string.format("vs %s %s", state.duel.opponent, gap >= 0 and "you're ahead" or "you're behind")
|
||||
gapBar.Visible = true
|
||||
gapLabel.Visible = true
|
||||
local frac = math.clamp(gap / (state.duel.winGap or 350), -1, 1)
|
||||
gapFill.Position = UDim2.new(0.5 + frac * 0.5, 0, 0, 0)
|
||||
gapFill.BackgroundColor3 = gap >= 0 and NEON_GREEN or NEON_PINK
|
||||
gapLabel.Text = string.format("hold %d studs to win", state.duel.winGap or 350)
|
||||
elseif mode.Timed then
|
||||
gapBar.Visible = false
|
||||
gapLabel.Visible = false
|
||||
hudMain.Text = fmtTime(state.lapTime)
|
||||
hudMain.TextColor3 = Color3.fromRGB(245, 245, 255)
|
||||
local best = state.bestLapTime or 0
|
||||
local parts = { string.format("SECTOR %d/4", state.sector or 1) }
|
||||
if best > 0 then
|
||||
table.insert(parts, "BEST " .. fmtTime(best))
|
||||
end
|
||||
if state.lastLapTime then
|
||||
table.insert(parts, "LAST " .. fmtTime(state.lastLapTime))
|
||||
end
|
||||
hudSub.Text = table.concat(parts, " ")
|
||||
else
|
||||
gapBar.Visible = false
|
||||
gapLabel.Visible = false
|
||||
hudMain.Text = string.format("%.2f km", state.sessionKm or 0)
|
||||
hudMain.TextColor3 = Color3.fromRGB(245, 245, 255)
|
||||
hudSub.Text = string.format("this run · %.1f km lifetime", state.totalKm or 0)
|
||||
end
|
||||
end)
|
||||
|
||||
rDuelOffer.OnClientEvent:Connect(function(info: any)
|
||||
if typeof(info) ~= "table" then
|
||||
return
|
||||
end
|
||||
duelPromptLabel.Text = string.format("%s pulled alongside.\nRun it?", tostring(info.opponent))
|
||||
duelPrompt.Visible = true
|
||||
task.delay(10, function()
|
||||
if duelPrompt.Visible then
|
||||
duelPrompt.Visible = false
|
||||
rRespondDuel:FireServer(false)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
rDuelResult.OnClientEvent:Connect(function(info: any)
|
||||
if typeof(info) ~= "table" then
|
||||
return
|
||||
end
|
||||
duelPrompt.Visible = false
|
||||
if info.expired then
|
||||
showResult("NO CONTEST", tostring(info.reason or ""), Color3.fromRGB(200, 200, 210))
|
||||
elseif info.won then
|
||||
showResult(
|
||||
"WIN",
|
||||
string.format("beat %s — %s", tostring(info.opponent), tostring(info.reason)),
|
||||
NEON_GREEN
|
||||
)
|
||||
else
|
||||
showResult("LOSS", string.format("%s got away", tostring(info.opponent)), NEON_PINK)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function ModeUI.abandon()
|
||||
rAbandonRun:FireServer()
|
||||
end
|
||||
|
||||
return ModeUI
|
||||
@@ -18,6 +18,7 @@ local Cars = require(Shared.Cars)
|
||||
local ChassisController = require(script.ChassisController)
|
||||
local CameraController = require(script.CameraController)
|
||||
local SoundController = require(script.SoundController)
|
||||
local ModeUI = require(script.ModeUI)
|
||||
|
||||
ChassisController.init()
|
||||
CameraController.init()
|
||||
@@ -272,7 +273,7 @@ local function rebuildPicker()
|
||||
end
|
||||
|
||||
-- Data sync -----------------------------------------------------------------
|
||||
rDataUpdated.OnClientEvent:Connect(function(carId: string)
|
||||
rDataUpdated.OnClientEvent:Connect(function(carId: string, _stats: any)
|
||||
currentCarId = carId
|
||||
local car = Cars.Get(carId)
|
||||
carLabel.Text = "Car: " .. (car and car.Name or carId)
|
||||
@@ -296,4 +297,13 @@ respawnBtn.MouseButton1Click:Connect(function()
|
||||
showToast("Respawning your car...")
|
||||
end)
|
||||
|
||||
print("[Wangan] Client UI ready (drive-only MVP).")
|
||||
-- Mode system UI (PA picker, run HUD, duel prompts) -------------------------
|
||||
ModeUI.build(screen)
|
||||
|
||||
local giveUpBtn = button("GIVE UP", Color3.fromRGB(255, 140, 60), 3)
|
||||
giveUpBtn.MouseButton1Click:Connect(function()
|
||||
ModeUI.abandon()
|
||||
showToast("Run abandoned — head back to Daikoku PA.")
|
||||
end)
|
||||
|
||||
print("[Wangan] Client UI ready (3 modes: cruise / duel / time attack).")
|
||||
|
||||
Reference in New Issue
Block a user