game-systems

When implementing game systems, follow these patterns for robust and exploiter-resistant mechanics.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "game-systems" with this command: npx skills add taozhuo/game-dev-skills/taozhuo-game-dev-skills-game-systems

Roblox Game Systems

When implementing game systems, follow these patterns for robust and exploiter-resistant mechanics.

Inventory System

Slot-Based Inventory

local InventoryService = {}

local DEFAULT_SLOTS = 20 local MAX_STACK = 99

function InventoryService.create(maxSlots) return { slots = {}, maxSlots = maxSlots or DEFAULT_SLOTS } end

function InventoryService.addItem(inventory, itemId, quantity) quantity = quantity or 1

-- Try to stack with existing
for slotIndex, slot in pairs(inventory.slots) do
    if slot.itemId == itemId and slot.quantity < MAX_STACK then
        local canAdd = math.min(quantity, MAX_STACK - slot.quantity)
        slot.quantity = slot.quantity + canAdd
        quantity = quantity - canAdd

        if quantity <= 0 then
            return true, slotIndex
        end
    end
end

-- Find empty slots for remaining
while quantity > 0 do
    local emptySlot = InventoryService.findEmptySlot(inventory)
    if not emptySlot then
        return false, "Inventory full"
    end

    local stackSize = math.min(quantity, MAX_STACK)
    inventory.slots[emptySlot] = {
        itemId = itemId,
        quantity = stackSize
    }
    quantity = quantity - stackSize
end

return true

end

function InventoryService.removeItem(inventory, itemId, quantity) quantity = quantity or 1 local removed = 0

-- Remove from slots (prefer partial stacks first)
local slots = {}
for slotIndex, slot in pairs(inventory.slots) do
    if slot.itemId == itemId then
        table.insert(slots, {index = slotIndex, quantity = slot.quantity})
    end
end

table.sort(slots, function(a, b) return a.quantity < b.quantity end)

for _, slotInfo in ipairs(slots) do
    local slot = inventory.slots[slotInfo.index]
    local toRemove = math.min(quantity - removed, slot.quantity)

    slot.quantity = slot.quantity - toRemove
    removed = removed + toRemove

    if slot.quantity <= 0 then
        inventory.slots[slotInfo.index] = nil
    end

    if removed >= quantity then
        break
    end
end

return removed >= quantity, removed

end

function InventoryService.hasItem(inventory, itemId, quantity) quantity = quantity or 1 local total = 0

for _, slot in pairs(inventory.slots) do
    if slot.itemId == itemId then
        total = total + slot.quantity
        if total >= quantity then
            return true
        end
    end
end

return false

end

function InventoryService.getItemCount(inventory, itemId) local total = 0 for _, slot in pairs(inventory.slots) do if slot.itemId == itemId then total = total + slot.quantity end end return total end

function InventoryService.findEmptySlot(inventory) for i = 1, inventory.maxSlots do if not inventory.slots[i] then return i end end return nil end

Shop System

Server-Side Shop with Validation

local ShopService = {}

local ShopItems = { sword_basic = {price = 100, currency = "coins", category = "weapons"}, potion_health = {price = 50, currency = "coins", category = "consumables"}, vip_pass = {price = 499, currency = "robux", productId = 123456789} }

function ShopService.canPurchase(player, itemId, quantity) quantity = quantity or 1 local item = ShopItems[itemId]

if not item then
    return false, "Item not found"
end

if item.currency == "robux" then
    -- Robux purchases handled differently
    return true, "Use promptPurchase"
end

local playerCurrency = DataManager.get(player, item.currency) or 0
local totalCost = item.price * quantity

if playerCurrency < totalCost then
    return false, "Not enough " .. item.currency
end

-- Check inventory space
local inventory = DataManager.get(player, "inventory")
local emptySlots = InventoryService.countEmptySlots(inventory)

if emptySlots < math.ceil(quantity / MAX_STACK) then
    return false, "Inventory full"
end

return true, totalCost

end

function ShopService.purchase(player, itemId, quantity) quantity = quantity or 1

local canBuy, result = ShopService.canPurchase(player, itemId, quantity)
if not canBuy then
    return false, result
end

local item = ShopItems[itemId]

if item.currency == "robux" then
    -- Prompt Robux purchase
    MarketplaceService:PromptProductPurchase(player, item.productId)
    return true, "Purchase prompted"
end

-- Deduct currency (atomic operation)
local success = DataManager.removeCurrency(player, item.currency, result)
if not success then
    return false, "Transaction failed"
end

-- Add item
local inventory = DataManager.get(player, "inventory")
local added = InventoryService.addItem(inventory, itemId, quantity)

if not added then
    -- Rollback currency
    DataManager.addCurrency(player, item.currency, result)
    return false, "Failed to add item"
