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.

Tycoons are one of Roblox’s most enduring genres because the loop is deeply satisfying at a visual level — players literally watch their base grow one building at a time. Currency flows in from droppers, gets converted at collectors, and gets spent on the next structure in a chain. Every purchase is a tangible, visible change to the world. This template provides the full server-authoritative infrastructure: plot claiming, a dropper spawning loop, collector touch detection, a button pad purchase system with prerequisite gating, and passive income tracking — all wired together with a clean init/start lifecycle to avoid circular dependencies.

Core Systems

Plot Claiming

Auto-assigns a tycoon plot to each player on join. Walk-on claiming via ClaimPad is also supported. Plots are released and cleaned up on player leave.

Dropper / Collector Pipeline

Dropper loops spawn physics parts on configurable intervals. Parts travel via conveyors to collectors, which award currency on touch. Per-plot part cap prevents overload.

Button Pad Purchasing

Server-side Touched detection on ButtonPads. Validates ownership, prerequisites, and currency before spawning the building model and enabling downstream pads.

Income Manager

Tracks per-player currency, multipliers, and passive income sources. Syncs to client HUD via CurrencyUpdate remote on every change.

Upgrade Tree

Linear-chain unlock dependencies enforce the intended build order. Each building lists a prerequisite field — pads remain invisible until prerequisites are met.

Prestige System

Optional reset-for-multiplier system. Destroys all buildings, zeros currency, applies a permanent +25% income multiplier per prestige level, and shows a counter on the plot.

Data Flow

[Dropper] --spawns Part--> [Conveyor] --moves Part--> [Collector] --fires event-->
[IncomeManager] --adds currency--> [Player Data] --fires remote--> [Client HUD]

Key Code: Plot Assignment

The PlotSystem handles plot ownership, building anchor positions, and cleanup. The autoAssign function is called on PlayerAdded to immediately give every player a plot.
-- ServerScriptService/Services/PlotSystem.lua

function PlotSystem.claimPlot(player: Player, plotIndex: number): boolean
    -- Already owns a plot
    if playerPlots[player] then return false end

    local plot = plots[plotIndex]
    if not plot then return false end

    -- Plot already claimed
    if plot.owner then return false end

    -- Claim it
    plot.owner = player
    plot.ownerId = player.UserId
    playerPlots[player] = plotIndex

    -- Visual feedback: change claim pad color or hide it
    local claimPad = plot.model:FindFirstChild("ClaimPad")
    if claimPad then
        claimPad.Transparency = 1
        claimPad.CanCollide = false
    end

    -- Set ownership label
    local billboardGui = plot.model:FindFirstChild("OwnerLabel")
    if billboardGui and billboardGui:IsA("BillboardGui") then
        local label = billboardGui:FindFirstChildOfClass("TextLabel")
        if label then
            label.Text = player.DisplayName .. "'s Tycoon"
        end
    end

    -- Teleport player to their plot spawn
    local character = player.Character
    if character and character.PrimaryPart then
        character:PivotTo(plot.spawnCFrame)
    end

    print("[PlotSystem] " .. player.Name .. " claimed Plot" .. plotIndex)
    return true
end

Key Code: Button Pad Purchase

When a player steps on a pad, the server validates ownership, prerequisites, and balance in a single debounced handler before spawning the building and unlocking downstream pads.
-- ServerScriptService/Services/ButtonPadManager.lua

function ButtonPadManager._onPadTouched(
    plotOwner: Player,
    pad: BasePart,
    buildingId: string,
    hit: BasePart
)
    -- Debounce check
    if purchaseDebounce[plotOwner] then return end

    -- Identify who touched the pad
    local character = hit.Parent
    if not character then return end
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid or humanoid.Health <= 0 then return end
    local touchPlayer = Players:GetPlayerFromCharacter(character)
    if not touchPlayer then return end

    -- Only the plot owner can purchase on their plot
    if touchPlayer ~= plotOwner then return end

    -- Already purchased this building
    if playerBuildings[plotOwner] and playerBuildings[plotOwner][buildingId] then return end

    local buildingData = TycoonConfig.Buildings[buildingId]
    if not buildingData then return end

    -- Check prerequisite
    if buildingData.prerequisite then
        if not playerBuildings[plotOwner][buildingData.prerequisite] then
            return -- prerequisite not met
        end
    end

    -- Check currency
    local playerCurrency = IncomeManager.getCurrency(plotOwner)
    if playerCurrency < buildingData.cost then return end

    -- Set debounce
    purchaseDebounce[plotOwner] = true

    -- Deduct currency and mark purchased
    IncomeManager.addCurrency(plotOwner, -buildingData.cost)
    playerBuildings[plotOwner][buildingId] = true

    -- Spawn the building model
    ButtonPadManager._spawnBuilding(plotOwner, buildingId, buildingData, pad)

    -- Disable the pad
    pad.Transparency = 1
    pad.CanCollide = false

    -- If this building is a dropper, register it
    if buildingData.incomePerDrop > 0 and buildingData.dropInterval > 0 then
        DropperSystem.registerDropper(plotOwner, buildingId, buildingData)
    end

    -- Reveal any pads whose prerequisite is this building
    ButtonPadManager._refreshPadVisibility(plotOwner)

    -- Release debounce after short delay
    task.delay(0.5, function()
        purchaseDebounce[plotOwner] = nil
    end)
