A full RPG template with character stats and leveling, quest system, NPC AI with dialog trees, zone gating, and combat mechanics for your adventure game.
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.
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.
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.lualocal 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 leveledend-- 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 trueend-- Calculate physical damage dealtfunction 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
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", },}
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 choiceselectChoiceRemote.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) endend)
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.luarequestZoneEntryRemote.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)
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.
Build me a samurai RPG. Players start in a cherry blossom village and fight banditsand demons across 3 zones. Add a katana skill tree, a monk NPC that gives meditationquests for MP buffs, and a boss demon in zone 3 that drops a legendary blade.