Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/brockmartin/roblox-game-skill/llms.txt

Use this file to discover all available pages before exploring further.

RPGs hook players through the promise of perpetual forward progress. Every session should leave the character measurably stronger — more XP, better gear, a completed quest, a newly unlocked zone. This template provides the structural backbone of a Blox Fruits-style adventure: a server-authoritative character stats system with a leveling XP curve and stat point allocation, a quest pipeline from accept through turn-in, NPC AI with a dialog tree system supporting branching choices and quest actions, and level-gated zone entry enforced on the server. All stat mutations are server-side — no client can inflate its own numbers.

Core Systems

Character Stats & Leveling

HP, MP, STR, DEF, SPD, Level, XP, StatPoints. XP follows a floor(100 × 1.5^(level-1)) curve. Players earn 3 stat points per level to allocate freely.

Quest System

kill, collect, and talk objective types. Quest state (available/active/completed) stored per player. Turn-in validates completion server-side and grants XP and gold rewards.

NPC AI & Dialog Trees

Dialog node graphs with sequential lines and branching DialogChoice arrays. Choices trigger offerQuest, turnInQuest, or openShop actions. Sessions tracked server-side.

Level-Gated Zones

RequestZoneEntry remote validated against ZoneDefinitions.levelRange.min. Players below the required level are denied with a minimum-level hint.

Combat System

Damage formula: max(1, floor((baseDamage + STR × 1.5) - DEF × 0.8)). Server-authoritative hit detection with cooldown enforcement.

Data Persistence

Per-player stats, quest progress, and inventory stored via ProfileService. Stats replicated to client via StatsUpdated remote on every mutation.

Key Code: Character Stats & Leveling

The CharacterStats shared module defines the XP formula and all stat mutation logic. The CharacterStatsService server script holds authoritative state and exposes a grantXP API for quest and combat systems to call.
-- ReplicatedStorage/Modules/CharacterStats.lua

local CharacterStats = {}

-- XP required to reach the next level from the current level
-- Formula: floor(100 * 1.5 ^ (level - 1))
function CharacterStats.xpToNextLevel(level: number): number
    return math.floor(100 * 1.5 ^ (level - 1))
end

-- Process XP gain. Returns true if at least one level-up occurred.
function CharacterStats.addXP(stats: CharacterData, amount: number): boolean
    assert(amount > 0, "XP amount must be positive")

    stats.XP += amount
    local leveled = false

    while stats.XP >= CharacterStats.xpToNextLevel(stats.Level) do
        stats.XP -= CharacterStats.xpToNextLevel(stats.Level)
        stats.Level += 1
        leveled = true

        -- Apply base stat growth
        stats.MaxHP += 10   -- +10 MaxHP per level
        stats.MaxMP += 5    -- +5 MaxMP per level

        -- Grant stat points for allocation
        stats.StatPoints += 3

        -- Heal to full on level up
        stats.HP = stats.MaxHP
        stats.MP = stats.MaxMP
    end

    return leveled
end

-- Allocate a stat point. Returns true on success.
local ALLOCATABLE_STATS = { STR = true, DEF = true, SPD = true, MaxHP = true, MaxMP = true }

function CharacterStats.allocateStat(stats: CharacterData, statName: string): boolean
    if stats.StatPoints <= 0 then return false end
    if not ALLOCATABLE_STATS[statName] then return false end

    stats[statName] += 1
    stats.StatPoints -= 1
    return true
end

-- Calculate physical damage dealt
function CharacterStats.calculateDamage(attackerSTR: number, defenderDEF: number, baseDamage: number): number
    local raw = baseDamage + (attackerSTR * 1.5)
    local reduction = defenderDEF * 0.8
    return math.max(1, math.floor(raw - reduction))
end

Key Code: Quest System

The QuestDefinitions module defines all quests with objectives and rewards. The QuestService server script manages per-player quest state and validates all progress and turn-in requests.
-- ReplicatedStorage/Modules/QuestDefinitions.lua (excerpt)

