How to make a daily & weekly challenge system

Yes I know this question has been asked countless times on the forum, but none of them really go in depth on how to make a proper challenge system.
image
WHAT IM TRYING TO ACHIEVE

    1. Players get 3 random challenges every day, and 3 random challenges every week
    1. Quest progress is tracked in the ui
    1. A currency reward is given on completion
    1. At the end of a week or day all the quests are cycled for new ones and the data for old the challenges are wiped.

I have no idea how to execute this, could someone walk me through the steps/process of making a system like this? I’m not asking for a copy and paste script, just some guidance.

7 Likes

I think to achieve this, it would require the knowledge of Datastore, which looks hard, but it’s actually simple than you think.

Then we would need os.time() to tell when to cycle, a Dictionary for picking challenges, a bindable event for rewarding, and a remote function for getting infos.

So first, we want to connect to PlayerAdded so we can start the challenge for them and start setting the data just for them. We will have a dictionary that will be used for setting async and to get the value for when to cycle and what challenges do they have.

Then we will need a loop to constantly check if the time is up. We will use a table to tell when it’s time to avoid using GetAsync in a loop.

Here is the code:

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local ChallengeData = DataStoreService:GetDataStore("ChallengeData")

local function GetAsync(key: string)
	local success, result = pcall(function()
		return ChallengeData:GetAsync(key)
	end)
	return success, result
end

local function SetAsync(key: string, value)
	local success, result = pcall(function()
		return ChallengeData:SetAsync(key, value)
	end)
	return success, result
end

local Challenges = {
	["GrassTouch"] = "Touch Grass For 5 Minutes"
}

local function CycleWeeklyChallenges(player: Player, value)
	value.WeeklyChallenges = {}
	
	for i = 1, 3 do
		table.insert(value.WeeklyChallenges, Challenges[math.random(1, #Challenges)])
	end

	SetAsync(tostring(player.UserId), value)
end

local function CycleDailyChallenges(player: Player, value)
	value.DailyChallenges = {}
	
	for i = 1, 3 do
		table.insert(value.DailyChallenges, Challenges[math.random(1, #Challenges)])
	end

	SetAsync(tostring(player.UserId), value)
end

local PlayingPlayers = {}

Players.PlayerAdded:Connect(function(player)
	local success, result = GetAsync(tostring(player.UserId))
	if success then
		if result then
			if os.time() - result.LastDailyCycled > 86400 then
				CycleDailyChallenges(player, result)
			end
			if os.time() - result.LastWeeklyCycled > 604800 then
				CycleDailyChallenges(player, result)
			end
		else
			local Value = {
				["LastDailyCycled"] = os.time();
				["LastWeeklyCycled"] = os.time();
				["WeeklyChallenges"] = {};
				["DailyChallenges"] = {}
			}

			for i = 1, 3 do
				table.insert(Value.WeeklyChallenges, Challenges[math.random(1, #Challenges)])
				table.insert(Value.DailyChallenges, Challenges[math.random(1, #Challenges)])
			end

			SetAsync(tostring(player.UserId), Value)
		end
	else
		--Inform the player of a potential data loss
	end
	PlayingPlayers[player] = result
end)

Players.PlayerRemoving:Connect(function(player)
	PlayingPlayers[player] = nil
end)

while task.wait(1) do
	for player, data in pairs(PlayingPlayers) do
		if os.time() - data.LastDailyCycled > 86400 then
			CycleDailyChallenges(player, data)
		end
		if os.time() - data.LastWeeklyCycled > 604800 then
			CycleDailyChallenges(player, data)
		end
	end
end

I did not test it, but hopefully it works right for you. You can modify this code to reward players and stuff.

5 Likes

This is extremely helpful actually! So how would I go about creating challenges? Like getting a certain amount of kills, getting a killstreak, earning a certain amount of currency, etc

There are a lot of ways to code this system, personally, I would code it in such a way to allow for high extendability and efficiency.

Off the top of my head, I would venture to say that we need a way to track user progress on challenges as well as a list of generated or hard-coded challenges to achieve.

Here is a free hand code example of what is going through my head.

local Goals = {
[1] = {
Kills = 100,
Progress = 0,
Discr = "Kill 100 Players",
}
}

Rewards = {
[1] = {
Gold = 100,
}
}

Here we have an example of the Goals table, what the user needs to do. Then we have a one-to-one relationship with the rewards table which allows us to attach the goal to the reward given the id of 1.

We can also consider a one to many with the rewards being the primary key and the foreign key being attacked to the Goals.

That would look like this:

local Goals = {
[1] = {
Kills = 100,
Progress = 0,
Discr = "Kill 100 Players",
RewardId = 1,
}
[2] = {
XP= 1000,
Progress = 0,
Discr = "Get 1000 XP",
RewardId = 1,
}
}

Rewards = {
[1] = {
Gold = 100,
}
}

Above is the established one-to-many with the rewards for the goals. This allows us to easily add new rewards and goals without overcomplicating the logic.

Now, Assuming we want to make this work, we need to use the actor-observer pattern. We need to setup events to keep track of when players do something that could add to the goals progress.

Lets say we have this player data table here.

local PlrData = {
Challenges = {1, 2}
}

This may look strange but bear with me, the values in this table correspond to another table called “PlayerProgress”. We would need to query that table to get the progress of the goals for that user.

That table structure would look like this:

local Database_PlayerProgress = {
[UserId] = {
[1] = {
Kills = 0;
}; 
[2] = {
XP = 0;
}
}
}

All we store is the progress of that challenge.

Now, when the user does something like kill the player, we can send an event to the controller and update that value.

function  KillPlayerEvent(Player : Player)
local UserID = Player.UserId
local Progress = Database_PlayerProgress[UserID][1]
if Progress  then -- this assumes we have the user id set.
Progress.Kills += 1
end
end

Saving the data is really simple, we have 4 separate tables, all of which contain separate data to keep things normalized and to prevent tight coupling.

This is my approach, many will disagree with me, but I come from a Data-Driven Design background and I have worked at some top Software companies and this is logically what came to mind. Hope this helped you.

4 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.