-
Notifications
You must be signed in to change notification settings - Fork 468
Open
Labels
bugSomething isn't workingSomething isn't workingnew solverThis issue is specific to the new solver.This issue is specific to the new solver.
Description
Type error is being caused by line 721 on leaderstats[name]:
leaderstats[name] = BigNumSub(leaderstats[name], value)
leaderstats's type shows as [intersect<a, ~nil>]: *blocked-4472478*
, if i set the type manually, the type error goes away.
Full script:
--* [[----------< Constants >----------]]
local COST_TYPES = {
Dropper1 = `Cash`,
Dropper2 = `Ore`,
Dropper3 = `Gems`,
Valuator = `Ore`,
Yielder = `Cash`
}
local MAX_UPGRADE_LEVEL = 500
local UPGRADER_MULTIPLIER_INCREASE = `0.05`
local ORE_RARITIES = {79.59, 12.5, 7.5, 0.4, 0.01} -- regular, shiny, gem, rainbow, void
local ORE_TIERS = {`Regular`, `Shiny`, `Gem`, `Rainbow`, `Void`}
local ORE_MULTIPLIERS = {Regular = 1, Shiny = 2.5, Gem = nil, Rainbow = 100, Void = 1000} -- gem nil since it only gives gems
local ORE_EXPIRE_TIME = 15
local UPGRADE_MILESTONE_REQUIREMENT = 25
local UPGRADE_COST_SCALAR_DROPPER = `25`
local UPGRADE_COST_SCALAR_UPGRADER = `100`
-- Cost curve base (keeps linear-ish feel) + multiplicative growth factor per 25 levels
local COST_CURVE_EXP_DROPPER = 1.15
local COST_CURVE_EXP_UPGRADER = 1.35
local COST_CURVE_FACTOR_DROPPER = `1.15`
local COST_CURVE_FACTOR_UPGRADER = `2.0`
-- Multiplicative r(L): r = clamp(BASE + step * floor((L-1)/25), BASE, MAX)
local COST_R_BASE_DROPPER = 1.12
local COST_R_STEP_DROPPER = 0.01
local COST_R_MAX_DROPPER = 1.22
local COST_R_BASE_UPGRADER = 1.16
local COST_R_STEP_UPGRADER = 0.012
local COST_R_MAX_UPGRADER = 1.26
-- Diminishing per-upgrade increments at milestones (no more big spikes)
local INCREMENT_DECAY_DROPPER = `0.985`
local PRICE_OF_FIRST_UPGRADE = `1`
local EARLY_EASING_MAX_LEVEL = 25
local EARLY_EASING_SCALAR_PER_LEVEL = `1.1`
local EARLY_EASING_CURVE_EXPONENT = 1.05
-- Use numeric type for number math (was string before)
local DROP_SPEED_DECREMENT_PER_MILESTONE = 0.05
local MINIMUM_DROP_SPEED = 1
local BUTTON_BUY_INTERVAL = 0.33
local SAVE_INTERVAL_SECONDS = 180 -- data will save on disconnect anyway, this is just an extra safety measure
local MINIMUM_TIME_BETWEEN_SAVES = 10
--* [[----------< Services >----------]]
local Players = game:GetService(`Players`)
local ReplicatedStorage = game:GetService(`ReplicatedStorage`)
local CollectionService = game:GetService(`CollectionService`)
local ServerStorage = game:GetService(`ServerStorage`)
local r = Random
local RNG = r.new()
--* [[----------< Modules >----------]]
local Modules = ReplicatedStorage.Modules
local BigNumber = require(Modules.BigNumber)
local Debounce = require(Modules.Debouncer)
local PlaySoundEffect = require(Modules.PlaySoundEffect)
local Announce = require(Modules.Announce)
--* [[----------< Remotes >----------]]
local Remotes = ReplicatedStorage:WaitForChild(`Remotes`)
local NotifyRemote = Remotes.Notify
local BuildRemote = Remotes.BuildObject
local ButtonPressRemote = Remotes.ButtonPress
local AnimateUpgradeRemote = Remotes.AnimateUpgrade
local ProcessOreRemote = Remotes.ProcessOre
local OreRarityEffectsRemote = Remotes.DoOreRarityEffects
local BuyFactoryRemote = Remotes.BuyFactory
local Factories: Folder = workspace:WaitForChild(`Factories`)
local OreStorage: Folder = Factories:WaitForChild(`OreStorage`)
local SharedPrefabs: Folder = ReplicatedStorage:WaitForChild(`Prefabs`)
local ServerPrefabs: Folder = ServerStorage:WaitForChild(`Prefabs`)
local OresFolder = ServerPrefabs:WaitForChild(`Ores`)
local OreRarityEffects: Folder = SharedPrefabs:WaitForChild(`OreRarityEffects`)
--* [[----------< Other >----------]]
local max, floor = math.max, math.floor
local BigNumAdd, BigNumSub, BigNumMax, BigNumMul = BigNumber.Add, BigNumber.Subtract, BigNumber.Max, BigNumber.Multiply
local NextNumber = RNG.NextNumber
local server_data_ready = false
local module = {}
function module:Run(core)
--* [[----------< Helper Functions >----------]]
-- Simple safe connect wrapper so handler errors don't kill the connection
local function SafeConnect(signal: RBXScriptSignal, name: string, fn: (...any) -> ...any): RBXScriptConnection
return signal:Connect(function(...)
local ok, err = xpcall(fn, debug.traceback, ...)
if not ok then
warn(`[FactoryController::{name}]`, err)
end
end)
end
local function CanPlayerAfford(leaderstats: Folder, player: Player, cost: string, currencyName: string): boolean
local playerCurrencyAmount: string = leaderstats[currencyName]
local biggerNumber = BigNumMax(cost, playerCurrencyAmount)
if biggerNumber == playerCurrencyAmount then return true end
return false
end
local function PickRarityFromList(list)
local chosenRandomPercent = NextNumber(RNG) * 100
local currentValue = 0
for tier, chance: number in list do
-- add to currentValue each iteration until randomValue is within chosenRandomPercent
currentValue += chance -- may need to use ipairs
if chosenRandomPercent <= currentValue then return tier end
end
return nil -- incase something goes wrong
end
local function CreateOre(factory: Model, ore: Part, cashValue: string, oreValue: string)
-- first, decide if the ore is regular, shiny, rainbow, or void
local rarity = ORE_TIERS[PickRarityFromList(ORE_RARITIES)]
if not rarity then return end
local multiplier: number? = ORE_MULTIPLIERS[rarity]
local oreClone = ore:Clone()
local oreGui = oreClone.BillboardGui
if rarity == `Gem` then
local gem = ServerPrefabs.Gems.FactoryGem:Clone()
oreClone:Destroy()
oreClone = gem
oreGui = oreClone.BillboardGui
end
if rarity ~= `Regular` and rarity ~= `Gem` then
oreClone:AddTag(rarity)
local oreEffects: Folder = OreRarityEffects[rarity]
for _, effect: Instance in oreEffects:GetChildren() do
local clone = effect:Clone()
if clone:IsA(`Sound`) then
clone.Playing = true
elseif clone:IsA(`ParticleEmitter`) then
clone.Enabled = true
end
clone.Parent = oreClone
end
end
if rarity ~= `Gem` then
-- Apply rarity multiplier only on spawned ore (not on labels). Use base strings.
local finalCashValue = cashValue
local finalOreValue = oreValue
if rarity ~= `Regular` and multiplier ~= nil then
finalCashValue = BigNumMul(cashValue, tostring(multiplier))
finalOreValue = BigNumMul(oreValue, tostring(multiplier))
end
oreGui.Cash.Cost.Text = finalCashValue
oreGui.Ore.Cost.Text = finalOreValue
end
oreClone.AssociatedFactory.Value = factory
-- if ore hasn't reached the goal by this point, destroy it
task.delay(ORE_EXPIRE_TIME, function()
if oreClone and oreClone.Parent then
oreClone:Destroy()
end
end) --TODO add animation to this
return oreClone, rarity
end
local function BuildObject(player: Player, object: Model, is_purchase: boolean?)
-- is_purchase will be false if player is loading from data
for _, instance: Instance in object:GetDescendants() do
if instance:IsA(`BasePart`) and instance.Name ~= `Transparent` and instance.Name ~= `UpgraderPart` then
instance.CanCollide =
if instance.Name == `Drop`
or instance.Name == `Transparent`
or instance.Name == `UpgraderPart`
then false else true
if is_purchase and instance.Name ~= `Drop` then
if player ~= nil then
BuildRemote:FireClient(player, instance)
end
task.wait(0.2)
end
instance.Transparency = if instance.Name == `Drop` then 0.5 else 0
elseif instance:IsA(`ParticleEmitter`) or instance:IsA(`Beam`) or instance:IsA(`Light`) then
instance.Enabled = true
end
end
end
local function UpgradeMachine(setting: Configuration, is_dropper: boolean?)
local level: number = setting:GetAttribute(`Level`)
local upgradeCost: string = setting:GetAttribute(`UpgradeCost`)
local cashWorthIncrease: string = setting:GetAttribute(`CashValueIncreasePerUpgrade`)
local oreValueIncrease: string = setting:GetAttribute(`OreValueIncreasePerUpgrade`)
-- increment level first, then compute milestone on new level
setting:SetAttribute(`Level`, level + 1)
local is_milestone = level % UPGRADE_MILESTONE_REQUIREMENT == 0
local costScalarPerLevel = if is_dropper then UPGRADE_COST_SCALAR_DROPPER else UPGRADE_COST_SCALAR_UPGRADER
-- Curved cost: base + (level * scalar) + (curveFactor * level^exp)
-- Then apply a mild multiplicative factor r(L) per 25-level segment (clamped)
local baseCostString = upgradeCost
if baseCostString == `0` or baseCostString == `` then baseCostString = PRICE_OF_FIRST_UPGRADE end
local costCurveExponent = if is_dropper then COST_CURVE_EXP_DROPPER else COST_CURVE_EXP_UPGRADER
local curveFactorString = if is_dropper then COST_CURVE_FACTOR_DROPPER else COST_CURVE_FACTOR_UPGRADER
-- Early-game easing for droppers: first 25 paid upgrades are cheaper
local priceLevelIndex = max(0, level - 1)
local effectiveScalarPerLevel = costScalarPerLevel
local effectiveCurveExponent = costCurveExponent
local effectiveCurveFactorString = curveFactorString
if is_dropper and priceLevelIndex <= EARLY_EASING_MAX_LEVEL then
effectiveScalarPerLevel = EARLY_EASING_SCALAR_PER_LEVEL
effectiveCurveExponent = EARLY_EASING_CURVE_EXPONENT
effectiveCurveFactorString = curveFactorString
end
local levelAsString = `{priceLevelIndex}`
local linearPart = BigNumMul(levelAsString, effectiveScalarPerLevel)
local levelPowerApproximationString = `{max(0, floor((priceLevelIndex ^ effectiveCurveExponent) + 0.5))}`
local curvePart = BigNumMul(effectiveCurveFactorString, levelPowerApproximationString)
local newCost = BigNumAdd(baseCostString, BigNumAdd(linearPart, curvePart))
-- multiplicative growth factor r(L)
local upgradeSegment = max(0, floor((level - 1) / UPGRADE_MILESTONE_REQUIREMENT))
local baseRadius = if is_dropper then COST_R_BASE_DROPPER else COST_R_BASE_UPGRADER
local stepRadius = if is_dropper then COST_R_STEP_DROPPER else COST_R_STEP_UPGRADER
local maxRadius = if is_dropper then COST_R_MAX_DROPPER else COST_R_MAX_UPGRADER
local radius = baseRadius + stepRadius * upgradeSegment
if radius > maxRadius then radius = maxRadius end
newCost = BigNumMul(newCost, (`{string.format("%.3f", radius)}`))
setting:SetAttribute(`UpgradeCost`, newCost)
if is_dropper then
-- Update settings as source of truth
local cashWorth = setting:GetAttribute(`CashValue`)
local oreWorth = setting:GetAttribute(`OreValue`)
setting:SetAttribute(`CashWorth`, BigNumAdd(cashWorth, cashWorthIncrease))
setting:SetAttribute(`OreWorth`, BigNumAdd(oreWorth, oreValueIncrease))
-- At milestones, slightly decrease future increment sizes and tighten drop speed
if is_milestone then
cashWorthIncrease = BigNumMul(cashWorthIncrease, INCREMENT_DECAY_DROPPER)
oreValueIncrease = BigNumMul(oreValueIncrease, INCREMENT_DECAY_DROPPER)
local dropSpeed: number = setting:GetAttribute(`DropSpeed`)
if dropSpeed > MINIMUM_DROP_SPEED then
setting:SetAttribute(`DropSpeed`, max(MINIMUM_DROP_SPEED, dropSpeed - DROP_SPEED_DECREMENT_PER_MILESTONE))
end
end
-- Reflect in UI
local buttons = setting:FindFirstAncestorWhichIsA(`Model`).Interior.Buttons
local button = buttons[`{setting.Name}Button`]
local buttonGui = button.Button.BillboardGui
local costLabel = buttonGui.Cost.Cost
local upgradeLabels = buttonGui.UpgradeVisual
costLabel.Text = upgradeCost
upgradeLabels.A.Text = cashWorth
upgradeLabels.C.Text = BigNumAdd(cashWorth, cashWorthIncrease)
else
-- Upgrader settings drive UI
local multiplier = setting:GetAttribute(`Multiplier`)
setting:SetAttribute(`Multiplier`, BigNumAdd(multiplier, UPGRADER_MULTIPLIER_INCREASE))
local buttons = setting:FindFirstAncestorWhichIsA(`Model`).Interior.Buttons
local button = buttons[`{setting.Name}Button`]
local buttonGui = button.Button.BillboardGui
local costLabel = buttonGui.Cost.Cost
local upgradeLabels = buttonGui.UpgradeVisual
costLabel.Text = upgradeCost
upgradeLabels.A.Text = multiplier
upgradeLabels.C.Text = BigNumAdd(multiplier, UPGRADER_MULTIPLIER_INCREASE)
end
end
-- for notifications that will be sent frequently - need to reduce them to not annoy the player
local function SendSpammyNotification(player: Player, message: string, msgType: ("Good" | "Neutral" | "Bad"))
if not core.SpammyNotificationDebounce then
NotifyRemote:FireClient(player, msgType, message)
core.SpammyNotificationDebounce = true
task.delay(3.5, function()
core.SpammyNotificationDebounce = false
end)
end
end
local function CheckIfStillTouching(character: Model, buttonHitbox: BasePart): boolean
local touchingParts = buttonHitbox:GetTouchingParts()
for _, part in touchingParts do
local touchingCharacter = part.Parent
if touchingCharacter == character then
return true
end
end
return false
end
--* [[----------< Factory Class: Helper Functions >----------]]
local Factory = {}
Factory.__index = Factory
function Factory.new(factory: Instance)
local self = setmetatable({}, Factory)
self.Factory = factory
self.BuyFolder = factory.Buy -- will be destroyed when the factory is bought
self.Exterior = factory.Exterior
self.Interior = factory.Interior
self.TierFolder = factory.Parent
self.TierConfig = self.TierFolder.TierConfig
self.FactoryType = self.TierConfig:GetAttribute(`FactoryType`)
self.FactoryConfig = factory.FactoryConfig
self.Dropper1Config = self.FactoryConfig.Dropper1
self.Dropper2Config = self.FactoryConfig.Dropper2
self.Dropper3Config = self.FactoryConfig.Dropper3
self.ValuatorConfig = self.FactoryConfig.Valuator
self.YielderConfig = self.FactoryConfig.Yielder
self.BuyCost = self.TierConfig.FactoryBuyCost
self.Owner = self.FactoryConfig:GetAttribute(`Owner`)
self.OwnerSign = self.Exterior.OwnerSign.Sign.SurfaceGui.Frame
-- components
self.Machines = self.Interior.Machines
self.Dropper1 = self.Machines.Dropper1
self.Dropper2 = self.Machines.Dropper2
self.Dropper3 = self.Machines.Dropper3
self.SellPoint = self.Machines.SellPoint
self.Valuator = self.Machines.Valuator
self.Yielder = self.Machines.Yielder
-- buttons
self.Buttons = self.Interior.Buttons
self.CollectButton = self.Buttons.CollectButton
self.DropperButton1 = self.Buttons.Dropper1Button
self.DropperButton2 = self.Buttons.Dropper2Button
self.DropperButton3 = self.Buttons.Dropper3Button
self.ValuatorButton = self.Buttons.ValuatorButton
self.YielderButton = self.Buttons.YielderButton
self.RNG = Random.new()
self.Connections = {}
self.LastSaveTime = 0
self.is_alive = true
self.DroppersUnlocked = 0
self:CheckForPlayersToAutoClaim()
self:SetupFactoryBuyButton()
return self
end
function Factory:ValidateButtonTouch(hit: BasePart, is_factory_buy: boolean?): (boolean, Player?)
if not hit.Parent:FindFirstChildWhichIsA(`Humanoid`) then return false, nil end
local player = Players[hit.Parent.Name]
if not player then return false, nil end
if is_factory_buy and player.Name ~= self.Owner then return false, nil end
if Debounce(player, 0.1) then return false, nil end
return true, player
end
function Factory:ValidateOre(ore: Part, upgrader: Model?, is_sell: boolean)
if not ore:HasTag(`Ore`) or is_sell and ore:HasTag(`Gem`) then return nil end
if not ore:FindFirstChild(`AssociatedFactory`) then return nil end
if ore.AssociatedFactory.Value ~= self.Factory then return nil end
if not is_sell and ore:GetAttribute(`UpgradedBy{upgrader}`) then return nil end
local owner = self.Owner and Players:FindFirstChild(self.Owner)
if not owner then return nil end
if not owner or ore:GetNetworkOwner() ~= owner then return nil end
return owner
end
function Factory:TouchBegin(hit: BasePart, buttonHitbox: BasePart, callback: () -> ()?)
-- ensure this doesn't run multiple times
-- this also ensures that the player can't somehow press multiple buttons at once
if self.is_touching then return end
local character: Model = hit.Parent
local player: Player? = Players:GetPlayerFromCharacter(character)
if not player or player ~= Players:FindFirstChild(self.Owner) then return end
self.is_touching = true
local buttonModel = buttonHitbox.Parent
task.defer(function()
while CheckIfStillTouching(character, buttonHitbox) do
ButtonPressRemote:FireClient(player, buttonModel, `press`)
callback(hit, buttonModel)
task.wait(BUTTON_BUY_INTERVAL)
end
self.is_touching = false
end)
end
function Factory:UpdateCollector()
local collectorPart = self.CollectButton.Button
local collectorGui = collectorPart.BillboardGui
local sellPointConfig = self.FactoryConfig.SellPoint
local cashToCollect = sellPointConfig:GetAttribute(`CashToCollect`)
local oreToCollect = sellPointConfig:GetAttribute(`OreToCollect`)
local gemsToCollect = sellPointConfig:GetAttribute(`GemsToCollect`)
local cashLabel = collectorGui.Cash.Cost
local oreLabel = collectorGui.Ore.Cost
local gemsLabel = collectorGui.Gems.Cost
cashLabel.Text = cashToCollect
oreLabel.Text = oreToCollect
gemsLabel.Text = gemsToCollect
end
function Factory:Save()
local data = core.GetData(Players:FindFirstChild(self.Owner))
if not data then return end
-- data references
local factoryData = data.FactoryData
if not factoryData then
factoryData = {}
data.FactoryData = factoryData
end
local factoryType: string = self.FactoryType
factoryData.Factories = factoryData.Factories or {}
local factory = factoryData.Factories[factoryType]
if not factory then
factory = {}
factoryData.Factories[factoryType] = factory
end
-- iterate through every machine
for _, machine: Configuration in self.FactoryConfig:GetChildren() do
-- ensure a table exists for this machine in the player's data
local machineData = factory[machine.Name]
if machineData == nil then
machineData = {}
factory[machine.Name] = machineData
end
-- in the machine folder, iterate through every attribute
for name: string, value: any in machine:GetAttributes() do
-- write current world value into data
machineData[name] = value
end
end
self.LastSaveTime = os.clock()
end
function Factory:Load()
local data = core.GetData(Players:FindFirstChild(self.Owner))
if not data then return end
-- data references
local factoryData = data.FactoryData
if not factoryData then return end
factoryData.Factories = factoryData.Factories or {}
local factory = factoryData.Factories[self.FactoryType]
if not factory then
factory = {}
factoryData.Factories[self.FactoryType] = factory
end
-- iterate through every machine
for _, machine: Configuration in self.FactoryConfig:GetChildren() do
local machineData = factory[machine.Name]
if not machineData then
-- initialize data snapshot from world defaults so it exists for next save
local init = {}
for name: string, value: any in machine:GetAttributes() do
init[name] = value
end
factory[machine.Name] = init
continue
end
-- in the machine, iterate through every attribute
for name: string, value: any in machine:GetAttributes() do
if machineData[name] ~= nil then
machine:SetAttribute(name, machineData[name])
end
end
end
end
function Factory:InitializeFactory()
local owner = Players:FindFirstChild(self.Owner)
local data = core.GetData(owner)
if not data then print(`no data`); return end
local factoryConfig = self.FactoryConfig
local physicalMachines = self.Machines
for _, machine: Configuration in factoryConfig:GetChildren() do
if machine.Name == `SellPoint` then continue end
local buttonModel = self.Buttons:FindFirstChild(`{machine.Name}Button`)
if not buttonModel then continue end
print(buttonModel)
print(buttonModel.Button)
local buttonGui = buttonModel.Button.BillboardGui
local costLabel = buttonGui.Cost.Cost
local upgradeLabels = buttonGui.UpgradeVisual
costLabel.Text = machine:GetAttribute(`UpgradeCost`)
local is_dropper = machine:GetAttribute(`OreValue`) ~= nil
local is_bought = machine:GetAttribute(`IsBought`)
if is_dropper then
local current = machine:GetAttribute(`CashValue`)
print(current)
upgradeLabels.A.Text = current
upgradeLabels.C.Text = BigNumAdd(current, machine:GetAttribute(`CashValueIncreasePerUpgrade`))
-- ore worth does not need to be updated as there's no visual for it
else
print(machine)
local current = machine:GetAttribute(`Multiplier`)
print(current)
upgradeLabels.A.Text = current
upgradeLabels.C.Text = BigNumAdd(current, UPGRADER_MULTIPLIER_INCREASE)
end
if is_bought then
local foundMachine: Model? = physicalMachines:FindFirstChild(machine.Name)
if not foundMachine then continue end
if owner and foundMachine then
BuildObject(owner, foundMachine, false)
end
end
end
self:UpdateCollector()
end
function Factory:Create()
self:SetupDroppers(self.Dropper1)
self:SetupDroppers(self.Dropper2)
self:SetupDroppers(self.Dropper3)
self:SetupUpgraders(self.Valuator.Name)
self:SetupUpgraders(self.Yielder.Name)
self:SetupBuyButtons()
self:SetupSellPoint()
self:SetupCollector()
self:Load()
self:InitializeFactory()
self:OnDisconnect()
-- save every SAVE_INTERVAL_SECONDS seconds as long as there hasn't been a save recently
task.defer(function()
while self.is_alive do
task.wait(SAVE_INTERVAL_SECONDS)
if not self.is_alive then break end
local owner = self.Owner
if not owner and Players:FindFirstChild(owner) then break end
if os.clock() - self.LastSaveTime < MINIMUM_TIME_BETWEEN_SAVES then continue end
local ok, err = xpcall(function()
self:Save()
end, debug.traceback)
if not ok then warn("[FactoryController::AutoSave]", err) end
end
end)
end
--* [[----------< Factory Class: Core Functions >----------]]
function Factory:CheckForPlayersToAutoClaim()
local function OnPlayerAdded(player: Player)
if not server_data_ready then
repeat task.wait(0.1) until core.GetData(player) ~= nil
server_data_ready = true -- only needs to happen once; then it'll be available for all other players
end
local data = core.GetData(player)
if not data then return end
local factoryData = data.FactoryData.Factories[self.FactoryType]
if factoryData == nil then return end
if not factoryData.Owned then return end
-- make sure the player doesn't already own a factory of this type
for _, factory in workspace.Factories[self.FactoryType]:GetChildren() do
if factory.FactoryConfig:GetAttribute(`Owner`) == player.Name then return end
end
-- if we reach this point, the player doesn't own a factory of this type
-- so we can claim it
self.FactoryConfig:SetAttribute(`Owner`, player.Name)
self.Owner = player.Name
self.OwnerSign.Owner.Text = `{player.Name}'s`
self:Create()
self.BuyFolder:Destroy()
self.Connections[`AutoClaimFactories`]:Disconnect()
end
self.Connections[`AutoClaimFactories`] = SafeConnect(Players.PlayerAdded, "PlayerAdded", OnPlayerAdded)
end
function Factory:SetupFactoryBuyButton()
local buyButton = self.BuyFolder.BuyFactory.Button
local buyCosts = self.BuyCost:GetAttributes()
local factoryType = self.FactoryType
local factoryConfig = self.FactoryConfig
local function FactoryBuyButtonTouched(hit)
local is_valid, player = self:ValidateButtonTouch(hit)
if not is_valid then return end
if Debounce(hit.Parent) then return end
local data = core.GetData(player)
if not data then return end
local leaderstats = data.Leaderstats
local factories = workspace.Factories
local factoriesData = data.FactoryData
local factoryData = factoriesData.Factories[factoryType]
for _, factory in factories[factoryType]:GetChildren() do
if factoryConfig:GetAttribute(`Owner`) == player.Name then
SendSpammyNotification(player, `You already own a {factoryType} Factory!`, `Bad`)
return
end
end
for name, value in buyCosts do
if not CanPlayerAfford(leaderstats, player, value, name) then
SendSpammyNotification(player, `You don't have enough {name} to buy this!`, `Bad`)
return
end
end
-- if we reach this point, we can buy the factory
-- take the currencies away from player
for name, value in buyCosts do
if value == `0` then continue end
local stat = leaderstats[name]
if stat then -- if cost is a leaderstat currency. it could also be an ore
leaderstats[name] = BigNumSub(leaderstats[name], value)
-- else --TODO
end
end
-- now claim the factory and run factory functions
factoryConfig:SetAttribute(`Owner`, player.Name)
self.Owner = player.Name
self.OwnerSign.Owner.Text = `{player.Name}'s`
factoryData.Owned = true
factoriesData.FactoriesOwned += 1
BuyFactoryRemote:FireClient(player, self.BuyFolder)
-- gradually destroy the buy folder as the client animations finish
task.delay(0.5, function()
local xpToAdd = 20 * (factoriesData.FactoriesOwned * 2.5)
core.AddXP(player, `{xpToAdd}`, self.BuyFolder.Door)
task.wait(0.5)
self.BuyFolder.Door:Destroy()
task.wait(4)
self.BuyFolder:Destroy()
end)
-- now run factory functions
self:Create()
end
self.Connections[`BuyButtonTouched`] = SafeConnect(buyButton.Touched, "BuyButtonTouched", FactoryBuyButtonTouched)
end
function Factory:SetupDroppers(dropper: Model)
local dropperConfig = self.FactoryConfig[dropper.Name]
local ORE_TO_DROP = OresFolder:FindFirstChild(self.FactoryType)
if not ORE_TO_DROP then return end
-- variables
local cashWorth, oreWorth = dropperConfig:GetAttribute(`CashValue`), dropperConfig:GetAttribute(`OreValue`)
local dropSpeed = dropperConfig:GetAttribute(`DropSpeed`)
local dropSound: Sound? = dropper.Drop:FindFirstChildWhichIsA(`Sound`)
local dropper_unlock_incremented = false
task.defer(function()
repeat task.wait(0.33) until not self.is_alive or dropperConfig:GetAttribute(`IsBought`)
if not self.is_alive then return end
while self.is_alive and dropperConfig:GetAttribute(`IsBought`) do
local ok, err = xpcall(function()
if not dropper_unlock_incremented then
dropper_unlock_incremented = true
self.DroppersUnlocked += 1
end
-- Use settings as source of truth for spawning
local ore, rarity = CreateOre(self.Factory, ORE_TO_DROP, cashWorth, oreWorth)
if not ore or not rarity then return end
local owner = self.Owner
ore.Position = dropper.Drop.Position
if rarity ~= `Regular` and rarity ~= `Gem` then
local findOwner = owner and Players:FindFirstChild(owner)
if findOwner then
OreRarityEffectsRemote:FireClient(findOwner, ore, rarity, dropper)
end
end
if rarity == `Gem` then
PlaySoundEffect(`GemDrop`, NumberRange.new(0.95, 1.05), ore)
elseif rarity == `Void` then
local findOwner = owner and Players:FindFirstChild(owner)
if findOwner then
Announce.Global(`A VOID ORE Spawned In {findOwner.Name}'s {self.FactoryType} Factory!!!`, `#00FFFF`, `[Server]`)
end
end
ore.Parent = OreStorage[`{rarity}Ores`]
local findOwner = owner and Players:FindFirstChild(owner)
if findOwner then
ore:SetNetworkOwner(findOwner)
end
if dropSound then dropSound:Play() end
task.wait(dropSpeed)
end, debug.traceback)
if not ok then warn("[FactoryController::DropperLoop]", err) end
if not self.is_alive then break end
end
end)
end
function Factory:SetupBuyButtons()
local owner = self.Owner
self.is_touching = false
local function BuyButtonTouched(hit, buttonModel: Model)
if buttonModel.Name == `CollectButton` then return end
local is_valid, player = self:ValidateButtonTouch(hit)
if not is_valid then return end
-- also make sure the player touching is the owner of the factory
if Players:FindFirstChild(owner) ~= player then return end
local data = core.GetData(player)
if not data then return end
-- find the cost, and make sure the player can afford it
local objectRealName = buttonModel.Name:gsub(`Button`, ``)
local associatedMachine = self.Machines[objectRealName]
local associatedSettings = self.FactoryConfig[objectRealName]
local level = associatedSettings:GetAttribute(`Level`)
local is_dropper = associatedSettings:GetAttribute(`OreValue`)
local is_bought = associatedSettings:GetAttribute(`IsBought`)
local is_max_level = associatedSettings:GetAttribute(`IsMaxLevel`)
if is_max_level then return end -- early exit flag to save performance a bit
if level >= MAX_UPGRADE_LEVEL then
associatedSettings:SetAttribute(`IsMaxLevel`, true)
return
end
local cost = associatedSettings:GetAttribute(`UpgradeCost`)
local costType = COST_TYPES[objectRealName]
if costType == `Ore` then
costType = self.FactoryType
end
local leaderstats = data.Leaderstats
local ores = data.Ores
-- find the location of the currency
local location = if leaderstats[costType] then leaderstats else ores
if not CanPlayerAfford(location, player, cost, costType) then
-- if the object has already been bought, change message to Upgrade instead
local buyOrUpgrade = if is_bought then `upgrade` else `buy`
SendSpammyNotification(player, `You don't have enough {costType} to {buyOrUpgrade} this!`, `Bad`)
return
end
-- if we reach this point, player can buy the upgrade - subtract the cost, and upgrade the machine
core.CurrencySubtract(player, costType, cost)
local xpToAdd = BigNumMul(`{level}`, `3.5`)
core.AddXP(player, xpToAdd, associatedMachine.PrimaryPart)
UpgradeMachine(self.FactoryConfig[objectRealName], is_dropper)
-- signal to client that the upgrade actually succeeded
ButtonPressRemote:FireClient(player, buttonModel, `success`)
if not is_bought then
associatedSettings:SetAttribute(`IsBought`, true)
local title = buttonModel.Button.BillboardGui.Title
local storedTitle = title.Text
title.Text = `Purchasing...`
BuildObject(player, associatedMachine, true)
title.Text = storedTitle:gsub(`Buy`, `Upgrade`)
end
end
local buttonsFolder = self.Buttons
for _, buttonModel: Model in buttonsFolder:GetChildren() do
if buttonModel.Name == `CollectButton` then continue end
local hitbox = buttonModel.Hitbox
self.Connections[`ButtonTouched_{buttonModel.Name}`] = SafeConnect(hitbox.Touched, `ButtonTouched_{buttonModel.Name}`, function(hit)
self:TouchBegin(hit, hitbox, BuyButtonTouched)
end)
end
end
function Factory:SetupUpgraders(upgrader: string)
local upgraderConfig = self.FactoryConfig[upgrader]
local machine = self.Machines[upgrader]
local upgraderUpgradePart = machine.UpgraderPart
task.defer(function()
repeat task.wait(0.33) until not self.is_alive or upgraderConfig:GetAttribute(`IsBought`)
if not self.is_alive then return end
local function UpgradeOreOnTouch(ore: BasePart)
local owner = self:ValidateOre(ore, machine)
if not owner then return end
-- ore is valid, so upgrade it
ore:SetAttribute(`UpgradedBy{upgrader}`, true)
local multiplier = upgraderConfig:GetAttribute(`Multiplier`)
local oreGui = ore.BillboardGui
local valueToUpgrade = if upgrader == `Valuator` then oreGui.Cash.Cost else oreGui.Ore.Cost
valueToUpgrade.Text = BigNumMul(valueToUpgrade.Text, multiplier)
PlaySoundEffect(`Hum`, NumberRange.new(0.8, 1.2), ore)
PlaySoundEffect(`Ding`, NumberRange.new(0.7, 1), ore)
local player: Player? = Players:FindFirstChild(owner)
if player then
AnimateUpgradeRemote:FireClient(player, ore, upgrader)
end
end
self.Connections[`UpgraderTouched_{upgrader}`] = SafeConnect(upgraderUpgradePart.Touched, `UpgraderTouched_{upgrader}`, UpgradeOreOnTouch)
end)
end
function Factory:SetupSellPoint()
local sellPart: Part = self.SellPoint.SellPart
local factoryConfig = self.FactoryConfig
local sellPointConfig = factoryConfig.SellPoint
local cashToCollect = sellPointConfig:GetAttribute(`CashToCollect`)
local oreToCollect = sellPointConfig:GetAttribute(`OreToCollect`)
local gemsToCollect = sellPointConfig:GetAttribute(`GemsToCollect`)
local points = self.Interior.Decoration.AnimCurvePoints
local function SellOreOnTouch(ore: BasePart)
local is_gem = ore:HasTag(`Gem`)
local owner = self:ValidateOre(ore, nil, true)
if not owner then return end
ore:AddTag(`Processed`)
local oreGui = ore.BillboardGui
if not is_gem then
local cashWorth = oreGui.Cash.Cost
local oreWorth = oreGui.Ore.Cost
sellPointConfig:SetAttribute(`CashToCollect`, BigNumAdd(cashToCollect, cashWorth.Text))
sellPointConfig:SetAttribute(`OreToCollect`, BigNumAdd(oreToCollect, oreWorth.Text))
else
local gemWorth = oreGui.Gems.Cost
sellPointConfig:SetAttribute(`GemsToCollect`, BigNumAdd(gemsToCollect, gemWorth.Text))
end
oreGui.Enabled = false
ProcessOreRemote:FireClient(owner, ore, sellPart, points)
self:UpdateCollector()
local xpToAdd = `{self.DroppersUnlocked * 30 * (ore:HasTag(`Shiny`) and 2 or 1)}`
core.AddXP(owner, xpToAdd, sellPart)
task.delay(3, function()
ore:Destroy()
end)
end
self.Connections[`SellPartTouched`] = SafeConnect(sellPart.Touched, "SellPartTouched", SellOreOnTouch)
end
function Factory:SetupCollector()
local collectorPart = self.CollectButton.Button
local collector_debounce = false
-- Cheaper zero-string check
local function IsZeroString(s: string?): boolean
if not s or s == `` then return true end
-- common zero encodings
return s == `0` or s == `0e0` or s == `0.0` or s == `0.00`
end
local function CollectorButtonTouched(hit)
local is_valid, player = self:ValidateButtonTouch(hit)
if not is_valid then return end
-- Debounce per player, longer than buy button (e.g. 2 seconds)
if collector_debounce then return end
collector_debounce = true
task.delay(2, function()
collector_debounce = false
end)
local factoryConfig = self.FactoryConfig
local sellPointConfig = factoryConfig.SellPoint
local cashToCollect = sellPointConfig:GetAttribute(`CashToCollect`)
local oreToCollect = sellPointConfig:GetAttribute(`OreToCollect`)
local gemsToCollect = sellPointConfig:GetAttribute(`GemsToCollect`)
local any_not_zero = false
if not IsZeroString(cashToCollect) then
core.CurrencyAdd(player, `Cash`, cashToCollect)
any_not_zero = true
end
if not IsZeroString(oreToCollect) then
core.CurrencyAdd(player, self.FactoryType, oreToCollect)
any_not_zero = true
end
if not IsZeroString(gemsToCollect) then
core.CurrencyAdd(player, `Gems`, gemsToCollect)
any_not_zero = true
end
ButtonPressRemote:FireClient(player, self.CollectButton, if any_not_zero then `success` else `press`)
sellPointConfig:SetAttribute(`CashToCollect`, `0`)
sellPointConfig:SetAttribute(`OreToCollect`, `0`)
sellPointConfig:SetAttribute(`GemsToCollect`, `0`)
self:UpdateCollector()
end
self.Connections[`CollectorButtonTouched`] = SafeConnect(collectorPart.Touched, "CollectorButtonTouched", CollectorButtonTouched)
end
function Factory:OnDisconnect()
local player = self.Owner and Players:FindFirstChild(self.Owner)
if not player then return end
local function PlayerRemoving(leavingPlayer)
print(`player removing`)
-- make sure the player who left is the current subject
if leavingPlayer == player then
self.is_alive = false
print(`player confirmed, saving`)
self:Save()
for _, connection: RBXScriptConnection in self.Connections do
if not connection or not connection.Disconnect then continue end
connection:Disconnect()
end
self.Connections = nil
--TODO add link to data script - make sure data is saved, then destroy factory & replace it
end
end
self.Connections[`Disconnect`] = SafeConnect(Players.PlayerRemoving, "PlayerRemoving", PlayerRemoving)
end
--* [[----------< Factory Controller: Init >----------]]
-- initialize factories
for _, factory: Model in CollectionService:GetTagged(`Factory`) do
task.defer(Factory.new, factory)
end
-- add new factories as they're created via player leaving & having their factory replaced
SafeConnect(CollectionService:GetInstanceAddedSignal(`Factory`), "FactoryAdded", Factory.new)
end
return module
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingnew solverThis issue is specific to the new solver.This issue is specific to the new solver.