end

Key Code: Dropper Loop

Each registered dropper runs its own task.spawn loop, spawning parts at the configured interval and storing the income value as a part attribute so the collector can award the right amount.
-- ServerScriptService/Services/DropperSystem.lua

function DropperSystem._dropperLoop(
    player: Player,
    buildingId: string,
    dropperInfo: any
)
    local buildingData = dropperInfo.buildingData

    while dropperInfo.active do
        task.wait(buildingData.dropInterval)

        if not dropperInfo.active then break end
        if not player.Parent then break end -- player left

        -- Performance cap: don't spawn more parts if at limit
        local currentCount = playerPartCount[player] or 0
        if currentCount >= TycoonConfig.Dropper.maxPartsPerPlot then
            continue
        end

        -- Find the dropper model to get spawn position
        local plotModel = PlotSystem.getPlotModel(player)
        if not plotModel then break end

        local buildingsFolder = plotModel:FindFirstChild("Buildings")
        if not buildingsFolder then continue end

        local dropperModel = buildingsFolder:FindFirstChild(buildingId)
        if not dropperModel then continue end

        local dropPoint = dropperModel:FindFirstChild("DropPoint", true)
        if not dropPoint then continue end

        local spawnPosition: Vector3
        if dropPoint:IsA("Attachment") then
            spawnPosition = dropPoint.WorldPosition
        elseif dropPoint:IsA("BasePart") then
            spawnPosition = dropPoint.Position
        else
            continue
        end

        -- Spawn the dropper part
        local part = Instance.new("Part")
        part.Name = "DropperPart"
        part.Size = TycoonConfig.Dropper.partSize
        part.Color = TycoonConfig.Dropper.partColor
        part.Material = Enum.Material.Neon
        part.Position = spawnPosition
        part.Anchored = false
        part.CanCollide = true
        part.Parent = buildingsFolder

        part:SetAttribute("IncomeValue", buildingData.incomePerDrop)
        part:SetAttribute("OwnerUserId", player.UserId)

        -- Auto-destroy after lifetime
        Debris:AddItem(part, TycoonConfig.Dropper.partLifetime)

        -- Track part count
        playerPartCount[player] = (playerPartCount[player] or 0) + 1
        part.Destroying:Connect(function()
            local count = playerPartCount[player]
            if count then
                playerPartCount[player] = math.max(0, count - 1)
            end
        end)
    end
end

Architecture

ScriptPurpose
TycoonServer.server.luauBootstrapper — creates remotes, inits all modules, connects player lifecycle
Services/PlotSystem.luauPlot claiming, boundary tracking, building anchor positions, plot release
Services/DropperSystem.luauPer-dropper spawn loops, part lifetime tracking, collector setup
Services/ButtonPadManager.luauTouch detection, prerequisite validation, building spawn, pad visibility
Services/IncomeManager.luauCurrency storage, multipliers, passive income loops, client HUD sync
Services/PrestigeSystem.luauEarnings threshold check, building reset, multiplier grant

Progression Tiers

TierCost RangeIncome Per DropTime to Unlock
Tier 1 (Starter)100100–50055–100–10 minutes
Tier 2 (Mid-game)2,5002,500–15,0002525–7515–45 minutes
Tier 3 (Late-game)50,00050,000–250,000150150–5001–3 hours
Tier 4 (End-game)500,000500,000–2,000,0001,0001,000–5,0003–8 hours
The dropper part problem: With 8 plots each running 3 droppers at 1-second intervals, that is 24 new physics parts per second. Without mitigation, this will tank frame rates on mobile. The template enforces a per-plot part cap (maxPartsPerPlot = 50), auto-destroys parts via Debris:AddItem after 15 seconds, and the Auto-Collect GamePass bypasses part physics entirely by ticking currency directly.

How to Use This Template

To scaffold a new tycoon game, describe your concept and the skill applies this template:
Build me a pizza restaurant tycoon. Players buy pizza ovens that drop pizza boxes,
conveyors carry boxes to the register, and customers pay automatically.
Add a delivery upgrade that doubles income at Tier 2.
See the New Game workflow for the complete 8-step build process.

Build docs developers (and LLMs) love