end

return true, "Purchase successful"

end

-- Handle Robux purchases MarketplaceService.ProcessReceipt = function(receiptInfo) local player = Players:GetPlayerByUserId(receiptInfo.PlayerId) if not player then return Enum.ProductPurchaseDecision.NotProcessedYet end

local productId = receiptInfo.ProductId
local itemId = getItemByProductId(productId)

if not itemId then
    return Enum.ProductPurchaseDecision.NotProcessedYet
end

local inventory = DataManager.get(player, "inventory")
local added = InventoryService.addItem(inventory, itemId, 1)

if added then
    DataManager.save(player)
    return Enum.ProductPurchaseDecision.PurchaseGranted
end

return Enum.ProductPurchaseDecision.NotProcessedYet

end

Trading System

Secure Trading

local TradingService = {} TradingService.activeTrades = {}

function TradingService.requestTrade(player1, player2) local tradeId = HttpService:GenerateGUID()

TradingService.activeTrades[tradeId] = {
    player1 = {
        player = player1,
        items = {},
        confirmed = false
    },
    player2 = {
        player = player2,
        items = {},
        confirmed = false
    },
    status = "pending",
    createdAt = os.clock()
}

-- Notify player2
TradeRequestRemote:FireClient(player2, player1.Name, tradeId)

return tradeId

end

function TradingService.addItem(tradeId, player, itemSlot) local trade = TradingService.activeTrades[tradeId] if not trade or trade.status ~= "active" then return false, "Invalid trade" end

local side = trade.player1.player == player and trade.player1 or
             trade.player2.player == player and trade.player2

if not side then
    return false, "Not in this trade"
end

-- Reset confirmations when items change
trade.player1.confirmed = false
trade.player2.confirmed = false

-- Validate player owns the item
local inventory = DataManager.get(player, "inventory")
local item = inventory.slots[itemSlot]

if not item then
    return false, "Item not found"
end

-- Check item isn't already in trade
for _, existingSlot in ipairs(side.items) do
    if existingSlot == itemSlot then
        return false, "Item already in trade"
    end
end

table.insert(side.items, itemSlot)

-- Update both players' UI
TradeUpdateRemote:FireClient(trade.player1.player, tradeId, trade)
TradeUpdateRemote:FireClient(trade.player2.player, tradeId, trade)

return true

end

function TradingService.confirmTrade(tradeId, player) local trade = TradingService.activeTrades[tradeId] if not trade or trade.status ~= "active" then return false end

local side = trade.player1.player == player and trade.player1 or
             trade.player2.player == player and trade.player2

if not side then return false end

side.confirmed = true

-- Check if both confirmed
if trade.player1.confirmed and trade.player2.confirmed then
    return TradingService.executeTrade(tradeId)
end

-- Update UI
TradeUpdateRemote:FireClient(trade.player1.player, tradeId, trade)
TradeUpdateRemote:FireClient(trade.player2.player, tradeId, trade)

return true

end

function TradingService.executeTrade(tradeId) local trade = TradingService.activeTrades[tradeId] trade.status = "executing"

local p1 = trade.player1.player
local p2 = trade.player2.player
local inv1 = DataManager.get(p1, "inventory")
local inv2 = DataManager.get(p2, "inventory")

-- Validate both players still have the items
for _, slot in ipairs(trade.player1.items) do
    if not inv1.slots[slot] then
        TradingService.cancelTrade(tradeId, "Item no longer available")
        return false
    end
end

for _, slot in ipairs(trade.player2.items) do
    if not inv2.slots[slot] then
        TradingService.cancelTrade(tradeId, "Item no longer available")
        return false
    end
end

-- Execute swap atomically
local p1Items = {}
local p2Items = {}

-- Remove items from player 1
for _, slot in ipairs(trade.player1.items) do
    table.insert(p1Items, inv1.slots[slot])
    inv1.slots[slot] = nil
end

-- Remove items from player 2
for _, slot in ipairs(trade.player2.items) do
    table.insert(p2Items, inv2.slots[slot])
    inv2.slots[slot] = nil
end

-- Add player 1's items to player 2
for _, item in ipairs(p1Items) do
    InventoryService.addItem(inv2, item.itemId, item.quantity)
end

-- Add player 2's items to player 1
for _, item in ipairs(p2Items) do
    InventoryService.addItem(inv1, item.itemId, item.quantity)
end

-- Save both players
DataManager.save(p1)
DataManager.save(p2)

-- Complete trade
trade.status = "completed"
TradingService.activeTrades[tradeId] = nil

TradeCompleteRemote:FireClient(p1, tradeId, true)
TradeCompleteRemote:FireClient(p2, tradeId, true)

return true

end

Quest System

Quest Manager

local QuestService = {}

