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
- Save to:
Client/GGPlugins/DailyReward/DailyReward.lua - Register in your plugin loader
- Add packet ID to
PluginPacket.lua
Server
- Save to:
Server/GGPlugins/ScriptDailyReward/DailyReward.lua - Register in
Plugins.lua - Add packet ID to
PluginPacket.lua - 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
- Open the window with
DailyReward:open()or add a hotkey - Click "Claim Reward" button
- Check you received zen and item
- Try claiming again - should show error
- Check database to verify record was created
- 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!