Reproducing Grow A Garden Offline System

I am trying to do a system like Grow A Garden Offline but for my game. The purpose of my Offline system is that Swords will keep running in the Anvil. When the Swords are offline and the player comeback it does some weird behavior in which the ScrollingFrame are getting frozen and the cooldown stops running.

Here’s the code of the player when he comes back in the game :

if #playerData.OfflineSwords > 0 then	
				if not DataLoaded[player.UserId] then
					print("DATA LOADED IS : ", DataLoaded[player.UserId])			

					DataLoaded[player.UserId] = true

					local TotalTimeTextLabel = playerPlot.Forge:FindFirstChild(AnvilOwned):FindFirstChild(AnvilOwned).Description.Frame:FindFirstChild("Total Time")

					local TimeLeftCooldown = CurrentTime - LastTimePlayerOnline

					local elapsed = TimeLeftCooldown
										
					for index, sword in ipairs(playerData.OfflineSwords) do
						local sword = playerData.OfflineSwords[index]

						if index <= NumberRunningSwordAllowed then
							if elapsed >= sword.Countdown then
								sword.Countdown = 0
								sword.IsFinished = true
								sword.IsRunning = false
								
								local RackService = require(ServerScriptService:WaitForChild("Services"):WaitForChild("RackService"))
								local MoneyService = require(ServerScriptService:WaitForChild("Services"):WaitForChild("MoneyService"))


								local function getSwordMultiplier(level)
									return 0.05 * level^2 + 0.35 * level + 1.3
								end

								local ChoosenModel = nil
								local TotalWeaponAvailable = 0
								local CraftedItem = sword.Name
								local lowestId = math.huge 

								for _, model in playerPlot.Ground.Objects:GetChildren() do
									if string.find(model.Name, "Sword holder") then
										local curAmount = model:GetAttribute("Amount")
										local maxAmount = model:GetAttribute("MaxAmount")
										local holderId = model:GetAttribute("HolderId")
										TotalWeaponAvailable += maxAmount

										-- Si le rack a encore de la place et que son HolderId est plus petit que le plus bas trouvé
										if (maxAmount - curAmount) > 0 and holderId < lowestId then
											lowestId = holderId
											ChoosenModel = model
										end
									end
								end

								local rawPrice = Material.Prices[sword.Material] * getSwordMultiplier(sword.Level)
								local selectedTeleportedPart = nil
								local lowestRank = math.huge

								for i = 1, ChoosenModel:GetAttribute("MaxAmount") do
									local swordName = "Sword" .. i .. "Location"
									local part = ChoosenModel:FindFirstChild("SwordHolder"):FindFirstChild(swordName)
									if part and part:GetAttribute("Availability") then
										local rank = part:GetAttribute("Rank")
										if rank and rank < lowestRank then
											lowestRank = rank
											selectedTeleportedPart = part

										end
									end
								end

								local playerData = DataService.getData(player)
								if playerData then
									local holder = playerData.Holders
									local IdHolderChoosen = 0
									local lowestId = math.huge -- on part de l'infini pour comparer

									for _, model in playerPlot.Ground.Objects:GetChildren() do
										if string.find(model.Name, "Sword holder") then
											local curAmount = model:GetAttribute("Amount")
											local maxAmount = model:GetAttribute("MaxAmount")
											local holderId = model:GetAttribute("HolderId")

											-- Si le rack a encore de la place et que son HolderId est plus petit que le plus bas trouvé
											if (maxAmount - curAmount) > 0 and holderId < lowestId then
												lowestId = holderId
												IdHolderChoosen = holderId
											end
										end
									end

									holder["Holder" .. IdHolderChoosen][sword.Material .. " " .. CraftedItem .. string.match(selectedTeleportedPart.Name, "%d+")] = {
										Count = 1,
										Name = CraftedItem,
										MaterialName = sword.Material,
										PositionPart = selectedTeleportedPart.Name,
										Price = math.floor(rawPrice * 100 + 0.5) / 100,
										Level = sword.Level
									}
									RackService.createNewSword(player, "Holder" .. IdHolderChoosen, sword.Material .. " " .. CraftedItem .. string.match(selectedTeleportedPart.Name, "%d+"))
								end

								-- 6. Donner l'XP et réorganiser l'ordre
								local amountExpGiven = LevelSystem.getExpMaterial(sword.Material .. " Ingot")
								LevelSystem.givePlayerExp(player, amountExpGiven)
								local LevelService = require(ServerScriptService:WaitForChild("Services"):WaitForChild("LevelService"))
								LevelService.increaseExp(player, amountExpGiven)
								MoneyService.giveForgePoint(player, sword.Level + 1)

								local OrderCrafting = 0
								for _, f in ipairs(playerPlot.Forge:FindFirstChild(AnvilOwned):FindFirstChild(AnvilOwned).BillboardGui.Frame.ScrollingFrame:GetChildren()) do
									if f:IsA("Frame") then
										local img = f:FindFirstChild("ImageLabel")
										if img then
											OrderCrafting += 1
											img:SetAttribute("LayoutOrder", OrderCrafting)
										end
									end
								end
							else
								
								local CloneFrame = UIsSword.ScreenGui:FindFirstChild(sword.Name):Clone()
								CloneFrame.Parent = playerPlot.Forge:FindFirstChild(AnvilOwned):FindFirstChild(AnvilOwned).BillboardGui.Frame.ScrollingFrame

								local ImageLabel = CloneFrame.ImageLabel
								
								sword.Countdown -= elapsed

								ImageLabel.Timer.Text = FormatTime(sword.Countdown, true) 
								ImageLabel:SetAttribute("LayoutOrder", index)
								ImageLabel:SetAttribute("Countdown", sword.Countdown)

								ImageLabel:SetAttribute("Material", sword.Material)
								ImageLabel:SetAttribute("Level", sword.Level)
								ImageLabel:SetAttribute("IsFinished", sword.IsFinished)
								ImageLabel:SetAttribute("IsRunning", sword.IsRunning)
								
							end
						else
							local CloneFrame = UIsSword.ScreenGui:FindFirstChild(sword.Name):Clone()
							CloneFrame.Parent = playerPlot.Forge:FindFirstChild(AnvilOwned):FindFirstChild(AnvilOwned).BillboardGui.Frame.ScrollingFrame

							local ImageLabel = CloneFrame.ImageLabel

							sword.Countdown -= elapsed
							
							ImageLabel.Timer.Text = FormatTime(sword.Countdown, true) 
							ImageLabel:SetAttribute("LayoutOrder", index)
							ImageLabel:SetAttribute("Countdown", sword.Countdown)

							ImageLabel:SetAttribute("Material", sword.Material)
							ImageLabel:SetAttribute("Level", sword.Level)
							ImageLabel:SetAttribute("IsFinished", sword.IsFinished)
							ImageLabel:SetAttribute("IsRunning", sword.IsRunning)
						end

						TotalTime += sword.Countdown

					end

					TotalTimeTextLabel.Text = "TOTAL TIME: " .. FormatTime(TotalTime, false)
					
					print("CURRENT NUMBER RUNNING WEAPON RIGHT NOW IS : ", CurrentNumberRunningWeapon)
					
					
				end
			end