local QuestDefinitions = { kill_enemies_1 = { title = "Enemy Slayer", description = "Defeat 10 enemies", objectives = { {type = "kill", target = "enemy", count = 10} }, rewards = { {type = "currency", currency = "coins", amount = 100}, {type = "experience", amount = 50} } }, collect_items_1 = { title = "Collector", description = "Collect 5 gems", objectives = { {type = "collect", item = "gem", count = 5} }, rewards = { {type = "currency", currency = "coins", amount = 200} } } }

function QuestService.startQuest(player, questId) local quest = QuestDefinitions[questId] if not quest then return false end

local playerQuests = DataManager.get(player, "activeQuests") or {}

-- Check not already active
if playerQuests[questId] then
    return false, "Quest already active"
end

-- Initialize progress
playerQuests[questId] = {
    startedAt = os.time(),
    progress = {}
}

for i, objective in ipairs(quest.objectives) do
    playerQuests[questId].progress[i] = 0
end

DataManager.set(player, "activeQuests", playerQuests)
return true

end

function QuestService.updateProgress(player, eventType, eventData) local playerQuests = DataManager.get(player, "activeQuests") or {} local updated = false

for questId, questProgress in pairs(playerQuests) do
    local quest = QuestDefinitions[questId]
    if not quest then continue end

    for i, objective in ipairs(quest.objectives) do
        if objective.type == eventType then
            local matches = true

            -- Check target matches
            if objective.target and eventData.target ~= objective.target then
                matches = false
            end
            if objective.item and eventData.item ~= objective.item then
                matches = false
            end

            if matches and questProgress.progress[i] < objective.count then
                questProgress.progress[i] = questProgress.progress[i] + (eventData.amount or 1)
                updated = true

                -- Check if quest completed
                if QuestService.isQuestComplete(questId, questProgress) then
                    QuestService.completeQuest(player, questId)
                end
            end
        end
    end
end

if updated then
    DataManager.set(player, "activeQuests", playerQuests)
end

end

function QuestService.isQuestComplete(questId, progress) local quest = QuestDefinitions[questId]

for i, objective in ipairs(quest.objectives) do
    if progress.progress[i] < objective.count then
        return false
    end
end

return true

end

function QuestService.completeQuest(player, questId) local quest = QuestDefinitions[questId] local playerQuests = DataManager.get(player, "activeQuests")

-- Remove from active
playerQuests[questId] = nil
DataManager.set(player, "activeQuests", playerQuests)

-- Add to completed
local completedQuests = DataManager.get(player, "completedQuests") or {}
completedQuests[questId] = os.time()
DataManager.set(player, "completedQuests", completedQuests)

-- Grant rewards
for _, reward in ipairs(quest.rewards) do
    if reward.type == "currency" then
        DataManager.addCurrency(player, reward.currency, reward.amount)
    elseif reward.type == "experience" then
        LevelingService.addExperience(player, reward.amount)
    elseif reward.type == "item" then
        local inventory = DataManager.get(player, "inventory")
        InventoryService.addItem(inventory, reward.item, reward.quantity or 1)
    end
end

QuestCompleteRemote:FireClient(player, questId, quest.rewards)

end

Pet System

Pet Manager

local PetService = {}

local PetDefinitions = { cat_basic = {name = "Cat", rarity = "common", bonuses = {luck = 5}}, dog_basic = {name = "Dog", rarity = "common", bonuses = {speed = 5}}, dragon_epic = {name = "Dragon", rarity = "epic", bonuses = {damage = 20, luck = 10}} }

function PetService.createPet(petType) local def = PetDefinitions[petType] if not def then return nil end

return {
    id = HttpService:GenerateGUID(),
    type = petType,
    name = def.name,
    level = 1,
    experience = 0,
    equipped = false
}

end

function PetService.equipPet(player, petId) local pets = DataManager.get(player, "pets") or {}

-- Find the pet
local targetPet
for _, pet in ipairs(pets) do
    if pet.id == petId then
        targetPet = pet
    else
        pet.equipped = false  -- Unequip others
    end
end

if not targetPet then
    return false, "Pet not found"
end

targetPet.equipped = true
DataManager.set(player, "pets", pets)

-- Apply bonuses
PetService.applyBonuses(player, targetPet)

-- Spawn visual pet
PetService.spawnPetVisual(player, targetPet)

return true

end

function PetService.applyBonuses(player, pet) local def = PetDefinitions[pet.type] if not def then return end

local levelMultiplier = 1 + (pet.level - 1) * 0.1  -- 10% per level

for stat, value in pairs(def.bonuses) do
    local bonus = math.floor(value * levelMultiplier)
    player:SetAttribute("PetBonus_" .. stat, bonus)
end

end

function PetService.spawnPetVisual(player, pet) local character = player.Character if not character then return end

-- Remove existing pet visual
local existing = character:FindFirstChild("PetModel")
if existing then existing:Destroy() end

local def = PetDefinitions[pet.type]
local petModel = ReplicatedStorage.Pets[pet.type]:Clone()
petModel.Name = "PetModel"
petModel.Parent = character

-- Pet following behavior
task.spawn(function()
    local hrp = character:FindFirstChild("HumanoidRootPart")
    while petModel.Parent and hrp do
        local targetPos = hrp.Position + hrp.CFrame.RightVector * 3 + Vector3.new(0, 2, 0)
        local currentPos = petModel.PrimaryPart.Position

        local direction = (targetPos - currentPos)
        if direction.Magnitude > 0.5 then
            local newPos = currentPos + direction.Unit * math.min(direction.Magnitude, 0.5)
            petModel:PivotTo(CFrame.lookAt(newPos, hrp.Position))
        end

        task.wait()
    end
end)

end

Leveling System

Experience & Leveling

local LevelingService = {}

-- XP required for each level: level^2 * 100 local function getRequiredXP(level) return level * level * 100 end

function LevelingService.addExperience(player, amount) local currentLevel = DataManager.get(player, "level") or 1 local currentXP = DataManager.get(player, "experience") or 0

currentXP = currentXP + amount

-- Check for level ups
local levelsGained = 0
while currentXP >= getRequiredXP(currentLevel) do
    currentXP = currentXP - getRequiredXP(currentLevel)
    currentLevel = currentLevel + 1
    levelsGained = levelsGained + 1
end

DataManager.set(player, "level", currentLevel)
DataManager.set(player, "experience", currentXP)

if levelsGained > 0 then
    LevelingService.onLevelUp(player, currentLevel, levelsGained)
end

return currentLevel, currentXP, levelsGained

end

function LevelingService.onLevelUp(player, newLevel, levelsGained) -- Heal to full local character = player.Character if character then local humanoid = character:FindFirstChildOfClass("Humanoid") if humanoid then humanoid.Health = humanoid.MaxHealth end end

-- Grant stat points
local statPoints = DataManager.get(player, "statPoints") or 0
DataManager.set(player, "statPoints", statPoints + levelsGained * 3)

-- Unlock abilities
local unlocks = AbilityUnlocks[newLevel]
if unlocks then
    for _, abilityId in ipairs(unlocks) do
        AbilityService.unlock(player, abilityId)
    end
end

-- Visual/audio feedback
LevelUpRemote:FireClient(player, newLevel)

end

function LevelingService.getProgress(player) local level = DataManager.get(player, "level") or 1 local xp = DataManager.get(player, "experience") or 0 local required = getRequiredXP(level)

return {
    level = level,
    currentXP = xp,
    requiredXP = required,
    progress = xp / required
}

end

Daily Rewards

Daily Login System

local DailyRewardService = {}

local DAILY_REWARDS = { {type = "coins", amount = 100}, {type = "coins", amount = 150}, {type = "coins", amount = 200}, {type = "gems", amount = 5}, {type = "coins", amount = 300}, {type = "gems", amount = 10}, {type = "item", itemId = "rare_chest", amount = 1} }

function DailyRewardService.checkDailyReward(player) local lastLogin = DataManager.get(player, "lastLoginTime") or 0 local loginStreak = DataManager.get(player, "loginStreak") or 0

local now = os.time()
local lastLoginDate = os.date("*t", lastLogin)
local currentDate = os.date("*t", now)

-- Check if it's a new day
local isNewDay = lastLoginDate.yday ~= currentDate.yday or
                 lastLoginDate.year ~= currentDate.year

if not isNewDay then
    return nil, "Already claimed today"
end

-- Check if streak continues or resets
local hoursSinceLastLogin = (now - lastLogin) / 3600
if hoursSinceLastLogin > 48 then
    loginStreak = 0  -- Reset streak
end

loginStreak = loginStreak + 1
local rewardIndex = ((loginStreak - 1) % #DAILY_REWARDS) + 1
local reward = DAILY_REWARDS[rewardIndex]

-- Update data
DataManager.set(player, "lastLoginTime", now)
DataManager.set(player, "loginStreak", loginStreak)

-- Grant reward
if reward.type == "coins" or reward.type == "gems" then
    DataManager.addCurrency(player, reward.type, reward.amount)
elseif reward.type == "item" then
    local inventory = DataManager.get(player, "inventory")
    InventoryService.addItem(inventory, reward.itemId, reward.amount)
end

return {
    day = loginStreak,
    reward = reward
}

end

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

audio-system

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

optimization

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

animation-system

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

vfx-effects

No summary provided by upstream source.

Repository SourceNeeds Review