QuestDefinitions.quests = {
    ["quest_first_hunt"] = {
        id = "quest_first_hunt",
        name = "First Hunt",
        description = "Prove your worth by hunting slimes in the Starter Meadow.",
        levelRequired = 1,
        giverNPC = "Elder Moran",
        turnInNPC = "Elder Moran",
        objectives = {
            {
                type = "kill",
                target = "Slime",
                amount = 5,
                description = "Defeat 5 Slimes",
            },
        },
        rewards = { xp = 150, gold = 50 },
    },

    ["quest_herb_gathering"] = {
        id = "quest_herb_gathering",
        name = "Herb Gathering",
        description = "Collect healing herbs for the village healer.",
        levelRequired = 2,
        giverNPC = "Healer Aria",
        turnInNPC = "Healer Aria",
        objectives = {
            {
                type = "collect",
                target = "HealingHerb",
                amount = 3,
                description = "Collect 3 Healing Herbs",
            },
        },
        rewards = { xp = 200, gold = 75, items = { "potion_small" } },
        prerequisiteQuestId = "quest_first_hunt",
    },
}

Key Code: NPC Dialog Trees

Dialog trees are node graphs with sequential lines and optional branching choices. The server tracks which node each player is on, validates choice indices, and fires offerQuest or turnInQuest actions.
-- ServerScriptService/DialogService.lua (excerpt)

-- Player selects a dialog choice
selectChoiceRemote.OnServerEvent:Connect(function(player: Player, choiceIndex: unknown)
    if typeof(choiceIndex) ~= "number" then return end

    local session = activeSessions[player]
    if not session then return end

    local currentNode = session.tree.nodes[session.currentNodeId]
    if not currentNode or not currentNode.choices then return end

    local choice = currentNode.choices[choiceIndex]
    if not choice then return end

    -- Handle action from the choice
    if choice.action then
        handleAction(player, choice.action)
    end

    -- Advance to next node or end
    if choice.nextNodeId then
        local nextNode = session.tree.nodes[choice.nextNodeId]
        if nextNode then
            session.currentNodeId = choice.nextNodeId

            if nextNode.action then
                handleAction(player, nextNode.action)
            end

            dialogNodeRemote:FireClient(player, nextNode)

            if not nextNode.choices and not nextNode.nextNodeId then
                task.delay(0, function()
                    activeSessions[player] = nil
                    dialogEndRemote:FireClient(player)
                end)
            end
        else
            activeSessions[player] = nil
            dialogEndRemote:FireClient(player)
        end
    else
        activeSessions[player] = nil
        dialogEndRemote:FireClient(player)
    end
end)

Key Code: Zone Entry Validation

Zone entry is fully server-authoritative. The client fires RequestZoneEntry with a zone ID — the server checks the player’s level against levelRange.min before teleporting.
-- ServerScriptService/ZoneService.lua

requestZoneEntryRemote.OnServerEvent:Connect(function(player: Player, zoneId: unknown)
    if typeof(zoneId) ~= "string" then return end

    local zoneDef = ZoneDefinitions.zones[zoneId]
    if not zoneDef then return end

    local stats = CharacterStatsService.getStats(player)
    if not stats then return end

    if stats.Level < zoneDef.levelRange.min then
        zoneEntryResultRemote:FireClient(player, false, "denied", zoneDef.levelRange.min)
        return
    end

    -- Teleport player to zone spawn
    local character = player.Character
    if character then
        local root = character:FindFirstChild("HumanoidRootPart")
        if root then
            root.CFrame = CFrame.new(zoneDef.spawnPosition)
        end
    end

    zoneEntryResultRemote:FireClient(player, true, zoneDef.id, nil)
end)

Architecture

ScriptPurpose
CharacterStatsService.luauAuthoritative HP/MP/STR/DEF/SPD/Level/XP storage; grantXP, takeDamage API
QuestService.luauQuest state machine per player: available → active → completed
NPCService.luauNPC spawning, AI state machine, loot drops on death
DialogService.luauDialog session tracking, node advancement, choice validation
ZoneService.luauLevel-gate enforcement, zone teleportation
CombatService.luauHit detection, damage calculation with stat scaling, cooldowns

XP Curve Reference

LevelXP to NextCumulative XP
11000
3225250
5506862
103,8448,488
1529,19366,430
20221,689509,607
Formula: XP_needed = floor(100 × 1.5^(level - 1))
NPC performance: Never update all NPCs every frame. Rotate through a subset each Heartbeat (e.g., update 10 NPCs per tick at 10 Hz). Despawn NPC models when no player is within 200 studs — respawn when a player re-enters range. Keep active NPC count per zone capped at 30.

How to Use This Template

Build me a samurai RPG. Players start in a cherry blossom village and fight bandits
and demons across 3 zones. Add a katana skill tree, a monk NPC that gives meditation
quests for MP buffs, and a boss demon in zone 3 that drops a legendary blade.
See the New Game workflow for the complete 8-step build process.

Build docs developers (and LLMs) love