game-systems
π―Skillfrom taozhuo/game-dev-skills
Implements robust game systems like inventory, shops, trading, and progression mechanics for Roblox RPGs with secure, stackable item management.
Installation
npx skills add https://github.com/taozhuo/game-dev-skills --skill game-systemsSkill Details
Implements common game systems including inventory, shops, trading, quests, achievements, pets, crafting, and leveling. Use when building RPG mechanics, progression systems, or monetization features.
Overview
# Roblox Game Systems
When implementing game systems, follow these patterns for robust and exploiter-resistant mechanics.
Inventory System
Slot-Based Inventory
```lua
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
```lua
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
```lua
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
```lua
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
```lua
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
```lua
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
```lua
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
```
More from this repository10
Manages audio systems with advanced sound pooling, priority management, and performance optimization for immersive game sound experiences.
Generates dynamic visual effects like particle systems, camera shakes, and impact animations for enhancing game experiences in Roblox.
optimization skill from taozhuo/game-dev-skills
animation-system skill from taozhuo/game-dev-skills
Generates conceptual game art images using Gemini AI, providing precise style-specific prompts for character, environment, and UI design across various game aesthetics.
Enables comprehensive combat mechanics with advanced hitbox detection, damage calculation, and hit validation for creating dynamic player-vs-player and player-vs-NPC combat interactions.
security-anticheat skill from taozhuo/game-dev-skills
Provides expert guidance on navigating Roblox-specific API behaviors, performance optimizations, and common coding pitfalls in Lua scripting.
Manages reliable Roblox data persistence by implementing robust data loading, saving, caching, and error handling for player progress and game state.
Generates procedurally randomized content like terrain, dungeons, and cities using advanced noise algorithms and algorithmic techniques.