Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Services
- local TweenService = game:GetService("TweenService") -- Tweening
- local RunService = game:GetService("RunService") -- Provides heartbeat event for continuous updates (conveyors)
- local PhysicsService = game:GetService("PhysicsService")-- Manages collision groups safely
- local DataStoreService = game:GetService("DataStoreService") -- Persists player cash between sessions
- local Players = game:GetService("Players") -- Manages player connections and stats
- local ReplicatedStorage = game:GetService("ReplicatedStorage") -- Stores shared assets and remotes
- -- Constants and configuration
- local DATASTORE_KEY = "CashStore" -- Identifier for our player datastore
- local BELT_SPEED = 10 -- Speed at which conveyor belts move parts
- local TWEEN_INFO = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)
- -- Prices used for placement validation and refunds
- local PRICES = {
- AsphaltDropper = 15,
- BasaltDropper = 70,
- BrickDropper = 350,
- CardboardDropper = 1750,
- CarpetDropper = 9000,
- StraightConveyor = 15,
- RCurvedConveyor = 100,
- LCurvedConveyor = 100,
- TShapedConveyor = 200,
- Sell = 70
- }
- -- Refund costs when deleting builds
- local COSTS = {
- AsphaltDropper = 15,
- BasaltDropper = 70,
- BrickDropper = 350,
- CardboardDropper = 1750,
- CarpetDropper = 9000,
- StraightConveyor = 15,
- RCurvedConveyor = 100,
- LCurvedConveyor = 100,
- TShapedConveyor = 200,
- Sell = 70,
- UpgradingConveyor = 1000000
- }
- -- How much each block pays out when sold
- local PAYOUTS = {
- [Enum.Material.Asphalt] = 1,
- [Enum.Material.Basalt] = 5,
- [Enum.Material.Brick] = 20,
- [Enum.Material.Cardboard] = 100,
- [Enum.Material.Carpet] = 500
- }
- -- Collision grouping names for separation of parts in physics engine
- local BLOCK_GROUP = "BlockGroup"
- local UPGRADE_GROUP = "UpgradeGroup"
- -- Remote events and shared assets
- local BuildModels = ReplicatedStorage:WaitForChild("BuildModels")
- local PlaceRemote = ReplicatedStorage:WaitForChild("PlaceBuildRemote")
- local DeleteRemote = ReplicatedStorage:WaitForChild("DeleteBuildRemote")
- -- Workspace references
- local Baseplate = workspace:WaitForChild("Baseplate")
- -- In-memory tracking of active conveyor belts and grid placements per player
- local activeBelts = {} -- List of belt parts for movement updates
- local placedGridByPlayer = {} -- Tracks which grid cells are occupied by each player
- -- Initialize datastore handle
- local dataStore = DataStoreService:GetDataStore(DATASTORE_KEY)
- -- Safely register collision groups once; protect against repeated calls
- pcall(function() PhysicsService:RegisterCollisionGroup(BLOCK_GROUP) end)
- pcall(function() PhysicsService:RegisterCollisionGroup(UPGRADE_GROUP) end)
- -- Define non-collidable relationships: blocks don't collide with each other or upgrades
- PhysicsService:CollisionGroupSetCollidable(BLOCK_GROUP, BLOCK_GROUP, false)
- PhysicsService:CollisionGroupSetCollidable(BLOCK_GROUP, UPGRADE_GROUP, false)
- PhysicsService:CollisionGroupSetCollidable(UPGRADE_GROUP, UPGRADE_GROUP, true)
- -- Helper: create a Tween to move a part by a vertical offset
- local function createTween(part, offsetY)
- -- Use part's current position as base and move by offsetY
- local targetPosition = part.Position + Vector3.new(0, offsetY, 0)
- return TweenService:Create(part, TWEEN_INFO, {Position = targetPosition})
- end
- -- Helper: spawn a new block just above an original dropper block
- local function spawnBlock(originalBlock)
- if not originalBlock or not originalBlock:IsA("BasePart") then return end -- Guard invalid input
- local clone = originalBlock:Clone() -- Duplicate the template block
- clone.Transparency = 0 -- Ensure block is visible
- clone.Anchored = false -- Allow physics simulation
- -- Calculate vertical offset so new block rests above original
- local offsetY = (originalBlock.Size.Y + clone.Size.Y) / 2 + 1
- clone.CFrame = originalBlock.CFrame * CFrame.new(0, offsetY, 0)
- clone.CollisionGroup = BLOCK_GROUP -- Assign collision group to avoid self-collisions
- clone.Parent = workspace -- Insert into world for physics and rendering
- end
- -- Starts pump animation and block spawning loop
- local function animatePump(pumpPart, blockTemplate)
- if not pumpPart or not blockTemplate then return end -- Guard invalid parameters
- -- Run a self-contained loop for this pump
- coroutine.wrap(function()
- while true do
- -- Verify pump and block still exist in world
- if not pumpPart:IsDescendantOf(workspace) then break end
- if not blockTemplate:IsDescendantOf(workspace) then break end
- createTween(pumpPart, 1.25):Play() -- Move pump up
- wait(0.5)
- createTween(pumpPart, -1.25):Play() -- Move pump down
- wait(0.5)
- spawnBlock(blockTemplate) -- generate a block
- end
- end)()
- end
- -- Destroys any dropped blocks touching the baseplate to avoid performance issues
- local function onBaseplateTouched(otherPart)
- if otherPart:IsA("BasePart") and otherPart.Name == "Block" then
- otherPart:Destroy() -- Remove old blocks immediately
- end
- end
- Baseplate.Touched:Connect(onBaseplateTouched)
- -- Registers a belt part for movement updates and sets its properties
- local function registerBelt(beltPart)
- if not beltPart or not beltPart:IsA("BasePart") then return end -- Guard invalid input
- beltPart.CanTouch = true -- Enable detection of parts on belt
- table.insert(activeBelts, beltPart)
- end
- -- Heartbeat event handler: moves all parts touching each registered belt
- local function conveyorHeartbeat()
- -- Remove belts that no longer exist
- for i = #activeBelts, 1, -1 do
- if not activeBelts[i]:IsDescendantOf(workspace) then
- table.remove(activeBelts, i)
- end
- end
- -- For each active belt, push touching parts forward
- for _, belt in ipairs(activeBelts) do
- local direction = belt.CFrame.LookVector.Unit * BELT_SPEED
- local partsOnBelt = workspace:GetPartsInPart(belt)
- for _, part in ipairs(partsOnBelt) do
- if part:IsA("BasePart") and not part.Anchored and part ~= belt then
- part.Velocity = Vector3.new(direction.X, part.Velocity.Y, direction.Z)
- end
- end
- end
- end
- RunService.Heartbeat:Connect(conveyorHeartbeat)
- -- Attaches sell logic so blocks pay out and get destroyed
- local function setupSellPart(sellPart)
- if not sellPart or not sellPart:IsA("BasePart") then return end -- Guard invalid input
- local function handleSellTouch(otherPart)
- if otherPart.Name ~= "Block" then return end -- Only process blocks
- local basePayout = PAYOUTS[otherPart.Material] or 0 -- Lookup material value
- if basePayout <= 0 then return end -- No payout means ignore
- if otherPart:GetAttribute("Upgraded") then -- Double payout if upgraded
- basePayout = basePayout * 2
- end
- otherPart:Destroy() -- Remove block from world
- for _, player in ipairs(Players:GetPlayers()) do
- local leaderstats = player:FindFirstChild("leaderstats")
- if leaderstats then
- local cashValue = leaderstats:FindFirstChild("Cash")
- if cashValue then cashValue.Value = cashValue.Value + basePayout end
- end
- end
- end
- sellPart.Touched:Connect(handleSellTouch)
- end
- -- Attaches upgrade logic to any 'UpgradingPart' so blocks get marked and colored
- local function setupUpgradePart(upgradePart)
- if not upgradePart or not upgradePart:IsA("BasePart") then return end -- Guard invalid input
- local function handleUpgradeTouch(otherPart)
- if otherPart.Name ~= "Block" then return end -- Only blocks
- if otherPart:GetAttribute("Upgraded") then return end -- Skip if already upgraded
- otherPart:SetAttribute("Upgraded", true) -- Mark as upgraded
- otherPart.Color = Color3.new(0, 1, 0) -- Visual indicator (green)
- end
- upgradePart.Touched:Connect(handleUpgradeTouch)
- end
- -- Scans workspace for relevant parts/models at startup and sets them up
- local function scanWorkspaceItem(item)
- if item:IsA("BasePart") then
- if item.Name == "SellPart" then setupSellPart(item) end
- if item.Name == "Belt" then registerBelt(item) end
- if item.Name == "UpgradingPart" then setupUpgradePart(item) end
- return
- end
- if item:IsA("Model") then
- local pump = item:FindFirstChild("Pump")
- local blockTemplate = item:FindFirstChild("Block")
- if pump then animatePump(pump, blockTemplate) end
- end
- end
- for _, descendant in ipairs(workspace:GetDescendants()) do
- scanWorkspaceItem(descendant)
- end
- -- Listen for new items added to workspace and configure them
- workspace.DescendantAdded:Connect(scanWorkspaceItem)
- -- load cash when they join
- local function onPlayerAdded(player)
- local statsFolder = Instance.new("Folder")
- statsFolder.Name = "leaderstats"
- statsFolder.Parent = player
- local cashValue = Instance.new("IntValue")
- cashValue.Name = "Cash"
- -- Initialize with stored cash or zero
- cashValue.Value = dataStore:GetAsync(player.UserId) or 0
- cashValue.Parent = statsFolder
- end
- Players.PlayerAdded:Connect(onPlayerAdded)
- -- Save player cash on leave
- local function onPlayerRemoving(player)
- local stats = player:FindFirstChild("leaderstats")
- if not stats then return end -- No stats means nothing to save
- local cashValue = stats:FindFirstChild("Cash")
- if cashValue then dataStore:SetAsync(player.UserId, cashValue.Value) end
- end
- Players.PlayerRemoving:Connect(onPlayerRemoving)
- -- Handles placement requests with guard clauses
- local function onPlaceRequested(player, modelName, cframe, cost)
- local leaderstats = player:FindFirstChild("leaderstats") or {} -- Guard missing stats
- local cashValue = leaderstats and leaderstats:FindFirstChild("Cash")
- if not cashValue or cashValue.Value < cost then return end -- Insufficient funds guard
- local template = BuildModels:FindFirstChild(modelName)
- if not template then return end -- Invalid model guard
- cashValue.Value = cashValue.Value - cost -- Deduct cost
- local modelClone = template:Clone()
- modelClone:PivotTo(cframe) -- Position clone
- -- Configure each part of the clone
- for _, part in ipairs(modelClone:GetDescendants()) do
- if not part:IsA("BasePart") then continue end
- part.Transparency = 0
- part.CanCollide = true
- if part.Name == "UpgradingPart" then
- part.Transparency = 0.5
- part.CollisionGroup = UPGRADE_GROUP
- elseif part.Name == "Block" then
- part.Transparency = 1 -- Hidden until spawned
- end
- end
- modelClone.Parent = workspace
- end
- PlaceRemote.OnServerEvent:Connect(onPlaceRequested)
- -- Handles deletion with refund and optional grid freeing
- local function onDeleteRequested(player, model)
- if not model or not model:IsA("Model") then return end -- Guard invalid input
- local leaderstats = player:FindFirstChild("leaderstats") or {}
- local cashValue = leaderstats and leaderstats:FindFirstChild("Cash")
- if not cashValue then return end -- Guard missing stats
- local refundAmount = COSTS[model.Name] or 0
- cashValue.Value = cashValue.Value + refundAmount -- Refund to player
- model:Destroy() -- Remove model
- end
- DeleteRemote.OnServerEvent:Connect(onDeleteRequested)
- -- Handles deletion with grid freeing refunds and clears grid cells
- local function onDeleteWithGrid(player, model, gridPos)
- if not model:IsDescendantOf(workspace) then return end -- Guard: model must be in world
- local leaderstats = player:FindFirstChild("leaderstats") or {}
- local cashValue = leaderstats and leaderstats:FindFirstChild("Cash")
- if not cashValue then return end -- Guard: ensure stats exist
- local refund = PRICES[model.Name] or 0 -- Use placement price for refund
- cashValue.Value = cashValue.Value + refund -- Refund to player
- -- Free up grid position if tracked
- if placedGridByPlayer[player] then
- placedGridByPlayer[player][gridPos] = nil
- end
- model:Destroy()
- end
- DeleteRemote.OnServerEvent:Connect(onDeleteWithGrid)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement