diff --git a/README.md b/README.md index fb1e56d..20e471e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,6 @@ ModulesOnRails is a collection of Wally packages to streamline Roblox developmen |---------|----------------|-------------| | [AdjustableTimer](https://raild3x.github.io/ModulesOnRails/api/AdjustableTimer) | `AdjustableTimer = "raild3x/adjustabletimer@1.0.0"` | A timer class that can be easily adjusted and paused without constant ticking. | | [AdjustableTimerManager](https://raild3x.github.io/ModulesOnRails/api/AdjustableTimerManager) | `AdjustableTimerManager = "raild3x/adjustabletimermanager@1.0.1"` | A replication manager for AdjustableTimer that allows for easy synchronization across clients in a Roblox game. It handles the replication of timer states and adjustments, ensuring that all clients have a consistent view of the timer's status. | -| [DropletManager](https://raild3x.github.io/ModulesOnRails/api/DropletManager) | `DropletManager = "raild3x/dropletmanager@0.0.3"` | A Droplet System for managing client-sided collectable items in a game. | +| [DropletManager](https://raild3x.github.io/ModulesOnRails/api/DropletManager) | `DropletManager = "raild3x/dropletmanager@0.1.0"` | A Droplet System for managing client-sided collectable items in a game. | | [PlayerDataManager](https://raild3x.github.io/ModulesOnRails/api/PlayerDataManager) | `PlayerDataManager = "raild3x/playerdatamanager@0.1.2"` | A class for managing player profiles. | | [PlayerProfileManager](https://raild3x.github.io/ModulesOnRails/api/PlayerProfileManager) | `PlayerProfileManager = "raild3x/playerprofilemanager@0.0.4"` | A class for managing player profiles. | diff --git a/lib/dropletmanager/src/Client/Droplet.lua b/lib/dropletmanager/src/Client/Droplet.luau similarity index 75% rename from lib/dropletmanager/src/Client/Droplet.lua rename to lib/dropletmanager/src/Client/Droplet.luau index 658eaed..56cb2ef 100644 --- a/lib/dropletmanager/src/Client/Droplet.lua +++ b/lib/dropletmanager/src/Client/Droplet.luau @@ -9,19 +9,18 @@ local RunService = game:GetService("RunService") local Players = game:GetService("Players") if not RunService:IsClient() then + -- Prevents server from loading this module return {} :: Droplet end --// Imports //-- local Packages = script.Parent.Parent.Parent local DropletUtil = require(script.Parent.Parent.DropletUtil) +local DropletsFolder = require(script.Parent.DropletsFolder) +local Promise = require(Packages.Promise) local RailUtil = require(Packages.RailUtil) local SuperClass = require(Packages.BaseObject) ---// Constants //-- -local PLAYER_MASS = 150 -- 1_000_000 -local MAXIMUM_DISPLAY_COLLECT_DISTANCE = 80 -- The furthest distance a player can be from a droplet for us to show the collection visual - --// Types //-- type ResourceTypeData = DropletUtil.ResourceTypeData @@ -30,24 +29,20 @@ local WorldAlignAttachment = Instance.new("Attachment") WorldAlignAttachment.Name = "DropletWorldAlignAttachment" WorldAlignAttachment.Parent = workspace.Terrain -local CharactersList = {} - local ResourceHeightRayParams = RaycastParams.new() ResourceHeightRayParams.FilterType = Enum.RaycastFilterType.Exclude ResourceHeightRayParams.CollisionGroup = DropletUtil.DROPLET_COLLISION_GROUP -ResourceHeightRayParams.FilterDescendantsInstances = {} +ResourceHeightRayParams.FilterDescendantsInstances = {DropletsFolder} RailUtil.Player.forEachCharacter(function(Character, janitor) - table.insert(CharactersList, Character) - ResourceHeightRayParams.FilterDescendantsInstances = CharactersList + ResourceHeightRayParams:AddToFilter(Character) janitor:Add(function() - RailUtil.Table.SwapRemoveFirstValue(CharactersList, Character) - ResourceHeightRayParams.FilterDescendantsInstances = CharactersList + RailUtil.Table.SwapRemoveFirstValue(ResourceHeightRayParams.FilterDescendantsInstances, Character) + ResourceHeightRayParams.FilterDescendantsInstances = table.clone(ResourceHeightRayParams.FilterDescendantsInstances) end) end) - local BulkRenderData = { Size = 0; Parts = {}; @@ -166,7 +161,7 @@ function Droplet.new(config: { local self = setmetatable(SuperClass.new(), Droplet) self:RegisterSignal("PositionChanged") - self:RegisterSignal("Timedout") + self:RegisterSignal("TimedOut") self:RegisterSignal("Collected") self._RenderClock = 0 @@ -179,28 +174,36 @@ function Droplet.new(config: { local DEFAULTS = self._ResourceTypeData.Defaults or {} self._CollectionRadius = DEFAULTS.CollectionRadius or 1.5 + self._MagnetizationRadius = DEFAULTS.MagnetizationRadius or 15 + self._MustSettleBeforeCollect = if DEFAULTS.MustSettleBeforeCollect ~= nil then DEFAULTS.MustSettleBeforeCollect else false self._MaxVelocity = DEFAULTS.MaxVelocity or 150 self._MaxForce = DEFAULTS.MaxForce or math.huge self._Mass = DEFAULTS.Mass or 1 + self._IsSettled = false self._Model = GenerateNewActor() self._Weld = self._Model.Weld self:BindToInstance(self._Model) - local RTData = self:GetResourceTypeData() - if RTData.SetupDroplet then - self._CustomSetupData = RTData.SetupDroplet(self) or {} - else - warn("No Setup function for resource type "..self._NetworkPacket.ResourceType) - end - - self:AddTask(task.delay(config.LifeTime, function() - -- warn(`Droplet expired: [{config.NetworkPacket.Seed}][{config.Id}]`) - self._TimingOut = true - self:FireSignal("Timedout") - TryCall(RTData.OnDropletTimeout, self) - self:Destroy() - end), nil, "LifeTimeThread") + local RTData = self:GetResourceTypeData() :: ResourceTypeData + if RTData.SetupDroplet then + self._CustomSetupData = RTData.SetupDroplet(self) or {} + else + warn("No Setup function for resource type " .. self._NetworkPacket.ResourceType) + end + + local timeoutThread = task.delay(config.LifeTime, function() + self._TimingOut = true + self:FireSignal("TimedOut") + Promise.new(function(resolve) + resolve(TryCall(RTData.OnDropletTimeout, self)) + end) + :timeout(60) + :finally(function() + self:Destroy() + end) + end) + self:AddTask(timeoutThread, nil, "TimeOutThread") -------------------------------- -- Settle Checkers -- @@ -228,8 +231,11 @@ function Droplet.new(config: { if settledFor >= 0.5 then CorePart.Anchored = true CorePart.CanCollide = false + self._IsSettled = true self:RemoveTask("SettleCheck") end + else + settledFor = 0 end end), nil, "SettleCheck") @@ -241,42 +247,35 @@ end -------------------------------------------------------------------------------- --[=[ - + Fetches the end-user defined value of the droplet. ]=] function Droplet:GetValue(): any return self._Value end --[=[ - + Fetches the metadata for the droplet if any has been given ]=] function Droplet:GetMetadata(): any? return self._NetworkPacket.Metadata end --[=[ - + Fetches the resource type data for the droplet. ]=] function Droplet:GetResourceTypeData(): ResourceTypeData return self._ResourceTypeData end --[=[ - + Fetches the position of the droplet. ]=] function Droplet:GetPosition(): Vector3 return self:GetPivot().Position; end --[=[ - @private -]=] -function Droplet:GetPivot(): CFrame - return self._Model:GetPivot() -end - ---[=[ - + Fetches the main actor model of the droplet. ]=] function Droplet:GetModel(): Actor return self._Model; @@ -291,53 +290,111 @@ function Droplet:GetSetupData(): any end --[=[ - Returns the seed and id of the droplet. Used for internal identification. - @return number -- The seed of the droplet - @return number -- The id of the droplet + Returns whether or not the droplet is in the process of timing out. ]=] -function Droplet:Identify(): (number, number) - return self._NetworkPacket.Seed, self._DropletId +function Droplet:IsTimingOut(): boolean + return self._TimingOut == true; end --[=[ - Returns whether or not the droplet is in the process of timing out. + Returns the magnetization radius for this droplet. + This is the distance from a player at which the droplet will begin moving towards them. ]=] -function Droplet:IsTimingOut(): boolean - return self._TimingOut == true; +function Droplet:GetMagnetizationRadius(): number + return self._MagnetizationRadius or 15 end +--[=[ + Returns whether or not the droplet has settled (come to a complete stop). +]=] +function Droplet:IsSettled(): boolean + return self._IsSettled == true +end + +--[=[ + Returns whether this droplet must settle before it can be collected. +]=] +function Droplet:MustSettleBeforeCollect(): boolean + return self._MustSettleBeforeCollect == true +end + +--[=[ + @private + Fetches the pivot CFrame of the droplet. +]=] +function Droplet:GetPivot(): CFrame + return self._Model:GetPivot() +end + +--[=[ + @private + Returns the seed and id of the droplet. Used for internal identification. + @return number -- The seed of the droplet + @return number -- The id of the droplet +]=] +function Droplet:Identify(): (number, number) + return self._NetworkPacket.Seed, self._DropletId +end -------------------------------------------------------------------------------- --// Methods //-- -------------------------------------------------------------------------------- --[=[ - Attaches a Model or Part to the droplet. Use this to add your visuals to the droplet. + Welds a Model or Part to the droplet. Use this to add your visuals to the droplet. + Provides optional C0 and C1 for the Weld created between the droplet and the object. + @param object Model | BasePart -- The object to attach to the droplet. + @param C0 CFrame? -- Optional C0 for the Weld. + @param C1 CFrame? -- Optional C1 for the Weld. ]=] -function Droplet:AttachModel(object: Model | BasePart) +function Droplet:Attach(object: Model | BasePart, C0: CFrame?, C1: CFrame?): Weld + local function setupPart(part: BasePart) + part.Anchored = false + part.Massless = true + part.CanCollide = false + part.CanTouch = false + part.CanQuery = false + part.CollisionGroup = DropletUtil.DROPLET_COLLISION_GROUP + end + local CorePart if object:IsA("BasePart") then CorePart = object + setupPart(object) else CorePart = object.PrimaryPart - assert(CorePart, "Model must have a PrimaryPart") + assert(CorePart, "Provided Model must have a PrimaryPart") + for _, part in object:QueryDescendants("BasePart") do + setupPart(part) + end end - (object :: any).Parent = self:GetModel() + (object :: PVInstance).Parent = self:GetModel() local Weld = Instance.new("Weld") Weld.Part0 = self:GetModel():FindFirstChild("AttachmentPart") Weld.Part1 = CorePart - Weld.C0 = CFrame.new(0, 0, 0) + Weld.C0 = C0 or CFrame.new(0, 0, 0) + Weld.C1 = C1 or CFrame.new(0, 0, 0) Weld.Parent = CorePart return Weld end +Droplet.AttachModel = Droplet.Attach -- Alias --[=[ - + @private + Collects the droplet for the specified player. + Lets the server know that the player has collected this droplet. + Destroys the droplet after collection. + You should not need to call this manually as it is handled by the DropletClientManager. ]=] function Droplet:Collect(playerWhoCollected: Player) + if self._Collected then + warn("Tried to collect but is already collected!", self._Collected) + return + end + self._Collected = playerWhoCollected self:RemoveTask("MagnetizationThread") local RTData = self:GetResourceTypeData() @@ -351,11 +408,17 @@ function Droplet:Collect(playerWhoCollected: Player) end --[=[ - + @private + Called when a player enters the droplet's collection radius. + Starts the magnetization process. + Called automatically when the server registers that a player has claimed this droplet. ]=] function Droplet:Claim(playerWhoClaimed: Player) - if self:IsTimingOut() then return warn("Tried to claim but is already Timing Out!") end - self:RemoveTask("LifeTimeThread") + if self:IsTimingOut() then + warn("Tried to claim but is already Timing Out!") + return + end + self:RemoveTask("TimeOutThread") self:Magnetize(playerWhoClaimed) @@ -365,12 +428,11 @@ end --[=[ @private + Starts the magnetization process for the droplet. + Called automatically when a player enters the droplet's collection radius. ]=] function Droplet:Magnetize(playerWhoCollected: Player) - self:RemoveTask("LifeTimeThread") - - local PlayerExists = playerWhoCollected and playerWhoCollected.Parent == Players - local Character = PlayerExists and playerWhoCollected.Character + self:RemoveTask("TimeOutThread") local GRAVITY = Vector3.new(0, -1, 0) @@ -380,26 +442,16 @@ function Droplet:Magnetize(playerWhoCollected: Player) assert(CorePart, "Model must have a PrimaryPart") CorePart.BrickColor = BrickColor.new("Bright red") - -- Avoid raycasting to include the visual model of the droplet itself in case it's a [Model]. - ResourceHeightRayParams:AddToFilter(Model) - self:AddTask(function() - local filteredInstances = ResourceHeightRayParams.FilterDescendantsInstances - local entryIndex = table.find(filteredInstances, Model) - if entryIndex then - table.remove(filteredInstances, entryIndex) - ResourceHeightRayParams.FilterDescendantsInstances = filteredInstances - end - end) - CorePart.Anchored = true CorePart.CanCollide = false + CorePart.CanQuery = false local MagnetizationStartTime = os.clock() local Velocity = CorePart.AssemblyLinearVelocity * Vector3.new(1,0.5,1) local function Update(dt: number) -- Handle situation where the player its magnetizing towards leaves the game/dies - if playerWhoCollected.Parent ~= Players or not playerWhoCollected.Character then + if playerWhoCollected.Parent ~= Players or not playerWhoCollected.Character or not playerWhoCollected.Character.PrimaryPart then self:Collect(playerWhoCollected) return end @@ -413,7 +465,7 @@ function Droplet:Magnetize(playerWhoCollected: Player) local DesiredVelocity = targetVector.Unit * self._MaxVelocity local Steering = DesiredVelocity - Velocity -- find the direction of force we need to apply - local attractionForce = CalculateGravitationalAttraction(self._Mass, PLAYER_MASS, targetDist) + local attractionForce = CalculateGravitationalAttraction(self._Mass, DropletUtil.PLAYER_MASS, targetDist) Steering += DesiredVelocity * attractionForce Steering = truncate(Steering, self._MaxForce) -- limit the steering force Steering /= self._Mass @@ -422,6 +474,7 @@ function Droplet:Magnetize(playerWhoCollected: Player) Velocity = truncate(Velocity + Steering * dt, self._MaxVelocity) :: Vector3 local g = GRAVITY * dt if currentPos.Y < targetPos.Y then + -- Adjust gravity to be less when below the target to prevent overshooting g /= math.max(1, targetPos.Y - currentPos.Y) end Velocity += g @@ -468,9 +521,12 @@ function Droplet:Magnetize(playerWhoCollected: Player) self:AddTask(RunService.PreAnimation:Connect(Update), nil, "MagnetizationThread") end + + local PlayerExists = playerWhoCollected and playerWhoCollected.Parent == Players + local Character = PlayerExists and playerWhoCollected.Character --Check if its a valid player and if the player is within a reasonable distance (like if the player is >50 studs away) - if Character and (Character:GetPivot().Position - self:GetPosition()).Magnitude < MAXIMUM_DISPLAY_COLLECT_DISTANCE then + if Character and (Character:GetPivot().Position - self:GetPosition()).Magnitude < DropletUtil.MAX_MAGNETIZATION_RADIUS then BeginMagnetization() else self:Collect(playerWhoCollected) @@ -500,6 +556,6 @@ end @within Droplet @type Droplet Droplet ]=] -export type Droplet = typeof(Droplet.new({} :: any)) +export type Droplet = typeof(Droplet) return Droplet \ No newline at end of file diff --git a/lib/dropletmanager/src/Client/DropletClientManager.lua b/lib/dropletmanager/src/Client/DropletClientManager.luau similarity index 51% rename from lib/dropletmanager/src/Client/DropletClientManager.lua rename to lib/dropletmanager/src/Client/DropletClientManager.luau index 7034e05..c7e8ab1 100644 --- a/lib/dropletmanager/src/Client/DropletClientManager.lua +++ b/lib/dropletmanager/src/Client/DropletClientManager.luau @@ -12,40 +12,57 @@ local Players = game:GetService("Players") --// Imports //-- local Packages = script.Parent.Parent.Parent local DropletUtil = require(script.Parent.Parent.DropletUtil) +local DropletsFolder = require(script.Parent.DropletsFolder) local Droplet = require(script.Parent.Droplet) -local RailUtil = require(Packages.RailUtil) -local OctoTree = require(Packages.OctoTree) +local Heap = require(Packages.Heap) local NetWire = require(Packages.NetWire) -local SuperClass = require(Packages.BaseObject) +local RailUtil = require(Packages.RailUtil) +local Octree = require(Packages.Octree) --// Types //-- -type Droplet = Droplet.Droplet type ResourceTypeData = DropletUtil.ResourceTypeData -type DropletNode = any --OctoTree.Node -- TODO: Add OctoTree types -type DropletOctree = OctoTree.Octree +type Droplet = Droplet.Droplet --// Constants //-- -local DEFAULT_COLLECTION_RADIUS = 15 - local Camera = workspace.CurrentCamera local LocalPlayer = Players.LocalPlayer -local DropletsFolder = Instance.new("Folder") -DropletsFolder.Name = "Droplets" -DropletsFolder.Parent = workspace - -local function CalculateEjectionVelocity(hForce, vForce, NumGen: Random): Vector3 +-------------------------------------------------------------------------------- +--// Util Functions //-- +-------------------------------------------------------------------------------- +--[=[ + Calculates the ejection velocity for a droplet with support for arbitrary ejection directions. + + @param hForce number | NumberRange -- Horizontal force (perpendicular to direction) + @param vForce number | NumberRange -- Vertical force (along direction axis) + @param direction Vector3? -- Direction to eject towards (defaults to Vector3.yAxis) + @param NumGen Random -- Random number generator + @return Vector3 -- The calculated ejection velocity +]=] +local function CalculateEjectionVelocity(hForce, vForce, direction: Vector3?, NumGen: Random): Vector3 + local ejectionDirection = (direction or Vector3.yAxis).Unit -- Ensure normalized + local HorizontalForce = DropletUtil.parse(hForce, NumGen) or NumGen:NextInteger(2, 25) local VerticalForce = DropletUtil.parse(vForce, NumGen) or NumGen:NextInteger(25, 50) - --// Generate a random force for the X and Z axis from a circular distribution + -- Create orthonormal basis from direction + -- We need two perpendicular vectors to the direction for the horizontal plane + local up = if math.abs(ejectionDirection.Y) < 0.99 then Vector3.yAxis else Vector3.xAxis + local right = ejectionDirection:Cross(up).Unit + local forward = right:Cross(ejectionDirection).Unit + + -- Generate a random horizontal direction in the plane perpendicular to ejection direction local RandomRotation = NumGen:NextNumber(-math.pi, math.pi) - local RandomDirection = RailUtil.Vector.rotateVector2(Vector2.new(1,0), RandomRotation) - local RandomForce = RandomDirection * HorizontalForce - - local EjectionVelocity = Vector3.new(RandomForce.X, VerticalForce, RandomForce.Y) - return EjectionVelocity + local horizontalX = math.cos(RandomRotation) * HorizontalForce + local horizontalZ = math.sin(RandomRotation) * HorizontalForce + + -- Combine: vertical force along direction, horizontal force perpendicular to it + local velocity = ejectionDirection * VerticalForce + + right * horizontalX + + forward * horizontalZ + + return velocity end local function ParseLocation(location): CFrame @@ -63,91 +80,47 @@ local function ParseLocation(location): CFrame end -------------------------------------------------------------------------------- ---// CLASS //-- +--// MODULE STATE //-- -------------------------------------------------------------------------------- +local DropletClientManager = {} -local SINGLETON - -local DropletClientManager = setmetatable({}, SuperClass) -DropletClientManager.ClassName = "DropletClientManager" -DropletClientManager.__index = DropletClientManager - -local function AssertIsSingleton(self) - assert(self == SINGLETON, "Called method on class instead of singleton") -end - ---[=[ - @tag constructor - @return DropletClientManager - - Creates a new DropletClientManager if one has not already been made, - returns the existing one if one already exists. -]=] -function DropletClientManager.new() - if SINGLETON then return SINGLETON end - local self = setmetatable(SuperClass.new(), DropletClientManager) - SINGLETON = self +local DropletStorage: {[number]: Droplet} = {} +local ResourceTypeDataMap: {[string]: ResourceTypeData} = {} +local RenderOctree: Octree.Octree = Octree.new() +local MagnetOctree: Octree.Octree = Octree.new() +local DistanceHeap: Heap.Heap = Heap.max() +local CollectionTracker = setmetatable({} :: {[Droplet]: boolean}, {__mode = "k"}) - self._DropletStorage = {} - self._ResourceTypeDataMap = {} +local Replicator = NetWire.Client("DropletServerManager") +Replicator.DropletCreated:Connect(function(...) + (DropletClientManager :: any):_OnCreateDroplet(...) +end) - if RunService:IsRunning() then - self._Replicator = NetWire.Client("DropletService") - - self._Replicator.DropletCreated:Connect(function(...) - self:_OnCreateDroplet(...) - end) - - self._Replicator.DropletClaimed:Connect(function(...: any) - self:_OnClaimDroplet(...) - end) - else - warn("Loaded DropletClientManager in edit mode") - end - - self._RenderOctoTree = OctoTree.new() :: DropletOctree - self._MagnetOctoTree = OctoTree.new() :: DropletOctree - self._CollectionTracker = setmetatable({}, {__mode = "k"}) - - self._RenderRadius = 100 - - self:AddTask(RunService.PreSimulation:Connect(function(dt: number) - self:_Update(dt) - end)) - - -- self:RegisterResourceType("Test", Import("TestResourceTypeData")) - - return self -end +Replicator.DropletClaimed:Connect(function(...: any) + (DropletClientManager :: any):_OnClaimDroplet(...) +end) +RunService.PreSimulation:Connect(function(dt: number) + (DropletClientManager :: any):_Update(dt) +end) -------------------------------------------------------------------------------- - --// Methods //-- + --// METHODS //-- -------------------------------------------------------------------------------- --[=[ Registers a new resource type. ]=] function DropletClientManager:RegisterResourceType(resourceType: string, data: ResourceTypeData) - AssertIsSingleton(self) - assert(not self._ResourceTypeDataMap[resourceType], "Resource type already registered") - self._ResourceTypeDataMap[resourceType] = data + assert(not ResourceTypeDataMap[resourceType], "Resource type already registered") + ResourceTypeDataMap[resourceType] = data end --[=[ Returns the resource type data for the given resource type ]=] function DropletClientManager:GetResourceTypeData(resourceType: string): ResourceTypeData? - AssertIsSingleton(self) - return self._ResourceTypeDataMap[resourceType] -end - ---[=[ - Gets the distance at which a droplet must be within to be collected by the LocalPlayer -]=] -function DropletClientManager:GetCollectionRadius(): number - AssertIsSingleton(self) - return self._Replicator.CollectionRadius:Get() or DEFAULT_COLLECTION_RADIUS + return ResourceTypeDataMap[resourceType] end -------------------------------------------------------------------------------- @@ -163,18 +136,29 @@ end ]=] function DropletClientManager:_Update(dt: number) dt = dt or 0 - - local CollectionTracker = self._CollectionTracker debug.profilebegin("Droplet Collection Check") if LocalPlayer.Character and LocalPlayer.Character.PrimaryPart then local PlayerPos = LocalPlayer.Character:GetPivot().Position - for node: DropletNode in self._MagnetOctoTree:ForEachInRadius(PlayerPos, self:GetCollectionRadius()) do + -- Use the maximum magnetization radius from the heap to determine search radius + local maxMagnetizationRadius = DistanceHeap:Peek() or DropletUtil.DEFAULT_COLLECTION_RADIUS + + for node in MagnetOctree:ForEachInRadius(PlayerPos, maxMagnetizationRadius) do local droplet = node.Object - if not CollectionTracker[droplet] then - CollectionTracker[droplet] = true - self:_RequestClaimDroplet(droplet) + if CollectionTracker[droplet] then + continue + end + local dropletPos = node.Position + local distance = (PlayerPos - dropletPos).Magnitude + + -- Check if within this droplet's specific magnetization radius + if distance <= droplet:GetMagnetizationRadius() then + -- Check if it must settle before collecting + if not droplet:MustSettleBeforeCollect() or droplet:IsSettled() then + CollectionTracker[droplet] = true + DropletClientManager:_RequestClaimDroplet(droplet) + end end end end @@ -185,9 +169,10 @@ function DropletClientManager:_Update(dt: number) debug.profileend() debug.profilebegin("Droplet Visualization Update") + local RENDER_RADIUS = DropletUtil.RENDER_RADIUS local isOnScreen = RailUtil.Camera.isOnScreen - local pos: Vector3 = (Camera.CFrame + Camera.CFrame.LookVector * (self._RenderRadius/2)).Position - for node: DropletNode in self._RenderOctoTree:ForEachInRadius(pos, self._RenderRadius) do + local pos: Vector3 = (Camera.CFrame + Camera.CFrame.LookVector * (RENDER_RADIUS/2)).Position + for node in RenderOctree:ForEachInRadius(pos, RENDER_RADIUS + 1) do if isOnScreen(node.Position) then node.Object:_Render(dt) end @@ -200,15 +185,22 @@ end Marks a droplet to be checked for collection ]=] function DropletClientManager:_MarkForCollection(droplet: Droplet) - local octree = self._MagnetOctoTree - local node = octree:CreateNode(droplet:GetPosition(), droplet) + assert(not MagnetOctree:FindFirstNode(droplet), "Droplet already marked for collection") + local node = MagnetOctree:CreateNode(droplet:GetPosition(), droplet) droplet:GetSignal("PositionChanged"):Connect(function(newPos) - octree:ChangeNodePosition(node, newPos) + MagnetOctree:ChangeNodePosition(node, newPos) end) - local function Remove() octree:RemoveNode(node) end - droplet:GetSignal("Timedout"):Once(Remove) + -- Track magnetization radius in heap for dynamic radius checking + local magnetizationRadius = droplet:GetMagnetizationRadius() + DistanceHeap:Push(magnetizationRadius) + + local function Remove() + MagnetOctree:RemoveNode(node) + DistanceHeap:RemoveFirstOccurrence(magnetizationRadius) + end + droplet:GetSignal("TimedOut"):Once(Remove) droplet:GetDestroyedSignal():Once(Remove) end @@ -217,15 +209,15 @@ end Marks a droplet for rendering by placing it into the octree ]=] function DropletClientManager:_MarkForRender(droplet: Droplet) - local octree = self._RenderOctoTree - local node = octree:CreateNode(droplet:GetPosition(), droplet) + assert(not RenderOctree:FindFirstNode(droplet), "Droplet already marked for render") + local node = RenderOctree:CreateNode(droplet:GetPosition(), droplet) droplet:GetSignal("PositionChanged"):Connect(function(newPos) - octree:ChangeNodePosition(node, newPos) + RenderOctree:ChangeNodePosition(node, newPos) end) - - local function Remove() octree:RemoveNode(node) end - droplet:GetSignal("Timedout"):Once(Remove) + + local function Remove() RenderOctree:RemoveNode(node) end + droplet:GetSignal("TimedOut"):Once(Remove) droplet:GetDestroyedSignal():Once(Remove) end @@ -234,7 +226,7 @@ end Called when the server informs us that a new droplet has been created ]=] function DropletClientManager:_OnCreateDroplet(networkPacket: DropletUtil.DropletNetworkPacket) - local rtData = self:GetResourceTypeData(networkPacket.ResourceType) + local rtData = DropletClientManager:GetResourceTypeData(networkPacket.ResourceType) assert(rtData, `Resource type '{tostring(networkPacket.ResourceType)}' not registered`) local DEFAULTS = rtData.Defaults @@ -245,7 +237,7 @@ function DropletClientManager:_OnCreateDroplet(networkPacket: DropletUtil.Drople networkPacket.Metadata = networkPacket.Metadata or DEFAULTS.Metadata local Droplets: {[number]: Droplet} = {} - self._DropletStorage[Seed] = { + DropletStorage[Seed] = { NetworkPacket = networkPacket, Droplets = Droplets, } @@ -253,43 +245,52 @@ function DropletClientManager:_OnCreateDroplet(networkPacket: DropletUtil.Drople local NumGen = Random.new(Seed) local EjectionDuration = DropletUtil.parse(networkPacket.EjectionDuration, NumGen) - for i, rawData in pairs(DropletUtil.calculateDropletValues(Value, Count, Seed, LifeTime)) do + for i, rawData in DropletUtil.calculateDropletValues(Value, Count, Seed, LifeTime) do local droplet = Droplet.new({ Id = i, NetworkPacket = networkPacket, - ResourceTypeData = self:GetResourceTypeData(networkPacket.ResourceType), + ResourceTypeData = DropletClientManager:GetResourceTypeData(networkPacket.ResourceType), Value = rawData.RawValue, LifeTime = rawData.RawLifeTime, - DropletClientManager = self, + DropletClientManager = DropletClientManager, }) - droplet:GetDestroyedSignal():Once(function() - -- print(`Droplet [{Seed}][{i}] destroyed`) + droplet:AddTask(function() Droplets[i] = nil end) droplet:GetSignal("Collected"):Connect(function(collector) if collector == LocalPlayer then - self:_RequestCollectDroplet(droplet) + DropletClientManager:_RequestCollectDroplet(droplet) end end) local dropletModel: Model = droplet:GetModel() - dropletModel:PivotTo(ParseLocation(networkPacket.SpawnLocation)) + local spawnCFrame = ParseLocation(networkPacket.SpawnLocation) + dropletModel:PivotTo(spawnCFrame) dropletModel.Parent = DropletsFolder local dropletPrimaryPart = dropletModel.PrimaryPart assert(dropletPrimaryPart, "Droplet model has no primary part") - dropletPrimaryPart.AssemblyLinearVelocity = (CalculateEjectionVelocity( + + -- Extract ejection direction from spawn location if it's a CFrame, otherwise use default + local ejectionDirection = networkPacket.EjectionDirection or DEFAULTS.EjectionDirection + if not ejectionDirection and typeof(networkPacket.SpawnLocation) == "CFrame" then + ejectionDirection = networkPacket.SpawnLocation.LookVector + elseif not ejectionDirection and typeof(networkPacket.SpawnLocation) == "table" and networkPacket.SpawnLocation.CF then + ejectionDirection = networkPacket.SpawnLocation.CF.LookVector + end + + dropletPrimaryPart.AssemblyLinearVelocity = CalculateEjectionVelocity( networkPacket.EjectionHorizontalVelocity or DEFAULTS.EjectionHorizontalVelocity, networkPacket.EjectionVerticalVelocity or DEFAULTS.EjectionVerticalVelocity, + ejectionDirection, NumGen - )) + ) - - table.insert(Droplets, droplet) + Droplets[i] = droplet task.wait(EjectionDuration/Count) end end @@ -300,7 +301,7 @@ end Called when the server informs us that a droplet has been claimed ]=] function DropletClientManager:_OnClaimDroplet(collector: Player, seed: number, dropletId: number) - local dropletRequest = self._DropletStorage[seed] + local dropletRequest = DropletStorage[seed] assert(dropletRequest, `No Droplet-Request found with seed '{seed}'`) local droplet = dropletRequest.Droplets[dropletId] @@ -320,7 +321,7 @@ end function DropletClientManager:_RequestClaimDroplet(droplet: Droplet) local seed, dropletId = droplet:Identify() --print(`Requesting claim of droplet [{seed}][{dropletId}]`) - self._Replicator.DropletClaimed:Fire(seed, dropletId) + Replicator.DropletClaimed:Fire(seed, dropletId) end --[=[ @@ -330,7 +331,7 @@ end function DropletClientManager:_RequestCollectDroplet(droplet: Droplet) local seed, dropletId = droplet:Identify() --print(`Requesting collect of droplet [{seed}][{dropletId}]`) - self._Replicator.DropletCollected:Fire(seed, dropletId) + Replicator.DropletCollected:Fire(seed, dropletId) end diff --git a/lib/dropletmanager/src/Client/DropletsFolder.luau b/lib/dropletmanager/src/Client/DropletsFolder.luau new file mode 100644 index 0000000..bc8a857 --- /dev/null +++ b/lib/dropletmanager/src/Client/DropletsFolder.luau @@ -0,0 +1,5 @@ +local DropletsFolder = Instance.new("Folder") +DropletsFolder.Name = "Droplets" +DropletsFolder.Parent = workspace + +return DropletsFolder \ No newline at end of file diff --git a/lib/dropletmanager/src/DropletUtil.lua b/lib/dropletmanager/src/DropletUtil.luau similarity index 80% rename from lib/dropletmanager/src/DropletUtil.lua rename to lib/dropletmanager/src/DropletUtil.luau index f02f125..6082ee0 100644 --- a/lib/dropletmanager/src/DropletUtil.lua +++ b/lib/dropletmanager/src/DropletUtil.luau @@ -96,8 +96,12 @@ type NumOrRangeOrWeightedArray = NumOrRange | WeightedArray - `[Defaults]` is a table of default values for the droplet. This can be left empty. The values in this table are used to fill in any missing values in the ResourceSpawnData - when a droplet is spawned as well as overriding certain behaviors internall for things + when a droplet is spawned as well as overriding certain behaviors internally for things like magnetization. + + - `CollectionRadius` - Distance from the player's center that the droplet must be to be collected + - `MagnetizationRadius` - Distance from the player at which the droplet starts moving towards them + - `MustSettleBeforeCollect` - If true, droplet must come to a complete stop before it can be claimed - `[SetupDroplet]` is called when a new droplet is created. Use this to setup your visuals and any variables you need to keep track of. All parts within this should be @@ -123,17 +127,21 @@ type NumOrRangeOrWeightedArray = NumOrRange | WeightedArray export type ResourceTypeData = { Defaults: { Value: any?; + Metadata: any?; CollectorMode: CollectorMode?; Count: NumOrRangeOrWeightedArray?, LifeTime: NumOrRangeOrWeightedArray?, EjectionDuration: NumOrRangeOrWeightedArray?, EjectionHorizontalVelocity: NumOrRangeOrWeightedArray?, EjectionVerticalVelocity: NumOrRangeOrWeightedArray?, + EjectionDirection: Vector3?, Mass: number?, MaxForce: number?, MaxVelocity: number?, CollectionRadius: number?, + MagnetizationRadius: number?, + MustSettleBeforeCollect: boolean?, --CalculateAttraction: (droplet: Droplet, player: Player) -> Vector3?, }; @@ -158,8 +166,9 @@ export type ResourceTypeData = { .LifeTime NumOrRange? -- The time before the droplet dissapears .Count NumOrRangeOrWeightedArray? -- The number of droplets to spawn .EjectionDuration NumOrRangeOrWeightedArray? -- The time it takes to spew out all the droplets - .EjectionHorizontalVelocity NumOrRangeOrWeightedArray? -- The horizontal velocity of the droplets when they are ejected - .EjectionVerticalVelocity NumOrRangeOrWeightedArray? -- The vertical velocity of the droplets when they are ejected + .EjectionHorizontalVelocity NumOrRangeOrWeightedArray? -- The horizontal velocity of the droplets when they are ejected (perpendicular to ejection direction) + .EjectionVerticalVelocity NumOrRangeOrWeightedArray? -- The vertical velocity of the droplets when they are ejected (along ejection direction) + .EjectionDirection Vector3? -- The direction to eject droplets towards (defaults to Vector3.yAxis) :::caution Special Behaviors Any index that takes a `NumOrRangeOrWeightedArray` will be parsed and calculated @@ -182,6 +191,7 @@ export type ResourceSpawnData = { EjectionDuration: NumOrRangeOrWeightedArray?; EjectionHorizontalVelocity: NumOrRangeOrWeightedArray?, EjectionVerticalVelocity: NumOrRangeOrWeightedArray?, + EjectionDirection: Vector3?, } @@ -193,8 +203,9 @@ export type DropletNetworkPacket = { SpawnTime: number, -- The moment the spawn request was made (os.clock) CollectorMode: CollectorMode, -- The mode of the collector EjectionDuration: number, -- The time it takes to eject/spawn all the droplets (in seconds) - EjectionHorizontalVelocity: NumOrRangeOrWeightedArray?, -- The horizontal velocity of the droplets when they are ejected - EjectionVerticalVelocity: NumOrRangeOrWeightedArray?, -- The vertical velocity of the droplets when they are ejected + EjectionHorizontalVelocity: NumOrRangeOrWeightedArray?, -- The horizontal velocity of the droplets when they are ejected (perpendicular to ejection direction) + EjectionVerticalVelocity: NumOrRangeOrWeightedArray?, -- The vertical velocity of the droplets when they are ejected (along ejection direction) + EjectionDirection: Vector3?, -- The direction to eject droplets towards (defaults to Vector3.yAxis if not provided) -- Data directly passed by ResourceSpawnData ResourceType: string, @@ -254,6 +265,72 @@ Enums.CollectorMode = { ]=] DropletUtil.DROPLET_COLLISION_GROUP = "Droplet" +--[=[ + @within DropletUtil + @server + @prop DEFAULT_COLLECTOR_MODE CollectorMode + The default collector mode for droplets +]=] +DropletUtil.DEFAULT_COLLECTOR_MODE = Enums.CollectorMode.MultiCollector + +--[=[ + @within DropletUtil + @server + @prop DEFAULT_COUNT NumOrRangeOrWeightedArray + The default number of droplets to spawn +]=] +DropletUtil.DEFAULT_COUNT = 1 + +--[=[ + @within DropletUtil + @server + @prop DEFAULT_LIFETIME NumOrRangeOrWeightedArray + The default lifetime for droplets +]=] +DropletUtil.DEFAULT_LIFETIME = 10 + +--[=[ + @within DropletUtil + @server + @prop DEFAULT_EJECTION_DURATION number + The default ejection duration for droplets. +]=] +DropletUtil.DEFAULT_EJECTION_DURATION = 1 + +--[=[ + @within DropletUtil + @client + @prop DEFAULT_COLLECTION_RADIUS number + The default radius within which droplets are marked for collection. +]=] +DropletUtil.DEFAULT_COLLECTION_RADIUS = 15 + +--[=[ + @within DropletUtil + @client + @prop MAX_MAGNETIZATION_RADIUS number + The radius within which droplets are allowed to magnetize towards players. Beyond this + radius, droplets will not magnetize and will just be instantly collected. This is to prevent + wildly swinging droplets that gain tons of acceleration from far away. +]=] +DropletUtil.MAX_MAGNETIZATION_RADIUS = 50 + +--[=[ + @within DropletUtil + @client + @prop RENDER_RADIUS number + The radius within which droplets will have their render functions called +]=] +DropletUtil.RENDER_RADIUS = 100 + +--[=[ + @within DropletUtil + @client + @prop PLAYER_MASS number + The default player mass for gravitational calculations +]=] +DropletUtil.PLAYER_MASS = 200 + -------------------------------------------------------------------------------- --// Utility functions //-- -------------------------------------------------------------------------------- diff --git a/lib/dropletmanager/src/ExampleResourceTypeData.lua b/lib/dropletmanager/src/ExampleResourceTypeData.luau similarity index 94% rename from lib/dropletmanager/src/ExampleResourceTypeData.lua rename to lib/dropletmanager/src/ExampleResourceTypeData.luau index 7fa6991..a6c292f 100644 --- a/lib/dropletmanager/src/ExampleResourceTypeData.lua +++ b/lib/dropletmanager/src/ExampleResourceTypeData.luau @@ -23,21 +23,24 @@ -------------------------------------------------------------------------------- return { + Defaults = { Value = NumberRange.new(0.6, 1.4); -- The value you want the droplet to have. This can be anything. -- Metadata = {}; -- You typically shouldnt default metadata. - + Count = NumberRange.new(2, 5); -- Number of droplets to spawn LifeTime = NumberRange.new(50, 60); -- Time before the droplet dissapears EjectionDuration = 1; -- Time it takes to spew out all the droplets EjectionHorizontalVelocity = NumberRange.new(0, 25); EjectionVerticalVelocity = NumberRange.new(25, 50); CollectorMode = DropletUtil.Enums.CollectorMode.MultiCollector; - + Mass = 1; -- Mass of the droplet (Used in magnitization calculations) MaxForce = math.huge; -- Maximum steering force applied to the droplet when magnitized to a player MaxVelocity = 150; -- Maxiumum velocity of the droplet when magnitized to a player CollectionRadius = 1.5; -- Radius from center of player the droplet must be to be considered 'collected' + MagnetizationRadius = 12; -- Radius from player in which the droplet will start being attracted to the player + MustSettleBeforeCollect = false; -- Whether the droplet must come to a complete stop before it can be collected }; --[[ @@ -91,7 +94,7 @@ VisualModel:ScaleTo(1) end), nil, "GrowThread") - droplet:AttachModel(VisualModel) + droplet:Attach(VisualModel) -- Important! return { VisualModel = VisualModel; @@ -214,19 +217,21 @@ GenericPart.Massless = true return { Defaults = { Value = NumberRange.new(0.6, 1.4); -- The value you want the droplet to have. This can be anything. - -- Metadata = {}; -- You typically shouldnt default metadata. Count = NumberRange.new(2, 5); -- Number of droplets to spawn LifeTime = NumberRange.new(50, 60); -- Time before the droplet dissapears EjectionDuration = 1; -- Time it takes to spew out all the droplets EjectionHorizontalVelocity = NumberRange.new(0, 25); EjectionVerticalVelocity = NumberRange.new(25, 50); + EjectionDirection = nil; -- Direction to eject towards (defaults to Vector3.yAxis, or CFrame.LookVector if SpawnLocation is a CFrame) CollectorMode = DropletUtil.Enums.CollectorMode.MultiCollector; Mass = 1; -- Mass of the droplet (Used in magnitization calculations) MaxForce = math.huge; -- Maximum steering force applied to the droplet when magnitized to a player MaxVelocity = 150; -- Maxiumum velocity of the droplet when magnitized to a player CollectionRadius = 1.5; -- Radius from center of player the droplet must be to be considered 'collected' + MagnetizationRadius = 12; -- Radius from player in which the droplet will start being attracted to the player + MustSettleBeforeCollect = true; -- Whether the droplet must come to a complete stop before it can be collected }; --[[ @@ -280,7 +285,7 @@ return { VisualModel:ScaleTo(1) end), nil, "GrowThread") - droplet:AttachModel(VisualModel) + droplet:Attach(VisualModel) return { VisualModel = VisualModel; diff --git a/lib/dropletmanager/src/Server/DropletServerManager.lua b/lib/dropletmanager/src/Server/DropletServerManager.luau similarity index 68% rename from lib/dropletmanager/src/Server/DropletServerManager.lua rename to lib/dropletmanager/src/Server/DropletServerManager.luau index 006b285..6487f0b 100644 --- a/lib/dropletmanager/src/Server/DropletServerManager.lua +++ b/lib/dropletmanager/src/Server/DropletServerManager.luau @@ -7,15 +7,15 @@ --// Services //-- local PhysicsService = game:GetService("PhysicsService") local Players = game:GetService("Players") -local RunService = game:GetService("RunService") --// Imports //-- local Packages = script.Parent.Parent.Parent local DropletUtil = require(script.Parent.Parent.DropletUtil) local ProbabilityDistributor = require(Packages.ProbabilityDistributor) -local BaseObject = require(Packages.BaseObject) +local RailUtil = require(Packages.RailUtil) local NetWire = require(Packages.NetWire) +--// Types //-- type table = {[any]: any} type CollectorMode = DropletUtil.CollectorMode type WeightedArray = ProbabilityDistributor.WeightedArray @@ -26,14 +26,8 @@ type NumOrRangeOrWeightedArray = NumOrRange | WeightedArray= 0, "Count must resolve to a non-negative number") - local EjectionDuration = ParseNRWT(data.EjectionDuration or DEFAULTS.EjectionDuration or DEFAULT_EJECTION_DURATION, NumGen) + local EjectionDuration = ParseNRWT(data.EjectionDuration or DEFAULTS.EjectionDuration or DropletUtil.DEFAULT_EJECTION_DURATION, NumGen) assert(typeof(EjectionDuration) == "number" and EjectionDuration >= 0, "EjectionDuration must resolve to a non-negative number") - local LifeTime = data.LifeTime or DEFAULTS.LifeTime or DEFAULT_LIFE_TIME + local LifeTime = data.LifeTime or DEFAULTS.LifeTime or DropletUtil.DEFAULT_LIFETIME AssertTypes("LifeTime", LifeTime, "number", "NumberRange") if typeof(LifeTime) == "NumberRange" then assert(LifeTime.Min >= 0 and LifeTime.Max >= 0, "LifeTime must resolve to a positive number") @@ -273,10 +209,11 @@ function DropletServerManager:Spawn(data: ResourceSpawnData): number Seed = Seed, Count = Count, SpawnTime = workspace:GetServerTimeNow(), - CollectorMode = data.CollectorMode or DEFAULTS.CollectorMode or DEFAULT_COLLECTOR_MODE, + CollectorMode = data.CollectorMode or DEFAULTS.CollectorMode or DropletUtil.DEFAULT_COLLECTOR_MODE, EjectionDuration = EjectionDuration, EjectionVerticalVelocity = AssertValidNRWT(data.EjectionVerticalVelocity), EjectionHorizontalVelocity = AssertValidNRWT(data.EjectionHorizontalVelocity), + EjectionDirection = data.EjectionDirection or DEFAULTS.EjectionDirection, ResourceType = data.ResourceType, Value = VALUE, @@ -293,19 +230,19 @@ function DropletServerManager:Spawn(data: ResourceSpawnData): number rawDropletData[i] = { ActualValue = rawData.RawValue } end - self._DropletStorage[Seed] = { + DropletStorage[Seed] = { NetworkPacket = NetworkPacket, DropletData = rawDropletData, PlayerTargets = ValidatePlayerTargets(data.PlayerTargets), } :: DropletUtil.DropletServerCacheData - self._Replicator.DropletCreated:FireFor(self._DropletStorage[Seed].PlayerTargets, NetworkPacket) + Replicator.DropletCreated:FireFor(DropletStorage[Seed].PlayerTargets, NetworkPacket) --// Schedule the droplet to be removed after its lifetime has expired //-- local LifetimeUpperBound = if typeof(LifeTime) == "NumberRange" then LifeTime.Max else LifeTime - self:AddTask(task.delay(LifetimeUpperBound + 30, function() - self._DropletStorage[Seed] = nil - end), nil, Seed) + task.delay(LifetimeUpperBound + 30, function() + DropletStorage[Seed] = nil + end) return Seed end @@ -318,19 +255,21 @@ end @return boolean -- Whether or not the claim was successful. ]=] function DropletServerManager:Claim(collector: Player, seed: number, dropletNumber: (number)?): boolean - AssertIsSingleton(self) - assert(collector and collector:IsA("Player"), "Invalid collector passed when attempting to claim droplet") assert(typeof(seed) == "number", `Invalid Seed passed when attempting to collect droplet: {tostring(seed)}, must be a number`) - local serverData = self:GetDropletServerData(seed) - if not serverData then return false end + local serverData = DropletServerManager:_GetDropletServerData(seed) + if not serverData then + warn(`Droplet request with seed '{seed}' does not exist when attempting to claim.`) + return false + end --// If no droplet number then collect all droplets if typeof(dropletNumber) ~= "number" then if not dropletNumber then local fullSuccess = true - for key in pairs(serverData.DropletData) do - local claimSuccess = self:Claim(collector, seed, key) + local keys = RailUtil.Table.Keys(serverData.DropletData) + for _, key in ipairs(keys) do + local claimSuccess = DropletServerManager:Claim(collector, seed, key) fullSuccess = fullSuccess and claimSuccess end return fullSuccess @@ -350,13 +289,11 @@ function DropletServerManager:Claim(collector: Player, seed: number, dropletNumb end dropletData.ClaimedBy = dropletData.ClaimedBy or {} - local alreadyClaimed = if collectorMode == DropletEnums.CollectorMode.SingleCollector then + local alreadyClaimed = if collectorMode == CollectorMode.SingleCollector then #dropletData.ClaimedBy > 0 else table.find(dropletData.ClaimedBy, collector) ~= nil if alreadyClaimed then - if self._DEBUG then - warn(`Player '{collector.Name}' has already claimed Droplet [{seed}][{dropletNumber}]`) - end + warn(`Player '{collector.Name}' has already claimed Droplet [{seed}][{dropletNumber}]`) return false end end @@ -364,15 +301,17 @@ function DropletServerManager:Claim(collector: Player, seed: number, dropletNumb --// Handle collection //-- table.insert(dropletData.ClaimedBy, collector) - if collectorMode == DropletEnums.CollectorMode.SingleCollector then - self._Replicator.DropletClaimed:FireFor(serverData.PlayerTargets, collector, seed, dropletNumber) - elseif collectorMode == DropletEnums.CollectorMode.MultiCollector then - self._Replicator.DropletClaimed:Fire(collector, collector, seed, dropletNumber) + if collectorMode == CollectorMode.SingleCollector then + Replicator.DropletClaimed:FireFor(serverData.PlayerTargets, collector, seed, dropletNumber) + elseif collectorMode == CollectorMode.MultiCollector then + Replicator.DropletClaimed:Fire(collector, collector, seed, dropletNumber) else error("Invalid CollectorMode: " .. tostring(collectorMode)) end - -- print(`{collector.Name} claimed Droplet [{seed}][{dropletNumber}]`) + if DEBUG then + print(`{collector.Name} claimed Droplet [{seed}][{dropletNumber}]`) + end return true end @@ -385,11 +324,9 @@ end @return boolean -- Whether or not the collection was successful. ]=] function DropletServerManager:Collect(collector: Player, seed: number, dropletNumber: (number)?): boolean - AssertIsSingleton(self) - assert(collector and collector:IsA("Player"), "Invalid collector passed when attempting to claim droplet") assert(typeof(seed) == "number", `Invalid Seed passed when attempting to collect droplet: {tostring(seed)}, must be a number`) - local serverData = self:GetDropletServerData(seed) + local serverData = DropletServerManager:_GetDropletServerData(seed) if not serverData then return false end --// If no droplet number then collect all droplets @@ -397,7 +334,7 @@ function DropletServerManager:Collect(collector: Player, seed: number, dropletNu if not dropletNumber then local fullSuccess = true for key in pairs(serverData.DropletData) do - local collectSuccess = self:Collect(collector, seed, key) + local collectSuccess = DropletServerManager:Collect(collector, seed, key) fullSuccess = fullSuccess and collectSuccess end return fullSuccess @@ -417,26 +354,24 @@ function DropletServerManager:Collect(collector: Player, seed: number, dropletNu end dropletData.CollectedBy = dropletData.CollectedBy or {} - local alreadyCollected = if collectorMode == DropletEnums.CollectorMode.SingleCollector then + local alreadyCollected = if collectorMode == CollectorMode.SingleCollector then #dropletData.CollectedBy > 0 else table.find(dropletData.CollectedBy, collector) ~= nil if alreadyCollected then - if self._DEBUG then - warn(`Player '{collector.Name}' has already collected droplet with seed '{seed}' and droplet number '{dropletNumber}'`) - end + warn(`Player '{collector.Name}' has already collected droplet with seed '{seed}' and droplet number '{dropletNumber}'`) return false end end --// Handle collection //-- - local resourceTypeData = self:GetResourceTypeData(dropletInfo.ResourceType) + local resourceTypeData = DropletServerManager:GetResourceTypeData(dropletInfo.ResourceType) TryCall(resourceTypeData.OnServerCollect, collector, dropletData.ActualValue, dropletInfo.Metadata) - if collectorMode == DropletEnums.CollectorMode.SingleCollector then + if collectorMode == CollectorMode.SingleCollector then serverData.DropletData[dropletNumber] = nil -- Remove this droplet from the droplet data - elseif collectorMode == DropletEnums.CollectorMode.MultiCollector then + elseif collectorMode == CollectorMode.MultiCollector then table.insert(dropletData.CollectedBy, collector) if #dropletData.CollectedBy == #serverData.PlayerTargets then @@ -446,7 +381,9 @@ function DropletServerManager:Collect(collector: Player, seed: number, dropletNu error("Invalid CollectorMode: " .. tostring(collectorMode)) end - -- print(`{collector.Name} collected Droplet [{seed}][{dropletNumber}]`) + if DEBUG then + print(`{collector.Name} collected Droplet [{seed}][{dropletNumber}]`) + end do -- Check to see if all droplets have been collected local isAllCollected = true @@ -457,7 +394,7 @@ function DropletServerManager:Collect(collector: Player, seed: number, dropletNu end end if isAllCollected then -- remove the droplet request - self._DropletStorage[seed] = nil + DropletStorage[seed] = nil end end @@ -465,19 +402,23 @@ function DropletServerManager:Collect(collector: Player, seed: number, dropletNu end --[=[ - Gets the collection radius for the given player. + @private + Generates a new unused seed ]=] -function DropletServerManager:GetCollectionRadius(player: Player): number - return self._Replicator.CollectionRadius:GetFor(player) +function DropletServerManager:_GenerateSeed(): number + local seed + repeat + seed = math.random(1, 2^20) + until not DropletStorage[seed] + return seed end --[=[ - Sets the collection radius for the given player. + @private + Returns the droplet server data for the given seed. ]=] -function DropletServerManager:SetCollectionRadius(player: Player, radius: number) - assert(typeof(radius) == "number" and radius >= 0, "Invalid radius given, must be a non-negative number") - self._Replicator.CollectionRadius:SetFor(player, radius) +function DropletServerManager:_GetDropletServerData(seed: number): DropletUtil.DropletServerCacheData? + return DropletStorage[seed] or warn("Droplet request with seed '" .. tostring(seed) .. "' does not exist") end - return DropletServerManager \ No newline at end of file diff --git a/lib/dropletmanager/src/init.lua b/lib/dropletmanager/src/init.luau similarity index 86% rename from lib/dropletmanager/src/init.lua rename to lib/dropletmanager/src/init.luau index c3666d3..b60667f 100644 --- a/lib/dropletmanager/src/init.lua +++ b/lib/dropletmanager/src/init.luau @@ -54,15 +54,17 @@ Part.Parent = Model Model.PrimaryPart = Part - droplet:AttachModel(Model) + -- Weld our model to the droplet + droplet:Attach(Model) + -- Create metadata to be used in in the render functions local SetupData = { Direction = if math.random() > 0.5 then 1 else -1 end; } - return CustomData + return SetupData end; - -- Ran when the droplet is within render range of the LocalPlayer's Camera + -- Ran when the droplet is within render range/view of the LocalPlayer's Camera OnRenderUpdate = function(droplet: Droplet, rendertimeElapsed: number): CFrame? local SetupData = droplet:GetSetupData() local OffsetCFrame = CFrame.new() @@ -122,7 +124,7 @@ local RunService = game:GetService("RunService") -local DropletUtil = require(script.DropletUtil) ---@module DropletUtil +local DropletUtil = require(script.DropletUtil) local Droplet = require(script.Client.Droplet) --[=[ @@ -143,23 +145,26 @@ export type ResourceTypeData = DropletUtil.ResourceTypeData ]=] export type ResourceSpawnData = DropletUtil.ResourceSpawnData +-------------------------------------------------------------------------------- + --// Module //-- +-------------------------------------------------------------------------------- local DropletManager = {} --[=[ @within DropletManager @prop Server DropletServerManager @server - Accessing this will automatically create a new DropletServerManager if one does not exist. ]=] -DropletManager.Server = nil ---@module DropletServerManager +local Server : typeof(require(script.Server.DropletServerManager)) = nil +DropletManager.Server = Server --[=[ @within DropletManager @prop Client DropletClientManager @client - Accessing this will automatically create a new DropletClientManager if one does not exist. ]=] -DropletManager.Client = nil ---@module DropletClientManager +local Client : typeof(require(script.Client.DropletClientManager)) = nil +DropletManager.Client = Client --[=[ @within DropletManager @@ -168,15 +173,16 @@ DropletManager.Client = nil ---@module DropletClientManager DropletManager.Util = DropletUtil -- Handles lazy initialization of the DropletManager -setmetatable(DropletManager, { +setmetatable(DropletManager :: any, { __index = function(t, k) if k == "Server" then assert(RunService:IsServer(), "Attempted to access DropletManager.Server on client") - t[k] = require(script.Server.DropletServerManager).new() + t[k] = require(script.Server.DropletServerManager) elseif k == "Client" then assert(RunService:IsClient(), "Attempted to access DropletManager.Client on server") - t[k] = require(script.Client.DropletClientManager).new() + t[k] = require(script.Client.DropletClientManager) end + return t[k] end }) diff --git a/lib/dropletmanager/wally.toml b/lib/dropletmanager/wally.toml index a4df5e8..1355dd2 100644 --- a/lib/dropletmanager/wally.toml +++ b/lib/dropletmanager/wally.toml @@ -2,7 +2,7 @@ name = "raild3x/dropletmanager" description = "A Droplet System for managing client-sided collectable items in a game." authors = ["Logan Hunt (Raildex)"] -version = "0.0.3" +version = "0.1.0" license = "MIT" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" @@ -16,9 +16,12 @@ docsLink = "DropletManager" unreleased = true [dependencies] +Heap = "raild3x/heap@2.1.3" Promise = "evaera/promise@^4.0.0" -OctoTree = "sleitnick/octo-tree@0.3.1" +Octree = "sleitnick/octo-tree@0.3.1" NetWire = "raild3x/netwire@^0" RailUtil = "raild3x/railutil@^1" BaseObject = "raild3x/baseobject@^0" ProbabilityDistributor = "raild3x/probabilitydistributor@^1" +Janitor = "howmanysmall/janitor@^1.16.0" +Signal = "howmanysmall/better-signal@2.1.0" \ No newline at end of file