Skip to main content

Daily Reward System

A complete example showing how to build a simple but functional daily reward system. Players can claim rewards once per day, with server-side validation and database storage.

What You'll Learn

  • Creating a basic UI window with a button
  • Sending requests to the server
  • Server-side validation and cooldowns
  • SQL database operations
  • Sending rewards to players

Features

  • Simple, clean UI
  • One reward per day per account
  • Database tracking of last claim date
  • Rewards: Zen + Item
  • Server-side validation (can't cheat!)

Client Side

First, let's build the client interface:

--[[
Daily Reward System - CLIENT
A simple plugin that lets players claim daily rewards
]]

-- Plugin setup
DailyReward = {
index = GetWindowIndex(),
group = UI_GROUP_INGAME,
width = 300,
height = 180,

-- State (updated by server)
canClaim = false,
lastClaimTime = ""
}

-- Register packet ID (MUST match server!)
PluginPacket = PluginPacket or {}
PluginPacket.DailyReward = 15100

-- Render the main window
function DailyReward.renderWindow(window)
-- Window border and background
UIRenderWindow(window, 0, 0, window.w, window.h, 0)

-- Title bar background
window:renderColor(0, 0, window.w, 35, RGBA(0, 0, 0, 200))

-- Title text
window:renderText(
"Daily Reward",
0, 10, window.w, 30,
4, -- Center aligned
RGBA(255, 215, 0, 255), -- Gold color
FontL
)

-- Separator line
window:renderColor(10, 40, window.w - 20, 2, RGBA(100, 100, 100, 255))

-- Instructions
local infoY = 55
window:renderText(
"Click the button below to",
0, infoY, window.w, 25,
4, RGBA(200, 200, 200, 255), FontM
)

window:renderText(
"claim your daily reward!",
0, infoY + 20, window.w, 25,
4, RGBA(200, 200, 200, 255), FontM
)

-- Status message
local statusText = ""
local statusColor = RGBA(255, 255, 255, 255)

if DailyReward.canClaim then
statusText = "Available to claim!"
statusColor = RGBA(0, 255, 0, 255) -- Green
else
statusText = "Already claimed today"
statusColor = RGBA(255, 0, 0, 255) -- Red
end

window:renderText(
statusText,
0, infoY + 50, window.w, 25,
4, statusColor, FontM
)

-- Last claim time (if available)
if DailyReward.lastClaimTime ~= "" then
window:renderText(
"Last claim: " .. DailyReward.lastClaimTime,
0, window.h - 30, window.w, 20,
4, RGBA(150, 150, 150, 255), FontS
)
end
end

-- Render the claim button
function DailyReward.renderButton(window, button)
if button.hover == 0 then
button:renderAsset(GlobalAsset.buttonN, 0, 0, button.w, button.h)
elseif button.click == 0 then
button:renderAsset(GlobalAsset.buttonH, 0, 0, button.w, button.h)
elseif button.click == 1 or button.click == 2 then
button:renderAsset(GlobalAsset.buttonC, 0, 0, button.w, button.h)
elseif button.click == 3 then
-- Released click - execute action!
button:renderAsset(GlobalAsset.buttonC, 0, 0, button.w, button.h)

-- Play click sound
SoundPlay(10)

-- Send request to server
MagicWorld:sendData(PluginPacket.DailyReward, {"claim"})

-- Show feedback
NoticeSend(1, "Checking reward...")
end

-- Button text
button:renderText(
"Claim Reward",
0, 0, button.w, button.h,
4, RGBA(255, 255, 255, 255), FontM
)
end

-- Render close button
function DailyReward.renderCloseButton(window, button)
if button.hover == 0 then
button:renderAsset(GlobalAsset.buttonCloseN, 0, 0, button.w, button.h)
elseif button.click == 0 then
button:renderAsset(GlobalAsset.buttonCloseH, 0, 0, button.w, button.h)
else
button:renderAsset(GlobalAsset.buttonCloseC, 0, 0, button.w, button.h)

if button.click == 3 then
window:setState(0)
window:setSpot(window.ox, window.oy)
SoundPlay(10)
end
end
end

-- Initialize plugin
BridgeFunction:push("OnLoad", function()
LogPrint("[DailyReward] Initializing plugin...")

local window = UIWindowAdd(
DailyReward.group,
DailyReward.index,
0, -- Start hidden
2, -- Depth
GetWindowCenterX(DailyReward.width),
GetWindowCenterY(DailyReward.height),
DailyReward.width,
DailyReward.height,
1, -- Scale
DailyReward.renderWindow,
nil
)

if window then
-- Make title bar draggable
window:setDragArea(0, 0, DailyReward.width, 35)

-- Add close button
window:addButton(
0, 1,
DailyReward.width - 30, 8,
20, 20,
DailyReward.renderCloseButton
)

-- Add claim button
local buttonWidth = 180
local buttonHeight = 35
window:addButton(
1, 1,
(DailyReward.width - buttonWidth) / 2,
DailyReward.height - buttonHeight - 15,
buttonWidth, buttonHeight,
DailyReward.renderButton
)

LogPrint("[DailyReward] Window created successfully!")
else
LogPrint("[DailyReward] ERROR: Failed to create window!")
end
end)

-- Receive packets from server
MagicWorld:pushRecv(PluginPacket.DailyReward, function(packet)
local responseType = packet:getString()

if responseType == "status" then
-- Server sent reward status
local canClaim = packet:getNumber() -- 0 or 1
local lastTime = packet:getString()

DailyReward.canClaim = (canClaim == 1)
DailyReward.lastClaimTime = lastTime

LogPrint(string.format(
"[DailyReward] Status received - Can claim: %s",
tostring(DailyReward.canClaim)
))

elseif responseType == "success" then
-- Reward claimed successfully!
local zenReceived = packet:getNumber()
local itemName = packet:getString()

NoticeSend(0, "═══════════════════")
NoticeSend(0, "Daily Reward Claimed!")
NoticeSend(0, string.format("+ %d Zen", zenReceived))
NoticeSend(0, string.format("+ %s", itemName))
NoticeSend(0, "═══════════════════")

SoundPlay(15)
DailyReward.canClaim = false

elseif responseType == "error" then
-- Error claiming reward
local errorMessage = packet:getString()
NoticeSend(1, errorMessage)
LogPrint("[DailyReward] Error: " .. errorMessage)
end

packet:free()
end)

-- Function to open the window
function DailyReward:open()
local window = UIWindowGet(self.group, self.index)
if window then
if window.state == 0 then
window:setState(1)
-- Request status from server
MagicWorld:sendData(PluginPacket.DailyReward, {"check_status"})
else
window:setState(0)
window:setSpot(window.ox, window.oy)
end
return true
end
return false
end

Server Side

Now the server logic with validation and database:

--[[
Daily Reward System - SERVER
Validates claims and stores data in database
]]

DailyReward = {}
DailyReward.ScriptVersion = "1.0.0"

-- Reward configuration
DailyReward.Config = {
zenReward = 5000000, -- 5kk zen

itemReward = {
section = 14, -- Jewels
index = 13, -- Jewel of Bless
level = 0
},
}

-- Register packet ID (MUST match client!)
PluginPacket = PluginPacket or {}
PluginPacket.DailyReward = 15100

-- Create database table
BridgeFunction:push("OnLoadScript", function()
local createTableQuery = [[
IF NOT EXISTS (SELECT * FROM sys.objects
WHERE object_id = OBJECT_ID(N'DailyReward'))
BEGIN
CREATE TABLE DailyReward (
AccountID VARCHAR(10) NOT NULL PRIMARY KEY,
CharName VARCHAR(10) NOT NULL,
LastClaimDate DATE NOT NULL,
TotalClaims INT NOT NULL DEFAULT 1,
LastClaimDateTime DATETIME NOT NULL DEFAULT GETDATE()
)
END
]]

SQLQuery(createTableQuery, function(query)
if query then
LogColor(2, "[DailyReward] Table verified/created")
else
LogColor(1, "[DailyReward] ERROR creating table!")
end
end)
end)

-- Check if player can claim today
function DailyReward:canClaimToday(aIndex, callback)
local accountId = GetObjectAccount(aIndex)
local charName = GetObjectName(aIndex)

local checkQuery = string.format([[
SELECT LastClaimDate,
CONVERT(VARCHAR, LastClaimDateTime, 120) as LastClaimTime
FROM DailyReward
WHERE AccountID = '%s'
]], accountId)

SQLQuery(checkQuery, function(query)
-- ALWAYS validate player is still online!
if GetObjectConnected(aIndex) ~= OBJECT_ONLINE then
return
end

if GetObjectName(aIndex) ~= charName then
return
end

local canClaim = false
local lastClaimTime = "Never"
local isFirstClaim = true

if query.result > 0 and query:fetch() then
isFirstClaim = false
local lastDate = query:getString("LastClaimDate")
lastClaimTime = query:getString("LastClaimTime")
local today = os.date("%Y-%m-%d")

if lastDate ~= today then
canClaim = true
end
else
canClaim = true
end

callback(canClaim, lastClaimTime, isFirstClaim)
end)
end

-- Give reward to player
function DailyReward:giveReward(aIndex)
local accountId = GetObjectAccount(aIndex)
local charName = GetObjectName(aIndex)

self:canClaimToday(aIndex, function(canClaim, lastTime, isFirstClaim)
if GetObjectConnected(aIndex) ~= OBJECT_ONLINE then
return
end

if not canClaim then
MagicWorld:sendData(aIndex, PluginPacket.DailyReward, {
"error",
"You already claimed your reward today!"
})
return
end

-- Check inventory space
if InventoryGetFreeSlotCount(aIndex) < 1 then
MagicWorld:sendData(aIndex, PluginPacket.DailyReward, {
"error",
"Inventory full! Free up space first."
})
return
end

-- Give zen
local currentZen = GetObjectMoney(aIndex)
SetObjectMoney(aIndex, currentZen + self.Config.zenReward)
MoneySend(aIndex, GetObjectMoney(aIndex))

-- Give item
ItemGive(
aIndex,
self.Config.itemReward.section,
self.Config.itemReward.index,
self.Config.itemReward.level
)

local itemId = (self.Config.itemReward.section * 512) +
self.Config.itemReward.index
local itemName = ItemGetName(itemId) or "Item"

-- Update database
local updateQuery = ""
if isFirstClaim then
updateQuery = string.format([[
INSERT INTO DailyReward (AccountID, CharName, LastClaimDate,
TotalClaims, LastClaimDateTime)
VALUES ('%s', '%s', CONVERT(DATE, GETDATE()), 1, GETDATE())
]], accountId, charName)
else
updateQuery = string.format([[
UPDATE DailyReward
SET LastClaimDate = CONVERT(DATE, GETDATE()),
TotalClaims = TotalClaims + 1,
LastClaimDateTime = GETDATE(),
CharName = '%s'
WHERE AccountID = '%s'
]], charName, accountId)
end

SQLQuery(updateQuery, function(query) end)

-- Send success to client
MagicWorld:sendData(aIndex, PluginPacket.DailyReward, {
"success",
self.Config.zenReward,
itemName
})

LogColor(2, string.format(
"[DailyReward] %s claimed: %d zen + %s",
charName, self.Config.zenReward, itemName
))
end)
end

-- Send status to client
function DailyReward:sendStatus(aIndex)
self:canClaimToday(aIndex, function(canClaim, lastTime, isFirstClaim)
if GetObjectConnected(aIndex) ~= OBJECT_ONLINE then
return
end

MagicWorld:sendData(aIndex, PluginPacket.DailyReward, {
"status",
canClaim and 1 or 0,
lastTime
})
end)
end

-- Handle packets from client
MagicWorld:pushRecv(PluginPacket.DailyReward, function(aIndex, packet)
local action = packet:getString()

if action == "check_status" then
DailyReward:sendStatus(aIndex)
elseif action == "claim" then
DailyReward:giveReward(aIndex)
end
end)

-- Send status when player enters game
BridgeFunction:push("OnCharacterEntry", function(aIndex)
DailyReward:sendStatus(aIndex)
end)

LogColor(3, "[DailyReward] Plugin loaded - Version: " .. DailyReward.ScriptVersion)

Key Concepts Demonstrated

1. UI Design

  • Clean, centered window
  • Draggable title bar
  • Close button
  • Action button with visual feedback
  • Status text with color coding

2. Client-Server Communication

  • Client sends action requests
  • Server validates and responds
  • Multiple response types (status, success, error)
  • Always freeing packets after use

3. Database Operations

  • Creating tables on load
  • Checking existing records
  • Inserting new records
  • Updating existing records
  • Using proper date comparisons

4. Server Validation

  • Always checking if player is still online
  • Validating player name matches
  • Checking inventory space
  • Preventing duplicate claims

5. User Feedback

  • Visual status indicators
  • Sound effects
  • Notifications
  • Clear error messages

Installation

Client

  1. Save to: Client/GGPlugins/DailyReward/DailyReward.lua
  2. Register in your plugin loader
  3. Add packet ID to PluginPacket.lua

Server

  1. Save to: Server/GGPlugins/ScriptDailyReward/DailyReward.lua
  2. Register in Plugins.lua
  3. Add packet ID to PluginPacket.lua
  4. Restart server

Possible Improvements

  • Add a hotkey to open the window (e.g., F6)
  • Show preview of rewards before claiming
  • Animation when claiming
  • Countdown timer to next reward
  • Claim history display
  • Streak bonuses (consecutive days)
  • Special rewards on weekends
  • Multiple reward tiers based on VIP level

Testing

  1. Open the window with DailyReward:open() or add a hotkey
  2. Click "Claim Reward" button
  3. Check you received zen and item
  4. Try claiming again - should show error
  5. Check database to verify record was created
  6. Wait until next day to claim again

This example shows the complete flow of a client-server plugin with database integration. Use it as a template for your own systems!