My map is made mostly out of Terrain.
I placed the Terrain high up from the baseplate (like 500 studs), so I can use the baseplate as a testing area.
Occasionally, in a live server, the Terrain just teleports to the bottom (right on top of the baseplate) without warning.
This is really annoying, and I don’t know what is causing this.
My code only uses Terrain:FillBall(), Terrain:ReadVoxels(), Terrain:GetMaterialColor(), Terrain:SetMaterialColor().
(GetMaterial and SetMaterial is rarely used)
As you can see, only the Terrain moved to the bottom and the map Parts did not
Hey @Ryan_MAN100 , I want to take a closer look at this. Help me rule out a couple of things if you can:
Is this visual only?
i.e if a player were to spawn, do they fall down, or is terrain collision still happening at the appropriate location.
Does it persist?
i.e it doesn’t magically snap back after some time or in response to a change in the world
Confirm you got a repro in a Roblox Studio play session (your latest reply suggests you did). That should make things easier to reproduce on my end. If the place file is something you’re comfortable sharing, let me know.
I’m sorry for the late reply, things have been busy and I haven’t checked too often
Yes, the collision does update and the players fall down too.
I don’t believe that it magically snap back after some time, but I can’t verify this since everyone eventually leaves after a few minutes because the game is basically unplayable.
Do you want the game files of my game?
I would willingly give it to you if it will help with figuring out the problem!
Oh wait, a player sent me a video. (right after I sent my previous reply)
It appears as though the terrain’s collision stays until the player dies. And afterwards, the collision updates. The raycasts from the player grappling on the terrain is working, so raycasts are hitting the updated terrain collision correctly.
Terrain that I had placed under the map also seems to have moved down (or deleted) too.
Players get teleported outside of the shelter (the safe where everyone are at the end of the day/cycle) BEFORE the rain started too. The players did not teleport to the same location though (you can see another player right before the recording player dies; they seem to also be standing on thin air).
(the player started recording after they got teleported out of the shelter; they are also taking damage because they left the shelter safe zone)
Terrain appears to also not be directly on top of the baseplate, but also below it
(this happens pretty often, see how the player reacts “Not again”)
this can happen to servers as young as 1 hours old to 20 hours old
I just had one happen back to back, so basically all of my active players got deleted from shutting down these 2 servers
more info:
The red circled parts are terrain (it looks like half of the terrain is above the baseplate while other half is below):
apparently, it can happen either at the start of the rain or at the end of the rain (rain is when the game is clearing out creatures and items and spawning new items, so it is generally when it is super laggy)
Here is our conversation
Some players did not get teleported out and stayed inside the safe zone (this was when it happened after the rain ended; the video from my previous replay was from when the rain started; not sure if this means anything):
Could you check whether this place was ever saved or published under Terrain Early Access, including by Team Create collaborators? Recent EAP reports mention unexpected terrain loss, though the symptoms differ
Sorry, I missed out on your replies too. The finding with collision is a useful hint.
In the meantime do double check on the Early Access @NaNesser pointed out. That may have changed aspects of how terrain is serialized, which is an immediate red flag with issues like yours.
You mentioned you are using the Terrain APIs to create this terrain, maybe you can share those scripts instead? e.g is the terrain created only once at startup, do you read/write to it as the game evolves, what is the rain script doing, and which scripts are running on the Server vs Client. If it is easier for you, go ahead and share the place file and I’ll take a look.
P.S: The attached video is no longer available, try resharing it if it is still around.
no, I don’t think I am using the Terrain Early Access thing. I never heard of it before. And this problem existed before that even existed. (I don’t remember when it started)
I built the map (Terrain) in Studios using the Terrain Editor, not with a script.
Here is the video again (Youtube):
Here are the lines where I did stuff with Terrain:
-- Destroy Terrain
if destroyTerrain and not TerrainHelper.IsInWater(position, radius * 2) then
local hitSize = math.min(radius / 5, 100)
workspace.Terrain:FillBall(position, hitSize, Enum.Material.Air)
end
local function isInWater(position : Vector3, size : Vector3?)
if not size then size = Vector3.new(2, 2, 2) end
local min = position - size / 2
local max = position + size / 2
local region = Region3.new(min, max):ExpandToGrid(4)
local material = workspace.Terrain:ReadVoxels(region, 4)[1][1][1]
return material == Enum.Material.Water
end
local function waterMultiplier(position : Vector3, size : Vector3)
if not size then size = Vector3.new(2, 2, 2) end
local min = position - size / 2
local max = position + size / 2
local region = Region3.new(min, max):ExpandToGrid(4)
local material = workspace.Terrain:ReadVoxels(region, 4)[1][1][1]
if material == Enum.Material.Water then
return 0.8
end
return 0.99
end
VisualsSetting.RealisticGrassChanged = function(enabled : boolean)
if enabled then
if workspace.Terrain:GetMaterialColor(Enum.Material.Grass) == Color3.fromRGB(116, 174, 47) then
workspace.Terrain:SetMaterialColor(Enum.Material.Grass, Color3.fromRGB(111, 126, 62))
end
else
if workspace.Terrain:GetMaterialColor(Enum.Material.Grass) == Color3.fromRGB(111, 126, 62) then
workspace.Terrain:SetMaterialColor(Enum.Material.Grass, Color3.fromRGB(116, 174, 47))
end
end
end
ApplyStyling.OnClientEvent:Connect(function(region : string)
local module : ModuleScript = RegionData:FindFirstChild(region)
if not module then
warn("No region styling for " .. region)
return
end
local Style = require(module)
-- Apply Terrain Color
for material : Enum.Material, color : Color3 in Style.TerrainColor do
if material == Enum.Material.Water then
Terrain.WaterColor = color
continue
end
Terrain:SetMaterialColor(material, color)
end
-- Apply Atmosphere Properties
TweenService:Create(Lighting.Atmosphere, tweenInfo, Style.Atmosphere):Play()
end)
I am not sure if this is worth noting, but I used this plugin to import terrain from another place:
The client doesn’t do anything with Terrain, only the Server does.
Here is the Time module that handles rain and stuff:
-- Services
local Lighting = game:GetService("Lighting")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
-- Modules
local Helper = require(ReplicatedStorage.Modules.Helper)
local WebhookHandler = require(ServerStorage.Modules.WebhookHandler)
local PlayerHandler = require(ServerStorage.Modules.PlayerHandler)
local CharacterHandler = require(ServerStorage.Modules.CharacterHandler)
local DataManager = require(ServerScriptService.Data.DataManager)
local KicksHandler = require(ServerScriptService.Modules.KicksHandler)
-- Remotes
local HighlightShelter = ReplicatedStorage.Remotes.Visuals.HighlightShelter
local SetClock = ReplicatedStorage.Remotes.Time.SetClock
local SetClockParams = require(SetClock.Params)
local SetRain = ReplicatedStorage.Remotes.Time.SetRain
local SetRainParams = require(SetRain.Params)
local Received = ReplicatedStorage.Remotes.Referral.Received
-- Bindables
local NewCycle = ServerStorage.Bindables.Time.NewCycle
local RainStarted = ServerStorage.Bindables.Time.RainStarted
local RespawnItems = ServerStorage.Bindables.ItemSpawner.Respawn
local ShelterGate = ServerStorage.Bindables.ShelterGate
local ShelterGateEnum = require(ShelterGate.Enum)
local StartCreatureSpawning = ServerStorage.Bindables.CreatureSpawner.Start
local StopCreatureSpawning = ServerStorage.Bindables.CreatureSpawner.Stop
-- Fun Panel
local Transition = ServerStorage.Bindables.Fun.Time.Transition
local SetDayLengthRange = ServerStorage.Bindables.Fun.Time.SetDayLengthRange
local SetNightLengthRange = ServerStorage.Bindables.Fun.Time.SetNightLengthRange
-- Data
local TimeData = require(ReplicatedStorage.Modules.Data.TimeData)
local TimeEnum = require(ReplicatedStorage.Modules.Data.TimeData.Enum)
local Assets = require(ReplicatedStorage.Modules.Assets)
local Time = {}
Time.TimeEvents = {}
Time.CurrentTimeEvents = {}
-- Data
Time.DayLengthRange = TimeData.DayLengthRange
Time.NightLengthRange = TimeData.NightLengthRange
-- Variables
Time.CurrentCycle = 0
Time.CurrentTime = TimeData.DaytimeRange.Y
Time.CurrentState = TimeEnum.State.Night
Time.StateDuration = 3
Time.TimeRate = TimeData.NighttimeLength / Time.StateDuration
-- Fun Panel
Time.FunPanel = {
Transition = false,
}
-- Debounce
Time.LastClockTimeUpdate = time()
--[[
Initialize the time system
- Setup tick
- Setup fun panel
- Setup player joining
]]
Time.Init = function()
-- Holds Functions that run at specific times
local timeEvents = {
StartRain = {
RemainingTime = TimeData.RainStartOffset,
State = TimeEnum.State.Day,
Event = Time.StartRain,
},
StopRain = {
State = TimeEnum.State.Day,
Event = Time.StopRain,
},
StartDangerousRain = {
State = TimeEnum.State.Night,
Event = Time.StartDangerousRain,
},
RespawnItems = {
Time = 0,
Event = Time.RespawnItems,
LastFired = -1, -- so items respawn at the start of the round
},
BatflyClearing = {
Time = 0,
Event = Time.BatflyClearing,
},
LizardClearing = {
Time = 0,
Event = Time.LizardClearing,
},
StartCreatureSpawning = {
State = TimeEnum.State.Day,
Event = Time.StartCreatureSpawning,
},
StopCreatureSpawning = {
RemainingTime = 10,
State = TimeEnum.State.Day,
Event = Time.StopCreatureSpawning,
},
OpenShelterGates = {
State = TimeEnum.State.Day,
Event = Time.OpenShelterGates,
},
CloseShelterGates = {
State = TimeEnum.State.Night,
Event = Time.CloseShelterGates,
}
}
-- Format Time Events
for eventName,event : TimeEvent in timeEvents do
if not event.Event then
warn("Event is missing from time event: ", event)
timeEvents[eventName] = nil
continue
end
if not event.LastFired then
event.LastFired = 0
end
end
-- Set Time Events
Time.TimeEvents = timeEvents
Time.CurrentTimeEvents = table.clone(timeEvents)
-- Setup workspace attributes
workspace:SetAttribute("ServerCycleCount", 0)
workspace:SetAttribute("TimeState", Time.CurrentState)
workspace:SetAttribute("StateDuration", Time.StateDuration)
workspace:SetAttribute("DayLengthRange", Time.DayLengthRange)
workspace:SetAttribute("NightLengthRange", Time.NightLengthRange)
-- Setup Fun Panel
Transition.Event:Connect(function()
Time.FunPanel.Transition = true
end)
SetDayLengthRange.Event:Connect(function(min : number, max : number)
Time.DayLengthRange = Vector2.new(min, max)
workspace:SetAttribute("DayLengthRange", Time.DayLengthRange)
timeEvents.StartRain.RemainingTime = math.min(TimeData.RainStartOffset, Time.DayLengthRange.X - 1)
end)
SetNightLengthRange.Event:Connect(function(min : number, max : number)
Time.NightLengthRange = Vector2.new(min, max)
workspace:SetAttribute("NightLengthRange", Time.NightLengthRange)
end)
-- Setup Tick
RunService.Heartbeat:Connect(function(deltaTime)
Time.Tick(deltaTime)
end)
-- Player joining handling
Players.PlayerAdded:Connect(function(player)
-- Set Clock on join (handle late joiners)
local clockParams = SetClockParams.new()
clockParams.CurrentState = Time.CurrentState
clockParams.StateDuration = Time.StateDuration
clockParams.Progress = Time.GetTimeProgressInCurrentState()
SetClock:FireClient(player, clockParams)
local rainParams = SetRainParams.new()
rainParams.Enabled = workspace:GetAttribute("Raining")
if Time.CurrentState == TimeEnum.State.Night then
-- if night, instantly enable
rainParams.Dangerous = true
-- Note: No need to set duration because it defaults to 0 and the thing ignores it anyways because it is dangerous rain
else
-- if day, set duration to the remaining time left
rainParams.Duration = Time.GetRemainingTimeInCurrentState()
end
SetRain:FireClient(player, rainParams)
end)
-- Exploit prevention
SetClock.OnServerEvent:Connect(function(player : Player)
KicksHandler.Kick(player, KicksHandler.Enum.KickCode.SetClockFiredFromClient, KicksHandler.Enum.Reason.TSC)
end)
SetRain.OnServerEvent:Connect(function(player : Player)
KicksHandler.Kick(player, KicksHandler.Enum.KickCode.StartRainFiredFromClient, KicksHandler.Enum.Reason.TSR)
end)
HighlightShelter.OnServerEvent:Connect(function(player)
KicksHandler.Kick(player, "HSFFC", "Highlight shelter fired from client")
end)
Received.OnServerEvent:Connect(function(player)
KicksHandler.Kick(player, "RCE", "Received Referral fired from client")
end)
end
--[[
Transition to the next state
]]
Time.TransitionToNextState = function(setCurrentTime : boolean?)
-- Get new state and the state's settings
local newState = Time.CurrentState == TimeEnum.State.Day and TimeEnum.State.Night or TimeEnum.State.Day
local stateDuration = Time.GetNewStateDuration(newState)
local newTimeRate = Time.GetTimeRate(newState, stateDuration)
-- Update state
Time.CurrentState = newState
Time.StateDuration = stateDuration
Time.TimeRate = newTimeRate
-- Set Workspace Attributes
workspace:SetAttribute("TimeState", newState)
workspace:SetAttribute("StateDuration", stateDuration)
print("Transitioned to", newState, "state. State duration:", stateDuration, "seconds. Time rate:", newTimeRate, "hours per second.")
-- Set Current Time to be the start of the next state
if setCurrentTime then
if newState == TimeEnum.State.Day then
Time.CurrentTime = TimeData.DaytimeRange.X
else
Time.CurrentTime = TimeData.DaytimeRange.Y
end
end
-- Set Clock on Clients
local params = SetClockParams.new()
params.CurrentState = newState
params.StateDuration = stateDuration
params.Progress = 0
SetClock:FireAllClients(params)
-- Handle New Cycle
if newState == TimeEnum.State.Day then
-- Increment Server Cycle Count
Time.CurrentCycle += 1
workspace:SetAttribute("ServerCycleCount", Time.CurrentCycle)
WebhookHandler.AppendEventMessage(`Cycle {Time.CurrentCycle} started! Length: \`{stateDuration}\` seconds`)
-- Fire Events
NewCycle:Fire()
-- Unhighlight Shelters
HighlightShelter:FireAllClients(false)
-- Update Player Stats
Time.UpdatePlayers()
workspace:SetAttribute("Raining", false)
end
-- Handle rain
if newState == TimeEnum.State.Night then
-- Fire Events
RainStarted:Fire()
end
end
--[[
Update the time based on the current state (called every frame)
> deltaTime - the time since the last update
]]
Time.Tick = function(deltaTime : number)
-- Store previous time
local previousTime = Time.CurrentTime
-- Calculate current time
local currentTime = (previousTime + (Time.TimeRate * deltaTime) )
-- Check if we've looped back to the start (why not %?, because I need to know when it loop)
local looped = false
if currentTime > 24 then
currentTime -= 24
looped = true
end
-- Skip current state
if Time.FunPanel.Transition then
if Time.CurrentState == TimeEnum.State.Day then
-- ez, set to start of day
currentTime = TimeData.DaytimeRange.Y
elseif Time.CurrentState == TimeEnum.State.Night then
-- go to the end of the Time Events list
currentTime = 24
-- fire necessary time events (pretty much all of them)
Time.HandleTimeEvents(previousTime, currentTime)
-- reset current time events
Time.CurrentTimeEvents = table.clone(Time.TimeEvents)
-- set time to start of day
currentTime = TimeData.DaytimeRange.X
-- prevent resetting (just in case)
looped = false
end
Time.FunPanel.Transition = false
end
-- Make sure time is capped within their state range (so the Time Events fire accurately)
if Time.CurrentState == TimeEnum.State.Day then
currentTime = math.clamp(currentTime, TimeData.DaytimeRange.X, TimeData.DaytimeRange.Y)
elseif Time.CurrentState == TimeEnum.State.Night then
if currentTime > TimeData.DaytimeRange.X and currentTime < TimeData.DaytimeRange.Y then
currentTime = TimeData.DaytimeRange.X
end
end
-- Set current time
Time.CurrentTime = currentTime
-- Update Clock Time
if time() - Time.LastClockTimeUpdate > 1 then
Lighting.ClockTime = currentTime
Time.LastClockTimeUpdate = time()
end
--------------------------------------------------
-- Handle Time Events
Time.HandleTimeEvents(previousTime, currentTime)
--------------------------------------------------
-- Reset Time Events
if looped then
Time.CurrentTimeEvents = table.clone(Time.TimeEvents)
end
--------------------------------------------------
-- Transition to next state if needed
if Time.GetStateFromCurrentTime(Time.CurrentTime) ~= Time.CurrentState then
Time.TransitionToNextState()
end
end
Time.HandleTimeEvents = function(previousTime : number, currentTime : number)
-- Cache values
local currentState = Time.CurrentState
-- Store Remaining Time (better to calculate once at the start because it won't change)
local remainingTime = Time.GetRemainingTimeInCurrentState()
--print("Remaining Time:", remainingTime, "Progress:", Time.GetProgressOfCurrentState() * 100, "%")
-- Fire Events
for eventName, event : TimeEvent in Time.CurrentTimeEvents do
-- Check if already fired
if event.LastFired >= Time.CurrentCycle then continue end
-- Check if correct state
if event.State and event.State ~= currentState then continue end
-- Check if Remaining Time is passed
if event.RemainingTime and remainingTime > event.RemainingTime then continue end
-- Check if Time is passed (previous time is before event time)
if event.Time then
local passedEvent = false
if previousTime <= currentTime then
-- Normal day progression (e.g., 14.5 -> 15.2)
passedEvent = (previousTime < event.Time and currentTime >= event.Time)
else
-- Midnight wrap-around (e.g., 23.8 -> 0.1)
passedEvent = (previousTime < event.Time or currentTime >= event.Time)
end
if not passedEvent then
continue
end
end
-- Fire event
event.Event()
event.LastFired = Time.CurrentCycle
-- Remove event
Time.CurrentTimeEvents[eventName] = nil
end
end
----------------------------------------------------------------------------------------------------
-- Time Event Functions
--[[
Add a time event to the TimeEvents table
> eventName - name of the event (must be unique; if not, it will override the slot)
> eventData - the event data
> ignoreCurrentCycle (optional) - if true, the event will ignore the current cycle
]]
Time.AddTimeEvent = function(eventName : string, eventData : TimeEvent, ignoreCurrentCycle : boolean?)
if ignoreCurrentCycle ~= nil then
if ignoreCurrentCycle then
eventData.LastFired = Time.CurrentCycle
else
eventData.LastFired = 0
end
end
if not eventData.LastFired then
eventData.LastFired = 0
end
-- formatting
if eventData.Time then
eventData.Time = eventData.Time % 24
end
if eventData.RemainingTime then
eventData.RemainingTime = math.max(eventData.RemainingTime, 0)
end
if eventData.State then
if not table.find(TimeEnum.State, eventData.State) then
warn("Invalid State:", eventData.State)
return
end
end
Time.TimeEvents[eventName] = eventData
Time.CurrentTimeEvents[eventName] = eventData
end
--[[
Remove a time event from the TimeEvents table
]]
Time.RemoveTimeEvent = function(eventName : string)
Time.TimeEvents[eventName] = nil
Time.CurrentTimeEvents[eventName] = nil
end
Time.StartRain = function()
print("Start Rain", Time.CurrentTime, Time.CurrentCycle)
workspace:SetAttribute("Raining", true)
-- Enable Rain
local params = SetRainParams.new()
params.Enabled = true
params.Duration = TimeData.RainStartOffset
SetRain:FireAllClients(params)
-- Highlight Shelters
HighlightShelter:FireAllClients(true)
end
Time.StartDangerousRain = function()
print("Start Dangerous Rain", Time.CurrentTime, Time.CurrentCycle)
-- Start Dangerous Rain
local params = SetRainParams.new()
params.Enabled = true
params.Dangerous = true
SetRain:FireAllClients(params)
-- Start Damaging & Starving
for _,player in Players:GetPlayers() do
CharacterHandler.RainCheck(player, Time.StateDuration)
CharacterHandler.FoodCheck(player, Time.StateDuration)
end
end
Time.StopRain = function()
print("Stop Rain", Time.CurrentTime, Time.CurrentCycle)
workspace:SetAttribute("Raining", false)
-- Disable Rain
local params = SetRainParams.new()
params.Enabled = false
params.Duration = TimeData.RainEndOffset
SetRain:FireAllClients(params)
end
Time.RespawnItems = function()
print("Respawn Items", Time.CurrentTime, Time.CurrentCycle)
RespawnItems:Fire()
end
-- TODO: I should get rid of this soon
Time.BatflyClearing = function()
print("Batfly Clearing", Time.CurrentTime, Time.CurrentCycle)
workspace.Creatures.Batflies:ClearAllChildren()
end
Time.LizardClearing = function()
print("Lizard Clearing", Time.CurrentTime, Time.CurrentCycle)
workspace.Creatures.Lizards:ClearAllChildren()
end
Time.StartCreatureSpawning = function()
print("Start Creature Spawning", Time.CurrentTime, Time.CurrentCycle)
StartCreatureSpawning:Fire()
end
Time.StopCreatureSpawning = function()
print("Stop Creature Spawning", Time.CurrentTime, Time.CurrentCycle)
StopCreatureSpawning:Fire()
end
Time.OpenShelterGates = function()
print("Open Shelter Gates", Time.CurrentTime, Time.CurrentCycle)
ShelterGate:Fire(ShelterGateEnum.State.Opened)
end
Time.CloseShelterGates = function()
print("Close Shelter Gates", Time.CurrentTime, Time.CurrentCycle)
ShelterGate:Fire(ShelterGateEnum.State.Closed)
end
----------------------------------------------------------------------------------------------------
-- Helper Functions
--[[
Get the progress of the current state (kinda like percentage but in decimal)
< Returns progress (0 - 1)
]]
Time.GetProgressOfCurrentState = function()
local state = Time.CurrentState
local currentTime = Time.CurrentTime
local stateDuration = Time.StateDuration
if state == TimeEnum.State.Day then
return ( (currentTime - TimeData.DaytimeRange.X) / TimeData.DaytimeLength)
elseif state == TimeEnum.State.Night then
if currentTime >= TimeData.DaytimeRange.Y then
return ( (currentTime - TimeData.DaytimeRange.Y) / TimeData.NighttimeLength )
elseif currentTime >= 0 then
local lastSectionLength = 24 - TimeData.DaytimeRange.Y -- time from TimeData.DaytimeRange.Y to 24
return ( (currentTime + lastSectionLength ) / TimeData.NighttimeLength )
end
end
end
--[[
Get the progress of the current state in seconds
< Returns progress in seconds
]]
Time.GetTimeProgressInCurrentState = function()
return Time.StateDuration * Time.GetProgressOfCurrentState()
end
--[[
Get the remaining time in the current state
< Returns remaining time in seconds
]]
Time.GetRemainingTimeInCurrentState = function()
return Time.StateDuration * (1 - Time.GetProgressOfCurrentState())
end
--[[
Get the state of time from the current time
> currentTime - the current time in hours
< Returns the state of time (Day or Night)
]]
Time.GetStateFromCurrentTime = function(currentTime : number)
if currentTime >= TimeData.DaytimeRange.X and currentTime < TimeData.DaytimeRange.Y then
return TimeEnum.State.Day
else
return TimeEnum.State.Night
end
end
--[[
Get the new time rate for the given state and duration
> newState - the new state to get the time rate for
> stateDuration - the duration of the new state
< Returns the time rate (in hours elapsed per second)
]]
Time.GetTimeRate = function(newState : string, stateDuration : number)
if newState == TimeEnum.State.Day then
return TimeData.DaytimeLength / stateDuration
elseif newState == TimeEnum.State.Night then
return TimeData.NighttimeLength / stateDuration
else
warn("Invalid state given to GetNewTimeRate:", newState)
return 0
end
end
Time.UpdatePlayers = function()
for _, player in Players:GetPlayers() do
local character = player.Character
if not character then continue end
local humanoid : Humanoid = character:FindFirstChild("Humanoid")
if not humanoid then continue end
if humanoid.Health <= 0 then continue end
local playerData = player:FindFirstChild("PlayerData")
if not playerData then continue end
-- Increase Cycle Count
playerData.Cycles.Value += 1
player:SetAttribute("CyclesSurvived", player:GetAttribute("CyclesSurvived") + 1)
-- Give Referral Rewards
if DateTime.now().UnixTimestamp <= 1793440800 then
if playerData.Cycles.Value >= 2 then
local referringPlayerId = player:GetJoinData().ReferredByPlayerId
if referringPlayerId then
local referringPlayer = Players:GetPlayerByUserId(referringPlayerId)
if referringPlayer then
DataManager.AddPearls(player, 100, Enum.AnalyticsEconomyTransactionType.Onboarding, "100PearlsReferral")
DataManager.AddPearls(referringPlayer, 100, Enum.AnalyticsEconomyTransactionType.Onboarding, "100PearlsReferral")
Received:FireClient(player, true)
Received:FireClient(referringPlayer, false)
end
end
end
end
-- Add Sleep Actions
PlayerHandler.AddAction(player, "Sleep", "")
PlayerHandler.AddAction(player, "Cycle", playerData.Cycles.Value + 1)
-- Increment Player's Karma
DataManager.IncrementKarma(player)
-- Grant Badges
if not workspace:GetAttribute("IsPrivateServer") and playerData.Cycles.Value >= 50 then
PlayerHandler.AwardBadge(player, 4446430336868065, true)
end
if not workspace:GetAttribute("IsPrivateServer") and player:GetAttribute("CyclesSurvived") >= 1000 then
PlayerHandler.AwardBadge(player, 3402815993852149, true)
end
end
end
--[[
Get a random new state duration for the given state
> newState - the new state to get the duration for
< Returns the duration of the state
]]
Time.GetNewStateDuration = function(newState : string)
if newState == TimeEnum.State.Day then
return math.random(Time.DayLengthRange.X, Time.DayLengthRange.Y)
elseif newState == TimeEnum.State.Night then
return math.random(Time.NightLengthRange.X, Time.NightLengthRange.Y)
else
warn("Invalid state given to GetNewStateDuration:", newState)
return 0
end
end
export type TimeEvent = {
Time : number?, -- in hours [0 - 24)
RemainingTime : number?, -- in seconds
State : string?,
Event : () -> (),
LastFired : number, -- last server cycle that this event was fired
}
--[[ Time Events (autofill did not work so I had to combine them)
{ -- Fires when the given time is reached
Time : number,
Event : () -> (),
LastFired : number
}
{ -- Fires when the given value is <= the remaining time left in the current state (can be combined with State)
RemainingTime : number,
State : string?,
Event : () -> (),
LastFired : number
}
{ -- Fires every time the state transitions to the given state
State : string,
Event : () -> (),
LastFired : number
}
{ -- Fires every new cycle
Event : () -> (),
LastFired : number
}
]]
return Time