-- PlatformManager.
lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Players = game:GetService("Players")
local CollectionService = game:GetService("CollectionService")
local gameStarted = false
local recommendedSongsQueue = {}
local roundCount = 0
local nextRecommendationRound = 2
local incorrectPlayers = {}
local hasRecommendedThisMatch = {}
local playerScores = {}
local mapInitialized = false -- đặt ở đầu script PlatformManager nếu chưa có
-- Workspace Objects
local lavaPart = workspace:WaitForChild("Lava")
local initialLavaY = [Link].Y
local lobbySpawn = workspace:WaitForChild("LobbySpawn")
local safePlatformTemplate = workspace:WaitForChild("SafePlatformTemplate")
[Link] = 1
local safePlatformsFolder = workspace:WaitForChild("SafePlatforms")
[Link] = "SafePlatforms"
-- State
local bestSnapshotEver = {}
local top3Snapshot = {}
local finalLeaderboard = {}
local hoverCounter = {}
local playerPlatformMap = {} -- giữ lại map bạn đang dùng
local platformOwnerMap = {} --map phụ để quản lý Owner platform
local burning = {}
local answerTimes = {}
local playerScores = {}
local justRaised = {}
local totalAnswers = 0
local roundStartTime = 0
local floatCounter = {} -- Đếm số lần người chơi vi phạm
local graceTime = {}
-- RemoteEvents
local smoothevent = ReplicatedStorage:WaitForChild("SmoothTweenPlayer")
local sendQuestionEvent = ReplicatedStorage:WaitForChild("SendQuestion")
local answerResultEvent = ReplicatedStorage:WaitForChild("AnswerResult")
local lobbyTimerEvent = ReplicatedStorage:WaitForChild("LobbyTimer")
local tweenPlayerEvent = ReplicatedStorage:FindFirstChild("TweenPlayerUp")
local displayLeaderboardEvt = ReplicatedStorage:FindFirstChild("DisplayLeaderboard")
if not displayLeaderboardEvt then
displayLeaderboardEvt = [Link]("RemoteEvent", ReplicatedStorage)
[Link] = "DisplayLeaderboard"
end
local displayScoreboardEvt = ReplicatedStorage:FindFirstChild("DisplayScoreboard")
if not displayScoreboardEvt then
displayScoreboardEvt = [Link]("RemoteEvent", ReplicatedStorage)
[Link] = "DisplayScoreboard"
end
local showLeaderboardUIEvt = ReplicatedStorage:FindFirstChild("ShowLeaderboardUI")
if not showLeaderboardUIEvt then
showLeaderboardUIEvt = [Link]("RemoteEvent", ReplicatedStorage)
[Link] = "ShowLeaderboardUI"
end
local recommendSongsEvent = ReplicatedStorage:FindFirstChild("RecommendSongs")
local ScoreRewardEvent = ReplicatedStorage:FindFirstChild("ScoreRewardEvent")
if not ScoreRewardEvent then
ScoreRewardEvent = [Link]("RemoteEvent")
[Link] = "ScoreRewardEvent"
[Link] = ReplicatedStorage
end
if not recommendSongsEvent then
recommendSongsEvent = [Link]("RemoteEvent")
[Link] = "RecommendSongs"
[Link] = ReplicatedStorage
end
local recommendationNotificationEvent =
ReplicatedStorage:FindFirstChild("RecommendationNotification")
if not recommendationNotificationEvent then
recommendationNotificationEvent = [Link]("RemoteEvent")
[Link] = "RecommendationNotification"
[Link] = ReplicatedStorage
end
local stopMusicEvent = ReplicatedStorage:FindFirstChild("StopMusic")
if not stopMusicEvent then
stopMusicEvent = [Link]("RemoteEvent", ReplicatedStorage)
[Link] = "StopMusic"
end
local gameStateEvent = ReplicatedStorage:FindFirstChild("GameState")
if not gameStateEvent then
gameStateEvent = [Link]("RemoteEvent", ReplicatedStorage)
[Link] = "GameState"
end
local SubtractScore = ReplicatedStorage:WaitForChild("SubtractScore")
local AFKStatusChanged = ReplicatedStorage:WaitForChild("AFKStatusChanged")
[Link]:Connect(function(player, cost)
if typeof(cost) ~= "number" or cost < 0 then return end
local stats = player:FindFirstChild("leaderstats")
if not stats then return end
local score = stats:FindFirstChild("Score")
if not score then return end
if [Link] >= cost then
[Link] -= cost
end
end)
local function cloneAppearanceToDummy(player, dummy)
if not player or not dummy then return end
-- ensure dummy has a Humanoid
local humanoid = dummy:FindFirstChildOfClass("Humanoid")
if not humanoid then
humanoid = [Link]("Humanoid")
[Link] = "Humanoid"
[Link] = dummy
end
-- 1) Lưu lại các scale gốc
local originalScales = {}
for _, name in
ipairs({"BodyDepthScale","BodyHeightScale","BodyWidthScale","HeadScale"}) do
local val = humanoid:FindFirstChild(name)
if val then originalScales[name] = [Link] end
end
-- 2) Lấy và apply HumanoidDescription
local success, description = pcall(function()
return Players:GetHumanoidDescriptionFromUserId([Link])
end)
if success and description then
humanoid:ApplyDescription(description)
-- 3) Khôi phục scale gốc
for name, v in pairs(originalScales) do
local obj = humanoid:FindFirstChild(name)
if obj then [Link] = v end
end
else
warn("[Podium] Không thể lấy description của ".. [Link])
end
end
local function checkLavaKill()
local lavaBottomY = [Link].Y - [Link].Y / 2
for _, pl in ipairs(Players:GetPlayers()) do
-- Chỉ kill khi InGame và còn platform
if not pl:GetAttribute("InGame") then
continue
end
if not playerPlatformMap[[Link]] then
continue
end
if pl:GetAttribute("ImmuneLava") then
continue
end
local char = [Link]
local head = char and char:FindFirstChild("Head")
local hum = char and char:FindFirstChildOfClass("Humanoid")
local root = char and char:FindFirstChild("HumanoidRootPart")
-- 🔹 Bỏ qua nếu đang đứng trên SafePlatform của mình
local safePlat = workspace:FindFirstChild("SafePlatforms") and
[Link]:FindFirstChild([Link] .. "_SafePlatform")
if safePlat and root and ([Link] - [Link]).Magnitude < 6 then
continue
end
if head and hum and [Link] > 0 then
local headY = [Link].Y
if lavaBottomY >= headY then
[Link] = 0
warn(("[LavaKill] %s bị giết vì lava vượt qua
đầu"):format([Link]))
end
end
end
end
local function getDistanceToGround(pos)
local rayOrigin = pos
local rayDirection = [Link](0, -1000, 0) -- bắn xuống
local rayParams = [Link]()
[Link] = {workspace:FindFirstChild("SafePlatforms")}
[Link] = [Link]
local result = workspace:Raycast(rayOrigin, rayDirection, rayParams)
if result then
return (pos - [Link]).Magnitude
else
return [Link]
end
end
local MAX_DISTANCE = 15
local MAX_HOVER_HEIGHT = 30
local DETECTION_LIMIT = 2
local function checkAntiCheatPosition()
local safeFolder = workspace:FindFirstChild("SafePlatforms")
if not safeFolder then
warn("Không tìm thấy folder SafePlatforms trong workspace")
return
end
for _, player in ipairs(Players:GetPlayers()) do
local char = [Link]
if not char or not char:IsDescendantOf(workspace) then continue end
local humanoid = char:FindFirstChildOfClass("Humanoid")
local root = char:FindFirstChild("HumanoidRootPart")
if not humanoid or [Link] <= 0 or not root then continue end
if player:GetAttribute("IsAFK") then continue end
if not player:GetAttribute("InGame") then continue end
-- Nếu còn trong grace → bỏ qua và reset counter
if graceTime[[Link]] and tick() < graceTime[[Link]] then
floatCounter[[Link]] = 0
hoverCounter[[Link]] = 0
continue
end
local platform = safeFolder:FindFirstChild([Link] .. "_SafePlatform")
if not platform then
warn("[AntiCheat] Không tìm thấy SafePlatform cho " .. [Link])
continue
end
-- 1) Bay quá xa khỏi SafePlatform
local dist = ([Link] - [Link]).Magnitude
if dist > MAX_DISTANCE then
floatCounter[[Link]] = (floatCounter[[Link]] or 0) + 1
if floatCounter[[Link]] >= DETECTION_LIMIT then
warn(("[AntiCheat] %s bay xa platform: %.1f
studs"):format([Link], dist))
player:Kick("AntiCheat: Phát hiện bay ra khỏi platform.")
end
else
floatCounter[[Link]] = 0
end
-- 2) Bay cao lơ lửng (không đứng trên gì cả)
local height = getDistanceToGround([Link])
if height > MAX_HOVER_HEIGHT then
hoverCounter[[Link]] = (hoverCounter[[Link]] or 0) + 1
if hoverCounter[[Link]] >= DETECTION_LIMIT then
warn(("[AntiCheat] %s lơ lửng: %.1f studs"):format([Link],
height))
player:Kick("AntiCheat: Phát hiện lơ lửng bất thường.")
end
else
hoverCounter[[Link]] = 0
end
end
end
-- ====== CẤU HÌNH NGUỒN HỢP LỆ VÀ GIỚI HẠN ======
local SCORE_LIMITS = {
["AnswerCorrect"] = 5, -- Trả lời đúng: tối đa 5 điểm / lần
["BonusRound"] = 1000,-- Bonus round (v10, v20, v30): tối đa 1000 điểm / lần
["Donation"] = 999999 -- Donate: gần như không giới hạn
-- === A) Helper: đảm bảo tạo/nhận platform cho 1 player ===
local function ensurePlatformForPlayer(pl)
if not pl or not [Link] or pl:GetAttribute("IsAFK") then
return nil
end
-- Nếu đã có sẵn và còn parent thì dùng luôn
local existingPlat = playerPlatformMap[[Link]]
if existingPlat and [Link] then
return existingPlat
end
-- Đảm bảo folder tồn tại
if not safePlatformsFolder or not [Link] then
safePlatformsFolder = workspace:FindFirstChild("SafePlatforms")
if not safePlatformsFolder then
safePlatformsFolder = [Link]("Folder")
[Link] = "SafePlatforms"
[Link] = workspace
print("[ensurePlatformForPlayer] Tạo mới folder SafePlatforms")
end
end
-- Tính vị trí theo grid hiện có
local current = #safePlatformsFolder:GetChildren()
local spacing = [Link].X + 2
local perRow = [Link]([Link](current + 1))
local row = current // perRow
local col = current % perRow
local offsetX = (col - (perRow-1)/2) * spacing
local offsetZ = (row - (perRow-1)/2) * spacing
local pos = [Link](
[Link].X + offsetX,
[Link].Y,
[Link].Z + offsetZ
-- Tạo platform
local clone = safePlatformTemplate:Clone()
[Link] = tostring([Link]).."_SafePlatform"
[Link] = 0
[Link] = true
[Link] = true
[Link] = safePlatformsFolder
[Link] = [Link](pos)
playerPlatformMap[[Link]] = clone
print(("[ensurePlatformForPlayer] ✅ Tạo platform cho %s tại (%.1f, %.1f, %.1f)")
:format([Link], pos.X, pos.Y, pos.Z))
return clone
end
-- ====== HÀM CỘNG ĐIỂM AN TOÀN ======
local function addScore(player, amount, source)
-- 1. Kiểm tra player hợp lệ
if not player or not player:IsA("Player") then
return false
end
-- 2. Kiểm tra nguồn cộng điểm hợp lệ
local maxAllowed = SCORE_LIMITS[source]
if not maxAllowed then
warn([Link]("[ANTI-CHEAT] %s (%d) cố gắng cộng điểm từ nguồn không
hợp lệ: %s",
[Link], [Link], tostring(source)))
player:Kick("Cộng điểm từ nguồn không hợp lệ!")
return false
end
-- 3. Giới hạn số điểm / lần theo từng nguồn
if type(amount) ~= "number" or amount <= 0 or amount > maxAllowed then
warn([Link](
"[ANTI-CHEAT] %s (%d) cố gắng cộng %s điểm từ nguồn %s (quá giới hạn
%d)",
[Link], [Link], tostring(amount), source, maxAllowed
))
player:Kick("Cộng điểm quá giới hạn!")
return false
end
-- 4. Cộng điểm
local ls = player:FindFirstChild("leaderstats")
local sv = ls and ls:FindFirstChild("Score")
if sv then
[Link] = [Link] + amount
print([Link]("[addScore] %s +%d điểm (%s) → %d",
[Link], amount, source, [Link]))
return true
end
return false
end
local function isPlayerAliveAndOnPlatform(plr)
local char = [Link]
local humanoid = char and char:FindFirstChildOfClass("Humanoid")
local root = char and char:FindFirstChild("HumanoidRootPart")
if not humanoid or [Link] <= 0 then return false end
if not root or not plr:GetAttribute("InGame") then return false end
-- Kiểm tra còn SafePlatform không
local safeFolder = workspace:FindFirstChild("SafePlatforms")
local platform = safeFolder and safeFolder:FindFirstChild([Link] .. "_SafePlatform")
if not platform then return false end
-- Kiểm tra player còn trong playerPlatformMap
if not playerPlatformMap[[Link]] then return false end
return true
end
-- 🧹 Xóa hết Owner của các platform
local function clearAllPlatformOwners()
if not safePlatformsFolder then return end
for _, plat in ipairs(safePlatformsFolder:GetChildren()) do
plat:SetAttribute("Owner", nil)
end
print("[Platform] ✅ Đã clear toàn bộ Owner trong SafePlatforms")
end
-- 🎯 Gán platform cho player (nếu trống)
local function assignPlatformForPlayer(plr)
if not plr or not [Link] then return nil end
if not safePlatformsFolder then return nil end
-- Nếu player đã có platform còn tồn tại thì dùng luôn
local existing = playerPlatformMap[[Link]]
if existing and [Link] then
return existing
end
-- Tìm platform trống
for _, plat in ipairs(safePlatformsFolder:GetChildren()) do
if not plat:GetAttribute("Owner") then
plat:SetAttribute("Owner", [Link])
playerPlatformMap[[Link]] = plat
print([Link]("[Platform] Gán %s vào %s", [Link], [Link]))
return plat
end
end
warn("[Platform] Không tìm thấy platform trống cho " .. [Link])
return nil
end
-- 🔄 Trả platform khi player rời game
local function releasePlatformForPlayer(plr)
if not plr then return end
if not safePlatformsFolder then return end
local plat = playerPlatformMap[[Link]]
if plat and [Link] then
plat:SetAttribute("Owner", nil)
print([Link]("[Platform] %s rời game → trả lại %s", [Link], [Link]))
end
playerPlatformMap[[Link]] = nil
end
local function teleportCharacterToPosition(character, position)
if not character or not [Link] then
return
end
local root = character:FindFirstChild("HumanoidRootPart") or
character:WaitForChild("HumanoidRootPart", 5)
local humanoid = character:FindFirstChildOfClass("Humanoid")
local player = Players:GetPlayerFromCharacter(character)
if not (root and humanoid) then
warn("[Teleport] Không tìm thấy HRP hoặc Humanoid cho nhân vật")
return
end
-- Bật miễn lava tạm thời
if player and [Link] then
player:SetAttribute("ImmuneLava", true)
end
-- 1. Reset velocity để ngăn rơi/đẩy lệch
[Link] = [Link]
[Link] = [Link]
-- 2. Khóa vật lý tạm thời
[Link] = true
[Link] = true
-- 3. Teleport cao hơn platform 5 studs
[Link] = [Link](position + [Link](0, 5, 0))
-- 4. Đợi 1 frame vật lý
[Link](0.15)
-- 5. Mở lại vật lý
[Link] = false
[Link] = false
-- 6. Ép nhân vật đứng vững
if humanoid:GetState() ~= [Link] then
humanoid:ChangeState([Link])
end
-- 7. Tắt miễn lava sau khi teleport
if player and [Link] then
[Link](0.05, function()
if player and [Link] then
player:SetAttribute("ImmuneLava", false)
end
end)
end
print([Link]("[Teleport] ✅ %s đã được dịch chuyển đến (%.1f, %.1f, %.1f)",
player and [Link] or "???", position.X, position.Y, position.Z))
end
local function resetMap()
-- 0) Đặt lại trạng thái InGame cho tất cả người chơi
for _, player in ipairs(Players:GetPlayers()) do
player:SetAttribute("InGame", false)
end
-- 0.1) Đảm bảo folder SafePlatforms tồn tại
if not safePlatformsFolder or not [Link] then
safePlatformsFolder = workspace:FindFirstChild("SafePlatforms")
if not safePlatformsFolder then
safePlatformsFolder = [Link]("Folder")
[Link] = "SafePlatforms"
[Link] = workspace
print("[resetMap] Tạo mới folder SafePlatforms")
end
end
-- 1) Reset vị trí lava ban đầu
[Link] = [Link]([Link].X, initialLavaY, [Link].Z)
-- 2) Xóa toàn bộ platform cũ
for _, child in ipairs(safePlatformsFolder:GetChildren()) do
child:Destroy()
end
playerPlatformMap = {}
-- 3) Reset các cờ bonus
for _, plr in ipairs(Players:GetPlayers()) do
plr:SetAttribute("Got10Bonus", false)
plr:SetAttribute("Got20Bonus", false)
plr:SetAttribute("Got30Bonus", false)
end
-- 4) Tạo lại platform cho mỗi người chơi
local allPlayers = Players:GetPlayers()
local n = #allPlayers
if n == 0 then
print("[resetMap] Không có người chơi để tạo platform")
return
end
local size = [Link].X
local spacing = size + 2
local perRow = [Link]([Link](n))
local originX = [Link].X
local originZ = [Link].Z
local platformY = [Link].Y
local createdCount = 0
for i, pl in ipairs(allPlayers) do
if pl:GetAttribute("IsAFK") then
continue
end
local idx = i - 1
local row = [Link](idx / perRow)
local col = idx % perRow
local offsetX = (col - (perRow-1)/2) * spacing
local offsetZ = (row - (perRow-1)/2) * spacing
local pos = [Link](originX + offsetX, platformY, originZ + offsetZ)
local clone = safePlatformTemplate:Clone()
[Link] = tostring([Link]).."_SafePlatform"
[Link] = 0
[Link] = true
[Link] = true
[Link] = safePlatformsFolder
[Link]() -- 1 frame để Roblox cập nhật physics
[Link] = [Link](pos)
playerPlatformMap[[Link]] = clone
createdCount += 1
-- Teleport về lobby & reset stats
if [Link] then
teleportCharacterToPosition([Link], [Link] +
[Link](0,5,0))
local h = [Link]:FindFirstChildOfClass("Humanoid")
if h then
[Link] = 16
[Link] = 50
[Link] = [Link]
h:SetAttribute("NoHeal", false)
end
end
end
-- 5) Đợi một chút để đảm bảo replicate platform tới client
[Link](0.5)
-- 6) Gửi trạng thái InGame = false
gameStateEvent:FireAllClients(false)
-- 7) Reset biến chơi
roundCount = 0
totalAnswers = 0
roundStartTime = 0
recommendedSongsQueue = {}
nextRecommendationRound = 1
answerTimes = {}
incorrectPlayers = {}
hasRecommendedThisMatch = {}
justRaised = {}
print([Link]("[resetMap] Hoàn tất: %d platform mới tạo", createdCount))
end
local OnPlayerDonated = ReplicatedStorage:WaitForChild("OnPlayerDonated")
local EquipSkinEvent = ReplicatedStorage:WaitForChild("EquipSkinEvent")
local ApplySkinEvent = ReplicatedStorage:WaitForChild("ApplySkinEvent")
-- Sau chỗ: local ApplySkinEvent = ReplicatedStorage:WaitForChild("ApplySkinEvent")
[Link]:Connect(function(player)
-- 1) Nếu là người đầu tiên → reset map
if not gameStarted and not mapInitialized then
resetMap()
mapInitialized = true
end
-- 2) Ghi nhận điểm
local ls = player:FindFirstChild("leaderstats")
playerScores[[Link]] = ls and (ls:FindFirstChild("Score") or { Value = 0 }).Value or 0
-- 3) Nếu game chưa bắt đầu nhưng map đã reset → tạo platform cho người chơi mới
if not gameStarted and mapInitialized and not playerPlatformMap[[Link]] and not
player:GetAttribute("IsAFK") then
-- Đếm số platform hiện tại
local existing = #safePlatformsFolder:GetChildren()
local spacing = [Link].X + 2
local numPerRow = [Link]([Link](existing + 1))
local row = existing // numPerRow
local col = existing % numPerRow
local offsetX = (col - numPerRow // 2) * spacing
local offsetZ = (row - numPerRow // 2) * spacing
local pos = [Link](
[Link].X + offsetX,
[Link].Y,
[Link].Z + offsetZ
local clone = safePlatformTemplate:Clone()
[Link] = [Link] .. "_SafePlatform"
[Link] = 0
[Link] = true
[Link] = pos
[Link] = safePlatformsFolder
playerPlatformMap[[Link]] = clone
end
-- 4) Gửi skin
player:GetAttributeChangedSignal("EquippedSkin"):Connect(function()
local skinId = player:GetAttribute("EquippedSkin") or ""
ApplySkinEvent:FireAllClients([Link], skinId)
end)
[Link](function()
local skinId = player:GetAttribute("EquippedSkin") or ""
ApplySkinEvent:FireAllClients([Link], skinId)
end)
-- 5) Character spawn
[Link]:Connect(function(char)
local humanoid = char:WaitForChild("Humanoid")
local root = char:WaitForChild("HumanoidRootPart")
burning[humanoid] = nil
graceTime[[Link]] = tick() + 3
floatCounter[[Link]] = 0
[Link]:Connect(function()
graceTime[[Link]] = tick() + 3
floatCounter[[Link]] = 0
if player then player:SetAttribute("ImmuneLava", false) end
end)
-- 1) Teleport về lobby, reset stats
teleportCharacterToPosition(char, [Link] + [Link](0, 5, 0))
local humanoid = char:WaitForChild("Humanoid")
-- 2) Cho phép hồi máu (lobby)
humanoid:SetAttribute("NoHeal", false)
-- Bỏ hẳn listener block-health ở đây
-- 3) Khi chết → dọn platform
[Link]:Connect(function()
local p = playerPlatformMap[[Link]]
if p then p:Destroy() end
playerPlatformMap[[Link]] = nil
end)
end)
if [Link] then
teleportCharacterToPosition([Link], [Link] +
[Link](0, 5, 0))
end
end)
[Link]:Connect(function(player, skinId)
-- skinId == "" nghĩa là gỡ skin
if type(skinId) ~= "string" then return end
-- lưu vào Attribute để sau này còn load lại
player:SetAttribute("EquippedSkin", skinId)
print(("[Server] [Link] từ %s, skinId = %q"):format([Link],
skinId))
-- broadcast cho tất cả client biết player này vừa thay đổi skin
ApplySkinEvent:FireAllClients([Link], skinId)
print(("[Server] Fire ApplySkinEvent → userId=%d, skinId=%q"):format([Link],
skinId))
end)
[Link]:Connect(function(player, isAFK)
if typeof(isAFK) ~= "boolean" then return end
player:SetAttribute("IsAFK", isAFK)
-- Nếu đang ở lobby (chưa start) và vừa tắt AFK -> chuẩn bị platform ngay
if not gameStarted and not isAFK then
local plat = ensurePlatformForPlayer(player)
if plat then
print(("[AFK] %s tắt AFK → chuẩn bị sẵn platform"):format([Link]))
end
end
end)
-- Hàm lấy data hiện tại để gửi lên client
local function collectScoreboardData()
local data = {}
for _, pl in ipairs(Players:GetPlayers()) do
local ls = pl:FindFirstChild("leaderstats")
if ls then
local sv = ls:FindFirstChild("Score")
[Link](data, {
player = [Link],
score = sv and [Link] or 0
})
end
end
return data
end
-- Khi có donate thành công
[Link]:Connect(function(pl, robuxAmount)
-- tương tự handler ở [Link]: Score đã được cộng vào leaderstats
-- giờ chỉ cần cập nhật lại bảng điểm ngay lập tức
local data = collectScoreboardData()
displayScoreboardEvt:FireAllClients(data)
end)
local RoundUpdateEvent = ReplicatedStorage:FindFirstChild("RoundUpdate")
[Link]:Connect(function(player, payload)
-- 1) Kiểm tra payload
if typeof(payload) ~= "table" then return end
local songsTable = [Link]
if typeof(songsTable) ~= "table" or #songsTable == 0 then return end
if #songsTable > 3 then
warn(("[PlatformManager] %s gửi quá nhiều bài (%d), giới hạn là
3"):format([Link], #songsTable))
return
end
-- 2) Lấy leaderstats và Score
local leaderstats = player:FindFirstChild("leaderstats")
local scoreVal = leaderstats and leaderstats:FindFirstChild("Score")
if not scoreVal then return end
-- 3) Tính cost thực tế (không tin client gửi)
local function calculateCost(n)
if n == 1 then return 10 end
if n == 2 then return 30 end
if n == 3 then return 45 end
return [Link]
end
local realCost = calculateCost(#songsTable)
if [Link] < realCost then
warn(("[PlatformManager] %s không đủ điểm (%d < %d) để đề xuất!")
:format([Link], [Link], realCost))
return
end
-- 4) Chặn spam (1 lần mỗi round)
if player:GetAttribute("UsedRecommend") then
warn(("[PlatformManager] %s đã dùng Recommend trong round
này!"):format([Link]))
return
end
player:SetAttribute("UsedRecommend", true)
-- 5) Validate & đưa từng questionId vào queue
for _, entry in ipairs(songsTable) do
local qid = [Link]
if typeof(qid) == "string" then
local mod =
[Link]:FindFirstChild("MusicQuestions"):FindFirstChild(qid)
if mod and mod:IsA("ModuleScript") then
[Link](recommendedSongsQueue, qid)
else
warn(("[PlatformManager] %s gửi qid không hợp lệ:
%s"):format([Link], tostring(qid)))
end
end
end
-- 6) Trừ điểm
[Link] -= realCost
-- 7) Tính các vòng mà player này đề xuất
local count = #songsTable
local rounds = {}
local startRound = nextRecommendationRound
for i = 0, count - 1 do
[Link](rounds, startRound + i)
end
nextRecommendationRound = startRound + count
-- 8) Gửi notification cho tất cả client
recommendationNotificationEvent:FireAllClients([Link], count, rounds)
-- 9) Debug log
print([Link](
"[PlatformManager] %s đã đề xuất %d bài (cost=%d) cho vòng %s",
[Link], count, realCost, [Link](rounds, ",")
))
print("[PlatformManager] Queue size =", #recommendedSongsQueue)
end)
-- Configurations
local QUESTIONS_FOLDER = ReplicatedStorage:WaitForChild("MusicQuestions")
local TIME_LIMIT = 10
local LAVA_STEP_PER_ROUND = 3.5
local STEP_INCREMENT =1
local MAX_SAFE_STEP =9
local TWEEN_TIME = 1.5
local LOBBY_MIN_PLAYERS =1
local LOBBY_MAX_WAIT = 30
local START_COUNTDOWN_TIME = 20
local GRID_COLS =3
local GRID_ROWS =3
local GRID_SPACING_X = [Link].X + 2
local GRID_SPACING_Z = [Link].Z + 2
local ORIGIN_POS = [Link]
local DAMAGE_PER_SECOND =4
local tweenInfo = [Link](TWEEN_TIME, [Link],
[Link])
local function tweenPlatformUsingGuide(player, boost)
local plat = playerPlatformMap[[Link]]
if not plat then return end
if player and [Link] then
player:SetAttribute("ImmuneLava", true)
end
-- Tạo Part guide
local guide = [Link]("Part")
[Link] = [Link] .. "_GuidePart"
[Link] = true
[Link] = false
[Link] = 1
[Link] = [Link](1, 1, 1)
[Link] = [Link]
[Link] = workspace
-- Tạo attachments
local att0 = [Link]("Attachment", plat)
local att1 = [Link]("Attachment", guide)
-- AlignPosition
local alignPos = [Link]("AlignPosition")
alignPos.Attachment0 = att0
alignPos.Attachment1 = att1
[Link] = true
[Link] = true
[Link] = 200
[Link] = [Link]
[Link] = plat
-- AlignOrientation
local alignOri = [Link]("AlignOrientation")
alignOri.Attachment0 = att0
alignOri.Attachment1 = att1
[Link] = true
[Link] = 200
[Link] = [Link]
[Link] = plat
-- Cấu hình platform
[Link] = false
[Link] = true
[Link] = false
plat:SetNetworkOwner(nil)
-- Tween guide đi lên
local info = [Link](TWEEN_TIME, [Link],
[Link])
local tween = TweenService:Create(guide, info, {
Position = [Link] + [Link](0, boost, 0)
})
tween:Play()
-- Cleanup guide sau 3s
[Link](TWEEN_TIME + 0.05, function()
if guide and [Link] then guide:Destroy() end
-- Sau khi tween xong → khoá platform lại
if plat and [Link] then
[Link] = true
-- Xoá Align nếu không dùng nữa
local ap = plat:FindFirstChildOfClass("AlignPosition")
if ap then ap:Destroy() end
local ao = plat:FindFirstChildOfClass("AlignOrientation")
if ao then ao:Destroy() end
local atts = plat:GetChildren()
for _, obj in pairs(atts) do
if obj:IsA("Attachment") then
obj:Destroy()
end
end
end
if player and [Link] then
-- chờ thêm 0.05s để đảm bảo physics ổn định
[Link](0.05)
player:SetAttribute("ImmuneLava", false)
end
end)
end
-- LẤY CÂU HỎI NGẪU NHIÊN
local function getRandomQuestion()
local modules = QUESTIONS_FOLDER:GetChildren()
local pickedModule
if #recommendedSongsQueue > 0 then
local nextName = [Link](recommendedSongsQueue, 1)
for _, mod in ipairs(modules) do
if [Link] == nextName then
pickedModule = mod
break
end
end
end
if not pickedModule then
if #modules > 0 then
pickedModule = modules[[Link](1, #modules)]
else
error("[PlatformManager] MusicQuestion folder is empty!")
end
end
local pickedData = require(pickedModule)
-- Lấy danh sách tất cả module khác (sai)
local wrongModules = {}
for _, mod in ipairs(modules) do
if mod ~= pickedModule then
[Link](wrongModules, mod)
end
end
-- Lấy ngẫu nhiên 3 đáp án sai
local wrongChoices = {}
while #wrongChoices < 3 and #wrongModules > 0 do
local index = [Link](1, #wrongModules)
local mod = [Link](wrongModules, index)
local data = require(mod)
[Link](wrongChoices, {
Name = [Link],
ImageId = [Link] or ""
})
end
-- Gộp đáp án Correct + sai rồi trộn
local allChoices = {
{ Name = [Link], ImageId = [Link] or "" }
for _, wrong in ipairs(wrongChoices) do
[Link](allChoices, wrong)
end
-- Trộn ngẫu nhiên
local function shuffle(t)
local shuffled, used = {}, {}
while #shuffled < #t do
local i = [Link](1, #t)
if not used[i] then
[Link](shuffled, t[i])
used[i] = true
end
end
return shuffled
end
return {
songName = [Link],
soundId = [Link],
choices = shuffle(allChoices)
end
local function disableCharacterMovement(character)
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then [Link] = 0; [Link] = 0 end
end
local function roll30Bonus()
-- r đồng đều [0,1)
local r = [Link]()
-- bias về thấp: exponent >1 → tập trung vào các giá trị nhỏ
local biased = r ^ 3
-- scale lên [100,999]
local minV, maxV = 100, 999
return [Link](minV + (maxV - minV) * biased + 0.5)
end
-- Lava damage on touch
[Link]:Connect(function(hit)
local character = [Link]
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local player = humanoid and Players:GetPlayerFromCharacter(character)
if player and humanoid and not burning[humanoid] then
-- Nếu player không trong trận thì bỏ qua
if not player:GetAttribute("InGame") then return end
-- Nếu player đang được miễn damage thì không bắt đầu loop
if player:GetAttribute("ImmuneLava") then
return
end
burning[humanoid] = true
[Link](function()
while burning[humanoid] and [Link] > 0 do
-- cập nhật player từ humanoid (an toàn nếu player respawn)
local p = Players:GetPlayerFromCharacter([Link])
if not p or not p:GetAttribute("InGame") then
break
end
-- Nếu đang miễn damage thì skip lần này (đợi cho tới khi attribute
false)
if p:GetAttribute("ImmuneLava") then
-- chờ 0.2s rồi tiếp tục vòng lặp, không gây damage
[Link](0.2)
else
humanoid:TakeDamage(DAMAGE_PER_SECOND)
[Link](1)
end
end
burning[humanoid] = nil
end)
end
end)
[Link]:Connect(function(hit)
local humanoid = [Link] and [Link]:FindFirstChildOfClass("Humanoid")
if humanoid then
burning[humanoid] = nil
end
end)
-- Lobby countdown (non-blocking)
local function startLobbyCountdown(sec)
for i = sec, 1, -1 do
print("Countdown:", i)
lobbyTimerEvent:FireAllClients(i)
wait(1)
end
lobbyTimerEvent:FireAllClients(0)
end
-- Lobby waiting
local function lobbyWait()
local startTime = tick()
while tick() - startTime < LOBBY_MAX_WAIT do
if #Players:GetPlayers() >= LOBBY_MIN_PLAYERS then break end
wait(1)
end
startLobbyCountdown(START_COUNTDOWN_TIME)
end
-- Arrange platforms and start game
graceTime = graceTime or {}
local function startGame()
gameStarted = true
print("[StartGame] Bắt đầu trận")
for _, player in ipairs(Players:GetPlayers()) do
if not player:GetAttribute("IsAFK") then
-- Đảm bảo platform tồn tại cho player này
local platform = playerPlatformMap[[Link]]
if not platform or not [Link] then
platform = ensurePlatformForPlayer(player)
end
if platform and [Link] then
local hrp = [Link]:FindFirstChild("HumanoidRootPart")
or [Link]:WaitForChild("HumanoidRootPart", 3)
if hrp then
local humanoid =
[Link]:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid:ChangeState([Link])
end
-- Cho anti-cheat grace time 3s
graceTime[[Link]] = tick() + 3
teleportCharacterToPosition([Link],
[Link])
disableCharacterMovement([Link])
player:SetAttribute("InGame", true)
print(("[StartGame] ✅ Đưa %s vào
trận"):format([Link]))
else
warn(("[StartGame] ❌ %s không có HumanoidRootPart khi
teleport"):format([Link]))
end
else
warn(("[StartGame] ❌ Không có platform cho
%s"):format([Link]))
end
end
end
-- Khóa hồi máu (giữ logic cũ)
for _, player in ipairs(Players:GetPlayers()) do
local char = [Link]
if char then
local humanoid = char:FindFirstChildOfClass("Humanoid")
if humanoid and not humanoid:GetAttribute("NoHeal") then
humanoid:SetAttribute("NoHeal", true)
local lastHealth = [Link]
humanoid:GetPropertyChangedSignal("Health"):Connect(function()
if humanoid:GetAttribute("NoHeal") and [Link]
> lastHealth then
[Link] = lastHealth
else
lastHealth = [Link]
end
end)
end
end
end
-- Gửi trạng thái & scoreboard
gameStateEvent:FireAllClients(true)
local initData = {}
for userId, score in pairs(playerScores) do
local pl = Players:GetPlayerByUserId(userId)
if pl then
[Link](initData, { player = [Link], score = score })
end
end
displayScoreboardEvt:FireAllClients(initData)
end
[Link]:Connect(function(player)
local p = playerPlatformMap[[Link]]
if p then p:Destroy() end
playerPlatformMap[[Link]] = nil
playerScores[[Link]] = nil
answerTimes[[Link]] = nil
hasRecommendedThisMatch[[Link]] = nil
-- đảm bảo tắt attribute
if player and [Link] == nil then
-- không cần pcall
player:SetAttribute("ImmuneLava", false)
end
end)
local function calulateCost(numSongs)
if numSongs == 1 then return 10 end
if numSongs == 2 then return 30 end
if numSongs == 3 then return 45 end
return [Link]
end
local MIN_BOOST = 3.5
local MAX_BOOST = MAX_SAFE_STEP
-- Gắn trước khi any other code, sau khi đã declare all biến (bao gồm gameStarted,
playerPlatformMap, v.v.)
-- Anti-spam AnswerResult
local answerCooldown = {}
local answerSpamCount = {}
local SPAM_LIMIT = 5 -- số lần tối đa trong 1 giây
local COOLDOWN_TIME = 1 -- giây
[Link]:Connect(function(player, result)
local now = tick()
local uid = [Link]
-- Kiểm tra cooldown
if answerCooldown[uid] and now - answerCooldown[uid] < COOLDOWN_TIME then
answerSpamCount[uid] = (answerSpamCount[uid] or 0) + 1
if answerSpamCount[uid] > SPAM_LIMIT then
warn("[AntiCheat] " .. [Link] .. " spam AnswerResult, kick!")
player:Kick("Spam AnswerResult quá mức")
return
end
return -- Bỏ qua do đang cooldown
else
answerCooldown[uid] = now
answerSpamCount[uid] = 0
end
-- Chỉ xử lý nếu player thực sự đang trong game
if not player:GetAttribute("InGame") then
return
end
-- Logic xử lý kết quả câu trả lời (như bạn đang có)
if result == "Correct" then
local timeTaken = tick() - roundStartTime
answerTimes[uid] = timeTaken
incorrectPlayers[uid] = nil
local ls = player:FindFirstChild("leaderstats")
local sv = ls and ls:FindFirstChild("Score")
addScore(player, 1, "AnswerCorrect")
local total = ls and ls:FindFirstChild("Correct")
if total then
[Link] = [Link] + 1
end
local ratio = [Link]((TIME_LIMIT - timeTaken) / TIME_LIMIT, 0, 1)
local boost = MIN_BOOST + (MAX_BOOST - MIN_BOOST) * ratio
tweenPlatformUsingGuide(player, boost)
else
incorrectPlayers[uid] = true
end
totalAnswers += 1
local aliveCount = 0
for _, pl in ipairs(Players:GetPlayers()) do
local h = [Link] and [Link]:FindFirstChildOfClass("Humanoid")
if h and [Link] > 0 then
aliveCount += 1
end
end
if totalAnswers >= aliveCount then
stopMusicEvent:FireAllClients()
end
end)
local function applyPlatformHeightsAndLeaderboard()
local ranking = {}
-- Chỉ lấy người có thời gian trả lời (tức là đã trả lời đúng)
for userIdStr, timeTaken in pairs(answerTimes) do
local userId = tonumber(userIdStr)
local pl = Players:GetPlayerByUserId(userId)
local plat = playerPlatformMap[userId]
if pl and plat and plat:IsDescendantOf(workspace) then
local y = [Link].Y
[Link](ranking, { player = pl, time = timeTaken, y = y })
-- Cập nhật độ cao platform cao nhất (cho podium)
local prevMax = bestSnapshotEver[userId]
if not prevMax or y > prevMax then
bestSnapshotEver[userId] = y
end
end
end
-- ✅ Lưu top3Snapshot theo độ cao platform (phục vụ podium cuối trận)
local rankingByHeight = [Link](ranking) -- tạo bản sao không ảnh hưởng sắp xếp
khác
[Link](rankingByHeight, function(a, b)
return a.y > b.y
end)
top3Snapshot = {}
for i = 1, [Link](3, #rankingByHeight) do
top3Snapshot[i] = rankingByHeight[i]
end
-- ✅ Hiển thị bảng xếp hạng theo thời gian trả lời (ai nhanh nhất đứng đầu)
[Link](ranking, function(a, b)
return [Link] < [Link]
end)
local clientLb = {}
local maxDisplay = 15 -- Giới hạn hiển thị top 15
for i = 1, [Link](maxDisplay, #ranking) do
local e = ranking[i]
[Link](clientLb, {
position = i,
player = [Link],
time = [Link]("%.2f", [Link]),
})
end
displayLeaderboardEvt:FireAllClients(clientLb)
end
local function noOneOnPlatform()
return next(playerPlatformMap) == nil
end
while true do
-- A) Đợi ở lobby
lobbyWait()
-- B) Reset map và tạo platform
resetMap()
-- B1) Chờ tối đa 3 giây để platform tạo xong cho tất cả player không AFK
local ready = false
for i = 1, 30 do -- 30 * 0.1s = 3 giây
local allReady = true
for _, player in ipairs(Players:GetPlayers()) do
if not player:GetAttribute("IsAFK") and not
playerPlatformMap[[Link]] then
allReady = false
break
end
end
if allReady then
ready = true
break
end
[Link](0.1)
end
if not ready then
warn("[PlatformManager] Không thể tạo đủ platform trước khi startGame")
end
-- C) Báo round 0 và bắt đầu game
RoundUpdateEvent:FireAllClients(0)
startGame()
[Link](1)
local gameOver = false
-- ==== D) Vòng chơi chính ====
while not gameOver do
-- 1) Tăng round & update UI
roundCount += 1
for _, plr in ipairs(Players:GetPlayers()) do
plr:SetAttribute("UsedRecommend", false)
end
RoundUpdateEvent:FireAllClients(roundCount)
totalAnswers = 0
-- 1.1) Thưởng BonusRound
if roundCount == 11 then
for _, plr in ipairs(Players:GetPlayers()) do
if isPlayerAliveAndOnPlatform(plr) and not
plr:GetAttribute("Got10Bonus") then
plr:SetAttribute("Got10Bonus", true)
addScore(plr, 25, "BonusRound")
ScoreRewardEvent:FireClient(plr, 25)
end
end
elseif roundCount == 21 then
for _, plr in ipairs(Players:GetPlayers()) do
if isPlayerAliveAndOnPlatform(plr) and not
plr:GetAttribute("Got20Bonus") then
plr:SetAttribute("Got20Bonus", true)
addScore(plr, 50, "BonusRound")
ScoreRewardEvent:FireClient(plr, 50)
end
end
elseif roundCount == 31 then
for _, plr in ipairs(Players:GetPlayers()) do
if isPlayerAliveAndOnPlatform(plr) and not
plr:GetAttribute("Got30Bonus") then
plr:SetAttribute("Got30Bonus", true)
local bonus = roll30Bonus()
addScore(plr, bonus, "BonusRound")
ScoreRewardEvent:FireClient(plr, bonus)
end
end
gameOver = true
end
-- 2) Lava nâng cao
local currentY = [Link].Y
local stepBase = LAVA_STEP_PER_ROUND + STEP_INCREMENT *
[Link](roundCount / 5)
local targetY
if roundCount < 10 then
targetY = currentY + stepBase
elseif roundCount == 10 then
local minY = [Link]
for _, plat in pairs(playerPlatformMap) do
minY = [Link](minY, [Link].Y)
end
local mid = (minY - currentY) * 0.5
targetY = currentY + [Link](mid, stepBase, stepBase * 2)
elseif roundCount % 5 == 0 then
local minY = [Link]
for _, plat in pairs(playerPlatformMap) do
minY = [Link](minY, [Link].Y)
end
targetY = [Link](currentY + stepBase, minY - 5)
else
targetY = currentY + stepBase
end
local lavaStartY = [Link].Y
local tweenStartTime = tick()
TweenService:Create(lavaPart, tweenInfo, {
Position = [Link]([Link].X, targetY, [Link].Z)
}):Play()
[Link](TWEEN_TIME)
if tick() - tweenStartTime > 20 and [Link]([Link].Y - targetY) > 1
then
warn("[PlatformManager] Lava tween bị kẹt! Reset vòng để tránh đóng
băng.")
resetMap()
break
end
checkLavaKill()
roundStartTime = tick()
-- 3) Gửi câu hỏi + phát nhạc
local question = getRandomQuestion()
for _, pl in ipairs(Players:GetPlayers()) do
sendQuestionEvent:FireClient(pl, question)
end
local oldSound = workspace:FindFirstChild("CurrentSong")
if oldSound then oldSound:Destroy() end
local newSound = [Link]("Sound")
[Link] = "CurrentSong"
[Link] = [Link]
[Link] = 1
[Link] = false
[Link] = workspace
newSound:Play()
-- 4) Chờ trả lời
local elapsed = 0
while elapsed < TIME_LIMIT do
if noOneOnPlatform() then
gameOver = true
break
end
[Link](1)
elapsed += 1
end
-- 5) Show leaderboard
applyPlatformHeightsAndLeaderboard()
[Link](2)
answerTimes = {}
incorrectPlayers = {}
checkAntiCheatPosition()
-- 6) Delay rồi kiểm tra tiếp
[Link](5)
if noOneOnPlatform() then
gameOver = true
end
end -- end while not gameOver
-- ==== E) Trận kết thúc: chờ 5s để client xem kết quả rồi sang lobby mới ====
for _, plr in ipairs(Players:GetPlayers()) do
local char = [Link]
local hum = char and char:FindFirstChildOfClass("Humanoid")
local root = char and char:FindFirstChild("HumanoidRootPart")
-- Đặt trạng thái InGame về false NGAY để tránh các script khác chặn
plr:SetAttribute("InGame", false)
if hum and root then
-- Nếu còn sống thì hồi máu
if [Link] > 0 then
[Link] = [Link]
end
-- Gỡ mọi ràng buộc vật lý còn sót
for _, obj in ipairs(root:GetChildren()) do
if obj:IsA("AlignPosition") or obj:IsA("AlignOrientation")
or obj:IsA("BodyGyro") or obj:IsA("BodyPosition")
or obj:IsA("BodyVelocity") or obj:IsA("VectorForce")
or obj:IsA("LinearVelocity") or obj:IsA("AngularVelocity")
then
obj:Destroy()
end
end
-- Reset trạng thái vật lý
[Link] = false
[Link] = false
[Link] = false
[Link] = true
-- Reset chỉ số di chuyển mặc định
[Link] = 16
[Link] = 50
[Link] = 7.2 -- mặc định Roblox
-- Đặt lại vị trí
[Link] = [Link]([Link] + [Link](0, 10, 0))
-- Gỡ NoHeal
hum:SetAttribute("NoHeal", false)
end
end
-- 1) Tính top 3 người cao nhất toàn trận dựa trên bestSnapshotEver
print("[Podium] Bắt đầu vinh danh top 3")
local bestList = {}
for userId, y in pairs(bestSnapshotEver) do
local pl = Players:GetPlayerByUserId(userId)
if pl then
[Link](bestList, { player = pl, y = y })
end
end
-- 2) Sort giảm dần
[Link](bestList, function(a,b) return a.y > b.y end)
-- 3) Lấy top 3
local podiumEntries = {}
for i = 1, [Link](3, #bestList) do
podiumEntries[i] = bestList[i]
end
-- 4) Chuẩn bị danh sách dummy
local podiumDummies = {
workspace:FindFirstChild("Podium1"),
workspace:FindFirstChild("Podium2"),
workspace:FindFirstChild("Podium3"),
-- 4.1) Reset toàn bộ trang phục cũ trên dummy
for _, dummy in ipairs(podiumDummies) do
if dummy then
for _, child in ipairs(dummy:GetChildren()) do
if child:IsA("Accessory")
or child:IsA("CharacterMesh")
or child:IsA("Shirt")
or child:IsA("Pants")
or child:IsA("BodyColors")
or child:IsA("Decal") then
child:Destroy()
end
end
end
end
-- 4.2) Clone appearance cho top 3
for i, entry in ipairs(podiumEntries) do
local pl = [Link]
local dummy = podiumDummies[i]
if pl and dummy then
print([Link]("[Podium] Gán model %s vào %s (height=%.2f)",
[Link], [Link], entry.y))
cloneAppearanceToDummy(pl, dummy)
else
warn([Link]("[Podium] Bỏ qua vị trí %d do thiếu dữ liệu", i))
end
end
-- Cuối cùng reset các biến của trận
answerTimes = {}
incorrectPlayers = {}
bestSnapshotEver = {}
playerPlatformMap = {}
mapInitialized = false
wait(5)
end -- end while true