It doesn’t count well the number of running weapon either. Here’s the code that count the number of weapon :

local scrollingFrame = playerPlot.Forge:FindFirstChild(AnvilOwned):FindFirstChild(AnvilOwned).BillboardGui.Frame.ScrollingFrame

for _, frame in ipairs(scrollingFrame:GetChildren()) do
	if frame:IsA("Frame") then
		CurrentNumberRunningWeapon += 1
		print("CURRENT NUMBER RUNNING WEAPON IS : ", CurrentNumberRunningWeapon, player.Name)

	end
end
					
handlePromptTriggered(player, false)

As you can see we got weapons that got removed from the Offlines but it still somehow add 2 weapons back and it was the previous one this is a very annoying bug. The CurrentNumberRunningWeapon counter although initialize at 0 when I assign the plot each time directly skipped through 10 which is like the equivalent of the Swords length it gave me and now I wonder if my problem is when I save the data or when I load the data

Here’s the code to save the last running weapon of the game :

local index = 0
			
			print("OFFLINE SWORDS IS : ", profile.Data.OfflineSwords)
			profile.Data.OfflineSwords = {}
			
			for _, frame in ipairs(ScrollingFrame:GetChildren()) do
				if frame:IsA("Frame") then
					local imageLabel = frame.ImageLabel
					index += 1
					
					imageLabel:SetAttribute("LayoutOrder", index)
					imageLabel:SetAttribute("IsRunning", false)

					profile.Data.OfflineSwords[index] = {
						Name = frame.Name,
						Countdown = imageLabel:GetAttribute("Countdown"),
						Material = imageLabel:GetAttribute("Material"),
						Level = imageLabel:GetAttribute("Level"),
						IsFinished = imageLabel:GetAttribute("IsFinished"),
						IsRunning = imageLabel:GetAttribute("IsRunning")
					}
				end
			end
			
			print("OFFLINE SWORDS LENGTH IS : ", #profile.Data.OfflineSwords)

When I comeback sometimes, it adds some frame for exemple the length of my ScrollingFrame was 4 but it adds the previous cooldown of a sword that has already finished which is very weird and I have no idea about why it’s doing this.

Note that handlePromptTriggered(player, false) is the function that runs the cooldown of each swords in the anvil

Here’s the proof of cooldown not working :

I imagine this is how they do it.

local function update(_, deltaTime: number)
   -- Plants grow by `deltaTime` seconds when this method is called
end

RunService.Stepped:Connect(update)
-- Returns the amount of seconds the user has last joined
local function getUserTimeAway(user): number
    local profile --< User's data is stored here
    local lastTimeOnline  = profile.LastTimeOnline --< Stored in UTC time
    local currentTime = DateTime.now().UnixTimestamp --< Same value used to store LastTimeOnline
    return currentTime - lastTimeOnline
end
Players.PlayerAdded:Connect(function(user)
    -- Wait for data to load, ensure data loaded successfully
    local timeAway = getUserTimeAway(user)
    update(timeAway) --< pass the user's offline time as the delta time
end)

This is pseudo code, but hopefully you get the idea. The game grows the user’s fruits by a small amount every frame. Use this same approach to grow their fruit a larger amount when they come back online, using the same method!

Global mutations are separate influences, and wouldn’t be in the update method. If you’re trying to do that, don’t bother. It’s too overpowered anyways!

2 Likes

Yeah but the way my system works is different because it’s swords now

Doesn’t change the logic whether it’s swords or no?
Player joins → Get offline time → calculate how much it should have grown → grow

Ok, then it’s actually a lot simpler then that! When you decide to start creating a sword at the anvil, save and tell the user when it finishes. For example:

-- Returns the amount of time it takes to cook a sword of the specified type
local function getCookTimeForSword(type)
    -- TODO
end

StartCookingSword.OnServerEvent:Connect(function(user, type)
    -- Validate that this user can create a sword of the specified type

    local profile --< Get the user's loaded profile
    profile.Data.CookSwordType = type
    profile.Data.CookSwordEndTime = DateTime.now().UnixTimestamp + getCookTypeForSword(type)
    
    -- Consider using Replica by @loleris to sync these changes to the client
end)

This way, you don’t have to have an update function. The end time is stored in the data, and it doesn’t change until that time is reached. You can check if the sword is done when they go online again!

Players.PlayerAdded:Connect(function(user)
	local profile --< Load or wait for user data here
	if not profile then return end

	local now = DateTime.now().UnixTimestamp
	local cookSwordEndTime = profile.Data.CookSwordEndTime

	if cookSwordEndTime and now > cookSwordEndTime then
		--< The sword is done cooking!
	else
		local thread = coroutine.running()
		
		user.AncestryChanged:Connect(function(_, parent)
			if not parent then
				pcall(task.cancel, thread)
			end
		end)
		
		task.wait(cookSwordEndTime - now)
		
		--< The sword is done cooking!
	end
end)

Using this code, the server will know when the sword is done cooking when the user joins. If it’s not done yet, it will wait until it does finish. And if they leave, that wait is cancelled and later the next server the user joins will check. Let me know if you have any questions!

Stepped doesn’t work on server, Heartbeat only does, and heartbeat is at 60fps, so deltatime is 1/60, he should adjust the away time to 1/awaySeconds (i think)

Both RunService.Stepped and RunService.Heartbeat work on the server. It’s RunService.RenderStepped that doesn’t fire because the server does no rendering.

Oh my bad then, also my mistake, dt == away time in s

There was still an error in my code, I just fixed it. time is the first argument passed to the Stepped event, then deltaTime. Weird they would do that, but oh well.

Actually, probably set the current time when a plant has been created. Basing off that you can create a growing system, as opposed to getting an offline time.

I’ve basically stored the time when the player left the game aka was online and when the player came back I calculated the difference between the CurrentTime and the LastTime that the player was offline

and pass it to ur grow function or wtv
example of grow function

local GrowthRate = 0.1
local function growPlant(dt: number)
    Plant.Size *= GrowthRate * dt
end