🎯

game-systems

🎯Skill

from taozhuo/game-dev-skills

VibeIndex|
What it does

Implements robust game systems like inventory, shops, trading, and progression mechanics for Roblox RPGs with secure, stackable item management.

game-systems

Installation

Install skill:
npx skills add https://github.com/taozhuo/game-dev-skills --skill game-systems
2
AddedJan 27, 2026

Skill Details

SKILL.md

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

🎯
audio-system🎯Skill

Manages audio systems with advanced sound pooling, priority management, and performance optimization for immersive game sound experiences.

🎯
vfx-effects🎯Skill

Generates dynamic visual effects like particle systems, camera shakes, and impact animations for enhancing game experiences in Roblox.

🎯
optimization🎯Skill

optimization skill from taozhuo/game-dev-skills

🎯
animation-system🎯Skill

animation-system skill from taozhuo/game-dev-skills

🎯
gemini-image-prompting🎯Skill

Generates conceptual game art images using Gemini AI, providing precise style-specific prompts for character, environment, and UI design across various game aesthetics.

🎯
combat-system🎯Skill

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

security-anticheat skill from taozhuo/game-dev-skills

🎯
roblox-api-patterns🎯Skill

Provides expert guidance on navigating Roblox-specific API behaviors, performance optimizations, and common coding pitfalls in Lua scripting.

🎯
data-persistence🎯Skill

Manages reliable Roblox data persistence by implementing robust data loading, saving, caching, and error handling for player progress and game state.

🎯
procedural-generation🎯Skill

Generates procedurally randomized content like terrain, dungeons, and cities using advanced noise algorithms and algorithmic techniques.