├── .gitignore ├── LICENSE ├── README.md ├── aftman.toml ├── default.project.json ├── development.project.json ├── src ├── Assets.rbxm ├── Functions.lua ├── Operator.lua ├── Settings.lua └── init.lua └── wally.toml /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbxl 2 | *.lock 3 | 4 | **/Packages/** 5 | **/ServerPackages/** 6 | **/DevPackages/** 7 | 8 | sourcemap.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 rotn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Logo 5 | 6 | 7 |
8 | 9 |
10 | 11 | 12 | v1.1.2 • [Model](https://create.roblox.com/marketplace/asset/15420466379/) • [Devforum](https://devforum.roblox.com/t/blood-engine-a-droplet-emitter-system/2545682) 13 | 14 |
15 | 16 | ## What is Blood Engine? 17 | Blood Engine is a versatile resource that can be utilized for various applications, including creating effects like paint, water, blood, and more. It offers numerous methods tailored to meet your specific needs. 18 | 19 | One of its key features is the ability to emit "droplets" - these are meshes that can take on the appearance of "Decals" or "Spheres". These droplets can be emitted from any given origin point with a given velocity. Upon landing on a surface, such as a wall or floor, they transform into a pool. 20 | 21 | This entire process is highly customizable, with 24 options at your disposal to tweak and adjust according to your requirements. This ensures that Blood Engine can adapt to a wide range of scenarios and use-cases, providing you with the flexibility to create the exact effect you're aiming for. 22 | 23 | ## Installation 24 | You can install Blood Engine through the latest release of the repository, the [Model](https://create.roblox.com/marketplace/asset/15420466379/) published on Roblox, or by using Wally: 25 | ```toml 26 | [dependencies] 27 | BloodEngine = "rotntake/blood-engine@1.1.2" 28 | ``` 29 | 30 | ## Usage 31 | #### Initialization 32 | Firstly, you'll need to initialize BloodEngine with your preferred settings. This can be done in either a client or server script. However, it's generally more advisable to do this on the client side, so we'll proceed with that approach. 33 | 34 | The settings provide you with control over various aspects of droplets and pools. These include the maximum number of droplets that can be created, the type of droplets to use, the velocity of droplets upon emission, and much more. 35 | ```lua 36 | -- Import the BloodEngine module 37 | local BloodEngine = require(PathToModule) 38 | 39 | -- Initialize BloodEngine with desired settings 40 | local Engine = BloodEngine.new({ 41 | Limit = 100, -- Sets the maximum number of droplets that can be created. 42 | Type = "Default", -- Defines the droplet type. It can be either "Default" (Sphere) or "Decal", 43 | RandomOffset = false, -- Determines whether a droplet should spawn at a random offset from a given position. 44 | OffsetRange = {-20, 10}, -- Specifies the offset range for the position vectors. 45 | DropletVelocity = {1, 2}, -- Controls the velocity of the emitted droplet. 46 | DropletDelay = {0.05, 0.1}, -- Sets the delay between emitting droplets in a loop (for the EmitAmount method). 47 | StartingSize = Vector3.new(0.01, 0.7, 0.01), -- Sets the initial size of the droplets upon landing. 48 | Expansion = true, -- Determines whether a pool can expand when a droplet lands on it. 49 | MaximumSize = 1, -- Sets the maximum size a pool can reach. 50 | }) 51 | ``` 52 | #### Emitting Droplets 53 | After initializing the module, you're all set to emit droplets. There are two key methods available for droplet emission: `EmitAmount` and `Emit`. 54 | ```lua 55 | -- Emit a specific amount of droplets from a given origin in specific or nil direction 56 | -- (Setting the Direction to nil will make droplets go in random directions) 57 | Engine:EmitAmount(Origin, Direction, Amount) 58 | 59 | -- Emit a single droplet from a given origin in a specific or nil direction 60 | Engine:Emit(Origin, Direction) 61 | ``` 62 | In this instance, we’ll be utilizing the `EmitAmount` method. Typically, you’d use the `Emit` method when you want to create your own loop instead of relying on the built-in loop of `EmitAmount` . This gives you more control over the emission process. 63 | -------------------------------------------------------------------------------- /aftman.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Aftman, a cross-platform toolchain manager. 2 | # For more information, see https://github.com/LPGhatguy/aftman 3 | 4 | # To add a new tool, add an entry to this table. 5 | 6 | [tools] 7 | wally = "UpliftGames/wally@0.3.2" 8 | rojo = "rojo-rbx/rojo@7.3.0" -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blood Engine", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } -------------------------------------------------------------------------------- /development.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blood Engine", 3 | "tree": { 4 | "$className": "DataModel", 5 | 6 | "ReplicatedStorage": { 7 | "$className": "ReplicatedStorage", 8 | "$ignoreUnknownInstances": true, 9 | 10 | "Packages": { 11 | "$className": "Folder", 12 | "$path": "Packages", 13 | 14 | "BloodEngine": { 15 | "$path": "src" 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Assets.rbxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotntake/BloodEngine/93aab15828a17409f44536cdb56a4a4bfce99061/src/Assets.rbxm -------------------------------------------------------------------------------- /src/Functions.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | @Description: Contains a list of useful functions. 3 | ]] 4 | 5 | local TweenService = game:GetService("TweenService") 6 | local Workspace = game:GetService("Workspace") 7 | 8 | -- Variable definitions 9 | local ParentClass = script.Parent 10 | local Assets = ParentClass.Assets 11 | 12 | -- Asset definitions 13 | local Images = Assets.Images 14 | local Essentials = Assets.Essentials 15 | local Effects = Assets.Effects 16 | 17 | -- Effect definitions 18 | local TrailEffects = Effects.Trail 19 | local ImpactEffects = Effects.Impact 20 | 21 | -- Essential definitions 22 | local FastCast = require(Essentials.FastCast) 23 | local Random = Random.new() 24 | 25 | -- Globals 26 | local Unpack = table.unpack 27 | local Decals = Images:GetChildren() 28 | 29 | -- Module definition 30 | local Functions = {} 31 | 32 | -- Variable definitions 33 | local Properties = { 34 | "Size", 35 | "Transparency", 36 | "Anchored", 37 | } 38 | 39 | --[[ 40 | A shorter way of doing: 41 | ```lua 42 | typeof(Variable) == "Type" 43 | ``` 44 | ]] 45 | function Functions.IsOfType(Any, Type: string) 46 | return typeof(Any) == Type 47 | end 48 | 49 | --[[ 50 | Allows the ability to insert an array of 51 | variables efficently onto a table. 52 | ]] 53 | function Functions.MultiInsert(List: {}, Variables: {}) 54 | for Key, Variable in Variables do 55 | --[[ 56 | Executes the variable if it's a function, 57 | It is expected to return a variable to later assign. 58 | ]] 59 | if Functions.IsOfType(Variable, "function") then 60 | Variable = Variable() 61 | end 62 | 63 | -- Adds in the variable with a key 64 | if Functions.IsOfType(Key, "string") then 65 | List[Key] = Variable 66 | end 67 | 68 | -- Adds in the variable without a key 69 | table.insert(List, Variable) 70 | end 71 | end 72 | 73 | --[[ 74 | Returns the name of the specified function 75 | within the class’s metatable. 76 | ]] 77 | function Functions.GetFunctionName(Function, Table) 78 | for Name, AltFunction in Table do 79 | return AltFunction == Function and Name 80 | end 81 | 82 | return nil 83 | end 84 | 85 | --[[ 86 | Sets up a `CastBehavior` for later use, 87 | then returns it. 88 | ]] 89 | function Functions.SetupBehavior(Cache, CastParams): FastCast.Behavior 90 | -- Define Variables 91 | local Behavior = FastCast.newBehavior() 92 | local Gravity = Workspace.Gravity 93 | 94 | -- Update Behavior properties 95 | Behavior.Acceleration = Vector3.new(0, -Gravity, 0) 96 | Behavior.MaxDistance = 500 97 | Behavior.RaycastParams = CastParams 98 | Behavior.CosmeticBulletProvider = Cache 99 | 100 | -- Export behavior 101 | return Behavior 102 | end 103 | 104 | --[[ 105 | Clones and parents Droplet effects from a template part. 106 | ]] 107 | function Functions.CreateEffects(Parent: MeshPart, ImpactName: string) 108 | -- Variable definitions 109 | local Trail = TrailEffects:Clone() 110 | 111 | local Attachment0 = Instance.new("Attachment") 112 | local Attachment1 = Instance.new("Attachment") 113 | local ImpactAttachment = Instance.new("Attachment") 114 | 115 | -- Update Trail-related properties 116 | Trail.Attachment0 = Attachment0 117 | Trail.Attachment1 = Attachment1 118 | 119 | Attachment1.Position = Vector3.new(0.037, 0, 0) 120 | Attachment0.Name = "Attachment0" 121 | Attachment1.Name = "Attachment1" 122 | 123 | Attachment0.Parent = Parent 124 | Attachment1.Parent = Parent 125 | Trail.Parent = Parent 126 | 127 | -- Update Impact-related properties 128 | for _, Effect in ipairs(ImpactEffects:GetChildren()) do 129 | local Clone = Effect:Clone() 130 | Clone.Parent = ImpactAttachment 131 | end 132 | 133 | ImpactAttachment.Name = ImpactName 134 | ImpactAttachment.Parent = Parent 135 | ImpactAttachment.Orientation = Vector3.new(0, 0, 0) 136 | end 137 | 138 | --[[ 139 | Returns an empty object template that's going to be used as a droplet. 140 | ]] 141 | function Functions.GetDroplet(ImpactName: string, IsDecal: boolean): {} 142 | -- Variable definitions 143 | local Droplet = Instance.new("MeshPart") 144 | 145 | -- Update properties 146 | Droplet.Size = Vector3.new(0.1, 0.1, 0.1) 147 | Droplet.Transparency = 0.25 148 | Droplet.Material = Enum.Material.Glass 149 | 150 | Droplet.Anchored = false 151 | Droplet.CanCollide = false 152 | Droplet.CanQuery = false 153 | Droplet.CanTouch = false 154 | 155 | -- Export droplet 156 | Functions.CreateEffects(Droplet, ImpactName) 157 | return Droplet 158 | end 159 | 160 | --[[ 161 | Returns a folder that handles droplets; If it doesn't exist, 162 | make a new one in Workspace.Terrain. 163 | ]] 164 | function Functions.GetFolder(Name: string): Folder 165 | -- Variable definitons 166 | local Terrain = Workspace.Terrain 167 | local DropletsFolder = (Terrain:FindFirstChild(Name) or Instance.new("Folder")) 168 | 169 | -- Update properties 170 | DropletsFolder.Name = Name 171 | DropletsFolder.Parent = Terrain 172 | 173 | -- Export folder 174 | return DropletsFolder 175 | end 176 | 177 | --[[ 178 | Returns a Vector3, given the array range. 179 | ]] 180 | function Functions.GetVector(Range: {}) 181 | -- Vector definition 182 | local Vector = Vector3.new( 183 | Random:NextNumber(Unpack(Range)), 184 | Random:NextNumber(Unpack(Range)), 185 | Random:NextNumber(Unpack(Range)) 186 | ) 187 | 188 | -- Export position with applied offset 189 | return Vector 190 | end 191 | 192 | --[[ 193 | NextNumber; Uses a global Random class, 194 | this is done for efficency. 195 | ]] 196 | function Functions.NextNumber(Minimum, Maximum): number 197 | return Random:NextNumber(Minimum, Maximum) 198 | end 199 | 200 | --[[ 201 | An efficent way of doing TweenService:Create(...) 202 | ]] 203 | function Functions.CreateTween(Object: Instance, Info: TweenInfo, Goal: {}): Tween 204 | -- Export tween 205 | return TweenService:Create(Object, Info, Goal) 206 | end 207 | 208 | --[[ 209 | Plays a sound in the given parent, 210 | used to play `End` & `Start` sounds. 211 | ]] 212 | function Functions.PlaySound(Sound: Sound, Parent: Instance) 213 | if not Sound then 214 | return 215 | end 216 | 217 | local SoundClone = Sound:Clone() 218 | SoundClone.Parent = Parent 219 | 220 | SoundClone.Ended:Connect(function() 221 | SoundClone:Destroy() 222 | end) 223 | 224 | SoundClone:Play() 225 | end 226 | 227 | --[[ 228 | Returns a random value/object from the 229 | given table. 230 | ]] 231 | function Functions.GetRandom(Table: {}) 232 | return #Table > 0 and Table[math.random(1, #Table)] 233 | end 234 | 235 | --[[ 236 | Resets the properties of the given droplet, 237 | used to return pools to be recycled. 238 | ]] 239 | function Functions.ResetDroplet(Object: Instance, Original: Instance) 240 | -- Variable definitions 241 | local Decal = Object:FindFirstChildOfClass("SurfaceAppearance") 242 | local Weld = Object:FindFirstChildOfClass("WeldConstraint") 243 | local Trail = Object:FindFirstChildOfClass("Trail") 244 | 245 | -- Reset all properties 246 | for _, Property: string in Properties do 247 | Object[Property] = Original[Property] 248 | end 249 | 250 | -- Update outsider properties 251 | if Trail then 252 | Trail.Enabled = false 253 | end 254 | 255 | if Weld then 256 | Weld:Destroy() 257 | end 258 | 259 | if Decal then 260 | Decal:Destroy() 261 | end 262 | 263 | -- Export object 264 | return Object 265 | end 266 | 267 | --[[ 268 | Manages the sequence of decals; 269 | initiates only when the Type is designated as Decals. 270 | ]] 271 | function Functions.ApplyDecal(Object: Instance, IsDecal: boolean) 272 | if not IsDecal then 273 | return 274 | end 275 | 276 | -- Variable definitions 277 | local Decal: SurfaceAppearance = Functions.GetRandom(Decals):Clone() 278 | 279 | -- Update Decal properties 280 | Decal.Parent = Object 281 | end 282 | 283 | --[[ 284 | Emits particles by looping 285 | through an attachment's children; emitting a specific 286 | amount of them using the given amount. 287 | ]] 288 | function Functions.EmitParticles(Attachment: Attachment, Amount: number) 289 | -- Variable definitions 290 | local Particles = Attachment:GetChildren() 291 | 292 | -- Emits particles 293 | for _, Particle: ParticleEmitter in Particles do 294 | if not Particle:IsA("ParticleEmitter") then 295 | continue 296 | end 297 | 298 | Particle:Emit(Amount) 299 | end 300 | end 301 | 302 | --[[ 303 | Returns the closest part within a given distance. 304 | ]] 305 | function Functions.GetClosest(Origin: BasePart, Magnitude: number, Ancestor): BasePart 306 | -- Variable definitions 307 | local Children = Ancestor:GetChildren() 308 | local ClosestPart = nil 309 | local MinimumDistance = math.huge 310 | 311 | for _, Part: BasePart in Children do 312 | local Distance = (Origin.Position - Part.Position).Magnitude 313 | 314 | local Logic = (not Part.Anchored and Origin ~= Part and Distance < Magnitude and Distance < MinimumDistance) 315 | 316 | if not Logic then 317 | continue 318 | end 319 | 320 | MinimumDistance = Distance 321 | ClosestPart = Part 322 | end 323 | 324 | -- Export closest part 325 | return ClosestPart 326 | end 327 | 328 | --[[ 329 | Provides the target angles; utilized to 330 | assign the orientation to base position or CFrame. 331 | ]] 332 | function Functions.GetAngles(IsDecal: boolean, RandomAngles: boolean): CFrame 333 | -- Variable definitions 334 | local RandomAngle = Functions.NextNumber(0, 180) 335 | local AngleX = (IsDecal and -math.pi / 2 or math.pi / 2) 336 | local AngleY = (RandomAngles and RandomAngle or 0) 337 | 338 | -- Export angles 339 | return CFrame.Angles(AngleX, AngleY, 0) 340 | end 341 | 342 | --[[ 343 | Delievers the target position; serves 344 | as a foundation that is subsequently 345 | applied with an orientation. 346 | ]] 347 | function Functions.GetCFrame(Position: Vector3, Normal: Vector3, IsDecal: boolean): CFrame 348 | -- Variable definitions 349 | local DecalOffset = (IsDecal and (Normal / 76) or Vector3.zero) 350 | 351 | local Base = (Position + DecalOffset) 352 | 353 | local Target = (Position + Normal) 354 | 355 | -- Export cframe 356 | return CFrame.new(Base, Target) 357 | end 358 | 359 | --[[ 360 | Refines the components of the given 361 | Vector3; utilized to implement modifications 362 | based on factors. 363 | ]] 364 | function Functions.RefineVectors(IsDecal: boolean, VectorData: Vector3) 365 | local YVector = (IsDecal and 0 or VectorData.Y) 366 | 367 | return Vector3.new(VectorData.X, YVector, VectorData.Z) 368 | end 369 | 370 | --[[ 371 | Weld, creates a WeldConstraint between two parts 372 | (Part0 and Part1). 373 | ]] 374 | function Functions.Weld(Part0: BasePart, Part1: BasePart): WeldConstraint 375 | -- Variable definitions 376 | local Weld = Instance.new("WeldConstraint") 377 | 378 | -- Update Part properties 379 | Part1.Anchored = false 380 | 381 | -- Update Weld properties 382 | Weld.Parent = Part1 383 | Weld.Part0 = Part0 384 | Weld.Part1 = Part1 385 | 386 | -- Export weld 387 | return Weld 388 | end 389 | 390 | --[[ 391 | Adds a connection to a table that holds connections. 392 | ]] 393 | function Functions.Connect(Connection: RBXScriptConnection, Holder: { RBXScriptConnection }) 394 | -- Update table 395 | table.insert(Holder, Connection) 396 | end 397 | 398 | --[[ 399 | Destroys and disconnects all the connections 400 | in a table that holds connections. 401 | ]] 402 | function Functions.DisconnectAll(Holder: { RBXScriptConnection }) 403 | -- Disconnect and destroy connections in Holder 404 | for Index, Connection: RBXScriptConnection in Holder do 405 | Connection:Disconnect() 406 | Holder[Index] = nil 407 | end 408 | end 409 | 410 | --[[ 411 | Basic function used to replace the initial module methods, 412 | therefore avoiding errors after deletion of the module. 413 | ]] 414 | function Functions.Replacement() 415 | warn("BLOOD-ENGINE - Attempt to call a deleted function.") 416 | end 417 | 418 | return Functions 419 | -------------------------------------------------------------------------------- /src/Operator.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | @ Description: 3 | This is the operator of the base system/class, 4 | it manages the functionality of the droplets, 5 | the events of the casts, the limit and such. 6 | ]] 7 | 8 | -- Variable definitions 9 | local ParentClass = script.Parent 10 | local Assets = ParentClass.Assets 11 | 12 | -- Asset definitions 13 | local Sounds = Assets.Sounds 14 | local Essentials = Assets.Essentials 15 | local Meshes = Assets.Meshes 16 | 17 | -- Sound definitions 18 | local EndFolder = Sounds.End:GetChildren() 19 | local StartFolder = Sounds.Start:GetChildren() 20 | 21 | -- Essential definitions 22 | local Functions = require(ParentClass.Functions) 23 | local PartCache = require(Essentials.PartCache) 24 | local Settings = require(ParentClass.Settings) 25 | local FastCast = require(Essentials.FastCast) 26 | 27 | -- Globals 28 | local Unpack = table.unpack 29 | 30 | -- Constants definition 31 | local TypeAttribute = "Type" 32 | local DecayAttribute = "Decaying" 33 | local ExpandAttribute = "Expanding" 34 | local MeshMap = { 35 | Default = Meshes.Droplet, 36 | Decal = Meshes.Decal, 37 | } 38 | 39 | -- Type definitions 40 | type Connections = { RBXScriptConnection } 41 | 42 | -- Class definition 43 | local Operator = {} 44 | Operator.__index = Operator 45 | 46 | --[[ 47 | Class constructor, constructs the class 48 | including other properties/variables. 49 | ]] 50 | function Operator.new(Class) 51 | local self = setmetatable({ 52 | Handler = Class.ActiveHandler, 53 | }, Operator) 54 | 55 | return self, self:Initialize(), self:InitializeCast() 56 | end 57 | 58 | --[[ 59 | Immediately called after the construction of the class, 60 | defines properties/variables for after-construction 61 | ]] 62 | function Operator:Initialize() 63 | -- Variable definitions 64 | local Handler: Settings.Class = self.Handler 65 | local FolderName: string = Handler.FolderName 66 | 67 | -- Essential definitions 68 | local Type = Handler.Type 69 | local Limit = Handler.Limit 70 | local CastParams = Handler.RaycastParams 71 | 72 | local Folder = Functions.GetFolder(FolderName) 73 | local Object = Functions.GetDroplet(Handler.SplashName) 74 | 75 | -- Class definitions 76 | local Cache = PartCache.new(Object, Limit, Folder) 77 | 78 | -- Insert variables 79 | Functions.MultiInsert(self, { 80 | Registry = {}, 81 | Connections = {}, 82 | 83 | Droplet = Object, 84 | Cache = Cache, 85 | Container = Folder, 86 | Caster = FastCast.new(), 87 | Behavior = function() 88 | return Functions.SetupBehavior(Cache, CastParams) 89 | end, 90 | }) 91 | end 92 | 93 | --[[ 94 | The Cast-Setup, which is executed immediately 95 | following the Initialization of the class. 96 | 97 | It efficiently manages events 98 | associated with the Caster. 99 | ]] 100 | function Operator:InitializeCast() 101 | -- Self definitions 102 | local Connections: Connections = self.Connections 103 | local Caster: FastCast.Class = self.Caster 104 | local Handler: Settings.Class = self.Handler 105 | local Container: Folder = self.Container 106 | 107 | -- Event definitions 108 | local LengthChanged = Caster.LengthChanged 109 | local RayHit = Caster.RayHit 110 | 111 | -- Caster Listeners 112 | Functions.Connect(LengthChanged:Connect(function(_, Origin, Direction, Length, _, Object: BasePart) 113 | if not Object then 114 | return 115 | end 116 | 117 | -- 3D Definition 118 | local ObjectSize = Object.Size 119 | local ObjectLength = ObjectSize.Z / 2 120 | 121 | local Offset = CFrame.new(0, 0, -(Length - ObjectLength)) 122 | 123 | local GoalCFrame = CFrame.new(Origin, Origin + Direction):ToWorldSpace(Offset) 124 | 125 | -- Update properties 126 | Object.CFrame = GoalCFrame 127 | end), Connections) 128 | 129 | Functions.Connect(RayHit:Connect(function(_, RaycastResult: RaycastResult, Velocity, Object: BasePart?) 130 | if not Object then 131 | return nil 132 | end 133 | 134 | -- Options definitions 135 | local RegistryData = self.Registry[Object] or Handler 136 | local Size = RegistryData.StartingSize 137 | local SizeRange = RegistryData.DefaultSize 138 | local YRange = RegistryData.YSize 139 | local Distance = RegistryData.Distance 140 | local Expansion = RegistryData.Expansion 141 | local IsDecal = RegistryData.Type == "Decal" 142 | 143 | -- Variable definitions 144 | local CastInstance = RaycastResult.Instance 145 | local Position = RaycastResult.Position 146 | local Normal = RaycastResult.Normal 147 | 148 | local VectorSize, SizeY = Functions.GetVector(SizeRange), Functions.NextNumber(table.unpack(YRange)) 149 | local GoalSize = Functions.RefineVectors(IsDecal, Vector3.new(VectorSize.X, VectorSize.Y / 4, VectorSize.X)) 150 | 151 | local GoalAngles = Functions.GetAngles(IsDecal, IsDecal) 152 | local GoalCFrame = Functions.GetCFrame(Position, Normal, IsDecal) * GoalAngles 153 | 154 | local ClosestPart = Functions.GetClosest(Object, Distance, Container) 155 | 156 | local ExpansionLogic = ( 157 | Expansion 158 | and ClosestPart 159 | and not ClosestPart:GetAttribute(DecayAttribute) 160 | and not ClosestPart:GetAttribute(ExpandAttribute) 161 | and ClosestPart:GetAttribute(TypeAttribute) == RegistryData.Type 162 | ) 163 | 164 | -- Clear the registry entry 165 | self.Registry[Object] = nil 166 | 167 | -- Evaluates if the droplet is close to another pool, if so, expand. 168 | if ExpansionLogic then 169 | self:Expanse(Object, ClosestPart, Velocity, GoalSize, RegistryData) 170 | return nil 171 | end 172 | 173 | -- Update properties 174 | Object.Anchored = true 175 | Object.Size = Size 176 | Object.CFrame = GoalCFrame 177 | Object.Transparency = Functions.NextNumber(Unpack(RegistryData.DefaultTransparency)) 178 | 179 | --[[ 180 | Transitions the droplet into a pool, 181 | then handles its later functionality. 182 | (Decay, Sounds, etc...) 183 | ]] 184 | Functions.CreateTween(Object, RegistryData.Tweens.Landed, { Size = GoalSize }):Play() 185 | 186 | self:HandleDroplet(Object, RegistryData) 187 | self:HitEffects(Object, Velocity, RegistryData) 188 | Functions.Weld(CastInstance, Object) 189 | 190 | return nil 191 | end), Connections) 192 | end 193 | 194 | --[[ 195 | Destroys PartCache, FastCast, 196 | and all the droplets associated with this engine/operator. 197 | ]] 198 | function Operator:Destroy() 199 | -- Self definitions 200 | local Connections: Connections = self.Connections 201 | local Cache: PartCache.Class = self.Cache 202 | local Caster: FastCast.Class = self.Caster 203 | local Container: Folder = self.Container 204 | 205 | -- Destroy classes 206 | Cache:Dispose() 207 | table.clear(Caster) 208 | 209 | Functions.DisconnectAll(Connections) 210 | table.clone(Connections) 211 | 212 | -- Destroy main container 213 | if Container then 214 | Container:Destroy() 215 | end 216 | 217 | -- Null everything, making the operator unusable 218 | self.Connections = nil 219 | self.Container = nil 220 | self.Cache = nil 221 | self.Caster = nil 222 | end 223 | 224 | --[[ 225 | Emitter, emits a certain amount of droplets, 226 | at a certain point of origin, with a certain given direction. 227 | ]] 228 | function Operator:Emit(Origin: Vector3, Direction: Vector3, Data: Settings.Class?) 229 | -- Class definitions 230 | local Caster: FastCast.Class = self.Caster 231 | local Behavior: FastCast.Behavior = self.Behavior 232 | local Cache: PartCache.Class = self.Cache 233 | local Handler: Settings.Class = self.Handler 234 | 235 | -- Create a clone of the default settings, and apply specific settings if provided 236 | local Clone = table.clone(Handler) 237 | Clone:UpdateSettings(Data or {}) 238 | Data = Clone 239 | 240 | -- Variable definitions 241 | local IsDecal = Data.Type == "Decal" 242 | local DropletVelocity = Data.DropletVelocity 243 | local Velocity = Functions.NextNumber(Unpack(DropletVelocity)) * 10 244 | 245 | local RandomOffset = Data.RandomOffset 246 | local OffsetRange = Data.OffsetRange 247 | local Position = Functions.GetVector(OffsetRange) / 10 248 | 249 | -- Final definitions 250 | local FinalPosition = Origin + Vector3.new(Position.X, 0, Position.Z) 251 | local FinalStart = (RandomOffset and FinalPosition or Origin) 252 | 253 | if #Cache.Open <= 0 then 254 | return 255 | end 256 | 257 | -- Caster definitions, fire the caster with given arguments 258 | local ActiveDroplet = Caster:Fire(FinalStart, Direction, Velocity, Behavior) 259 | 260 | local RayInfo = ActiveDroplet.RayInfo 261 | local Droplet: MeshPart = RayInfo.CosmeticBulletObject 262 | 263 | -- Update the mesh's look and color 264 | Droplet:ApplyMesh(MeshMap[Data.Type]) 265 | Droplet.Color = Data.DropletColor 266 | 267 | -- Assign the registry entry and update the attributes 268 | self.Registry[Droplet] = Data 269 | Droplet:SetAttribute(TypeAttribute, Data.Type) 270 | Droplet:SetAttribute(DecayAttribute, false) 271 | Droplet:SetAttribute(ExpandAttribute, false) 272 | 273 | -- Execute essential functions 274 | self:UpdateDroplet(Droplet, Data) 275 | Functions.PlaySound(Functions.GetRandom(StartFolder), Droplet) 276 | end 277 | 278 | --[[ 279 | A small function, designed to update the properties 280 | of a recently emitted droplet. 281 | ]] 282 | function Operator:UpdateDroplet(Object: BasePart, Data: Settings.Class) 283 | -- Variable definitions 284 | local DropletTrail = Data.Trail 285 | local DropletVisible = Data.DropletVisible 286 | local IsDecal = Data.Type == "Decal" 287 | 288 | -- Object definitions 289 | local Trail = Object:FindFirstChildOfClass("Trail") 290 | 291 | -- Update Object properties 292 | Object.Transparency = DropletVisible and 0 or 1 293 | Trail.Enabled = DropletTrail 294 | 295 | -- Execute essential functions 296 | Functions.ApplyDecal(Object, IsDecal) 297 | end 298 | 299 | --[[ 300 | Handles the given droplet/object after 301 | it landed on a surface. 302 | ]] 303 | function Operator:HandleDroplet(Object: BasePart, Data: Settings.Class) 304 | -- Object definitions 305 | local Trail = Object:FindFirstChildOfClass("Trail") 306 | 307 | -- Variable definitions 308 | local Tweens = Data.Tweens 309 | local DecayDelay = Data.DecayDelay 310 | 311 | local DecayInfo = Tweens.Decay 312 | local DecayTime = Functions.NextNumber(Unpack(DecayDelay)) 313 | 314 | local ScaleDown = Data.ScaleDown 315 | local FinalSize = ScaleDown and Vector3.new(0.01, 0.01, 0.01) or Object.Size 316 | 317 | -- Tween definitions 318 | local DecayTween = Functions.CreateTween(Object, DecayInfo, { Transparency = 1, Size = FinalSize }) 319 | 320 | -- Update Droplet properties 321 | Trail.Enabled = false 322 | 323 | -- Listeners 324 | DecayTween.Completed:Connect(function() 325 | DecayTween:Destroy() 326 | Object:SetAttribute("Decaying", nil) 327 | self:ReturnDroplet(Object) 328 | end) 329 | 330 | -- Reset the droplet after the given DecayDelay has passed 331 | task.delay(DecayTime, function() 332 | DecayTween:Play() 333 | Object:SetAttribute("Decaying", true) 334 | end) 335 | end 336 | 337 | --[[ 338 | HitEffects, a sequence of effects to enhance 339 | the visuals of the droplet->pool 340 | ]] 341 | function Operator:HitEffects(Object, Velocity: Vector3, Data: Settings.Class) 342 | -- Variable definitions 343 | local SplashName = Data.SplashName 344 | local SplashAmount = Data.SplashAmount 345 | local SplashByVelocity = Data.SplashByVelocity 346 | local Divider = Data.VelocityDivider 347 | local IsDecal = Data.Type == "Decal" 348 | 349 | local Magnitude = Velocity.Magnitude 350 | local FinalVelocity = Magnitude / Divider 351 | local FinalAmount = (SplashByVelocity and FinalVelocity or Functions.NextNumber(Unpack(SplashAmount))) 352 | local Splash: Attachment = Object:FindFirstChild(SplashName) 353 | 354 | -- Execute essential functions 355 | Splash.Orientation = Vector3.new(0, 0, IsDecal and 0 or 180) 356 | Functions.PlaySound(Functions.GetRandom(EndFolder), Object) 357 | Functions.EmitParticles(Splash, FinalAmount) 358 | end 359 | 360 | --[[ 361 | Simulates the pool expansion 362 | effect when a droplet is near 363 | a pool. 364 | 365 | It checks the distance between 366 | a threshold, then triggers changes 367 | on the droplet & pool. 368 | ]] 369 | function Operator:Expanse( 370 | Object: BasePart, 371 | ClosestPart: BasePart, 372 | Velocity: Vector3, 373 | Size: Vector3, 374 | Data: Settings.Class 375 | ) 376 | -- Variable definitions 377 | local Divider = Data.ExpanseDivider 378 | local MaximumSize = Data.MaximumSize 379 | local IsDecal = Data.Type == "Decal" 380 | 381 | -- Info definitions 382 | local Tweens = Data.Tweens 383 | local Expand = Tweens.Expand 384 | 385 | -- Value definitions 386 | local PoolSize = ClosestPart.Size 387 | local FinalVelocity = Velocity / 20 388 | local GoalSize = Vector3.new(Size.X, Size.Y / Divider, Size.Z) / Divider 389 | 390 | local FirstSize = Functions.RefineVectors( 391 | IsDecal, 392 | Vector3.new(PoolSize.X - FinalVelocity.Z, PoolSize.Y + FinalVelocity.Y, PoolSize.Z - FinalVelocity.Z) 393 | ) 394 | 395 | local LastSize = Vector3.new(PoolSize.X, PoolSize.Y, PoolSize.Z) + GoalSize 396 | 397 | local FinalSize = (LastSize.X < MaximumSize and LastSize or PoolSize) 398 | 399 | -- Update properties 400 | ClosestPart:SetAttribute("Expanding", true) 401 | ClosestPart.Size = FirstSize 402 | 403 | -- Transition to Expanded size 404 | local Tween = Functions.CreateTween(ClosestPart, Expand, { Size = FinalSize }) 405 | 406 | Tween:Play() 407 | Tween.Completed:Connect(function() 408 | ClosestPart:SetAttribute("Expanding", nil) 409 | Tween:Destroy() 410 | end) 411 | 412 | -- Execute essential functions 413 | Functions.PlaySound(Functions.GetRandom(EndFolder), ClosestPart) 414 | self:ReturnDroplet(Object) 415 | end 416 | 417 | --[[ 418 | Resets the given droplet/pool, 419 | then returns it to the Cache. 420 | ]] 421 | function Operator:ReturnDroplet(Object: Instance) 422 | -- Self definitions 423 | local Cache: PartCache.Class = self.Cache 424 | local Template: Instance = self.Droplet 425 | 426 | -- Execute essential functions 427 | Functions.ResetDroplet(Object, Template) 428 | Cache:ReturnPart(Object) -- Ignore, ReturnPart exists 429 | end 430 | 431 | -- Exports the class 432 | export type Class = typeof(Operator.new(...)) 433 | 434 | return Operator 435 | -------------------------------------------------------------------------------- /src/Settings.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | @ Description: 3 | A class that handles settings, 4 | a group of keys that have an assigned value. 5 | ]] 6 | 7 | -- Class definition 8 | local Settings = {} 9 | Settings.__index = Settings 10 | 11 | --[[ 12 | Class constructor, constructs the class 13 | including other properties/variables. 14 | ]] 15 | function Settings.new(Data: {}) 16 | local self = setmetatable({ 17 | FolderName = "Droplets", -- Specifies the name of the folder containing the droplets. 18 | Type = "Default", -- Defines the droplet type. It can be either "Default" (Sphere) or "Decal". 19 | Limit = 500, -- Sets the maximum number of droplets that can be created. 20 | Filter = {}, -- An array/table of instances that should be ignored during droplet collision. 21 | 22 | YSize = { 0.1, 0.175 }, -- Specifices the range of the thickness/flatness/depth of the pool. Lesser is flatter. 23 | DefaultSize = { 0.4, 0.7 }, -- Specifies the default size range of a pool. 24 | DefaultTransparency = { 0.3, 0.4 }, -- Specifies the default transparency range of a pool. 25 | StartingSize = Vector3.new(0.1, 0.3, 0.1), -- Sets the initial size of the droplets upon landing. 26 | ScaleDown = true, -- Determines whether the pool should scale down when decaying. 27 | 28 | DropletDelay = { 0.01, 0.03 }, -- Sets the delay between emitting droplets in a loop (for the EmitAmount method). 29 | DropletVelocity = { 1, 2 }, -- Controls the velocity of the emitted droplet. 30 | DropletVisible = false, -- Determines if the droplet is visible upon emission. 31 | DropletColor = Color3.fromRGB(103, 0, 0), -- Determines the color of the emitted droplet. 32 | 33 | RandomOffset = true, -- Determines whether a droplet should spawn at a random offset from a given position. 34 | OffsetRange = { -5, 5 }, -- Specifies the offset range for the position vectors. 35 | 36 | SplashName = "Impact", -- The name of the attachment that releases particles on surface contact. 37 | SplashAmount = { 5, 10 }, -- Sets the number of particles to emit upon impact. 38 | SplashByVelocity = true, -- If true, sets the number of particles based on the velocity of the droplet. 39 | VelocityDivider = 8, -- Controls how much the velocity can affect the splash amount, Higher values reduce the effect. 40 | 41 | Expansion = true, -- Determines whether a pool can expand when a droplet lands on it. 42 | Distance = 0.2, -- Sets the distance (in studs) within which the droplet should check for nearby pools 43 | ExpanseDivider = 3, -- Controls how much a pool's size can increase. Higher values reduce the increase. 44 | MaximumSize = 0.7, -- Sets the maximum size a pool can reach. 45 | 46 | Trail = true, -- Controls the visibility of the trail during droplet emission. 47 | DecayDelay = { 10, 15 }, -- Sets the delay before the droplet decays and recycles 48 | 49 | -- Contains all the tweens used by the module 50 | Tweens = { 51 | Landed = TweenInfo.new(0.5, Enum.EasingStyle.Cubic), -- Used for when a droplet has landed on a surface. 52 | Decay = TweenInfo.new(1, Enum.EasingStyle.Cubic), -- Used for when a droplet is decaying. 53 | Expand = TweenInfo.new(0.5, Enum.EasingStyle.Cubic), -- Used for when a droplet is expanding (Pool Expansion). 54 | }, 55 | }, Settings) 56 | 57 | -- Fill the default settings with values from the Data array 58 | for Setting, Value in Data do 59 | if Setting == "Tweens" then 60 | for Tween, Info in Value do 61 | self.Tweens[Tween] = Info 62 | end 63 | 64 | continue 65 | end 66 | 67 | self[Setting] = Value 68 | end 69 | 70 | return self, self:CreateParams() 71 | end 72 | 73 | --[[ 74 | Updates settings with values from the provided array. 75 | ]] 76 | function Settings:UpdateSettings(Data: {}) 77 | -- Variable definitions 78 | local Filter = self.Filter 79 | local Params = self.RaycastParams 80 | 81 | for Setting, Value in Data do 82 | self[Setting] = Value 83 | end 84 | 85 | -- Update Param properties 86 | Params.FilterDescendantsInstances = Filter 87 | end 88 | 89 | --[[ 90 | Manages the instantiation of the RaycastParams 91 | aswell as the configuration of the filter. 92 | ]] 93 | function Settings:CreateParams() 94 | -- Variable definitions 95 | local Filter = self.Filter 96 | local Params = RaycastParams.new() 97 | 98 | -- Update Params properties 99 | Params.FilterType = Enum.RaycastFilterType.Exclude 100 | Params.FilterDescendantsInstances = Filter 101 | 102 | -- Assign Params as a self value 103 | self.RaycastParams = Params 104 | end 105 | 106 | -- Exports the class and its type 107 | export type Class = typeof(Settings.new(...)) 108 | 109 | return Settings 110 | -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | @ Writer: @Smileeiles 3 | @ Version: v1.1.3 4 | @ Description: 5 | A droplet emitter system, 6 | used to emit droplets from a specified origin point. 7 | 8 | These droplets are then given a velocity, 9 | and upon landing on a surface, transform into pools. 10 | 11 | This process can be customized to suit various needs and genres. 12 | ]] 13 | 14 | -- Essential definitions 15 | local Operator = require(script.Operator) 16 | local Settings = require(script.Settings) 17 | local Functions = require(script.Functions) 18 | 19 | -- Globals 20 | local Unpack = table.unpack 21 | 22 | -- Class definition 23 | local BloodEngine = {} 24 | BloodEngine.__index = BloodEngine 25 | 26 | --[[ 27 | Class constructor, constructs the class 28 | including other properties/variables. 29 | ]] 30 | function BloodEngine.new(Data: Settings.Class) 31 | local self = setmetatable({}, BloodEngine) 32 | return self, self:Initialize(Data) 33 | end 34 | 35 | --[[ 36 | Immediately called after the construction of the class, 37 | defines properties/variables for after-construction 38 | ]] 39 | function BloodEngine:Initialize(Data: {}) 40 | Functions.MultiInsert(self, { 41 | ActiveHandler = Settings.new(Data or {}), 42 | ActiveEngine = function() 43 | return Operator.new(self) 44 | end, 45 | }) 46 | end 47 | 48 | --[[ 49 | Emitter, emits droplets based on given amount, 50 | origin & direction. 51 | 52 | This is utilized when you prefer 53 | not to create a loop just for the 54 | purpose of emitting a few droplets. 55 | ]] 56 | function BloodEngine:EmitAmount(Origin: Vector3 | BasePart, Direction: Vector3, Amount: number, Data: Settings.Class?) 57 | -- Class definitions 58 | local Handler: Settings.Class = self.ActiveHandler 59 | 60 | -- Variable definitions 61 | local DropletDelay = Handler.DropletDelay 62 | 63 | for _ = 1, Amount, 1 do 64 | -- Define variables for later use 65 | local DelayTime = Functions.NextNumber(Unpack(DropletDelay)) 66 | 67 | -- Emit a droplet in the specified direction & origin 68 | self:Emit(Origin, Direction, Data) 69 | 70 | -- Delays the next droplet to be emitted 71 | task.wait(DelayTime) 72 | end 73 | end 74 | 75 | --[[ 76 | EmitOnce, a variant of the Emit method; emits a single droplet. 77 | Unlike Emit, which uses a loop to emit multiple droplets, 78 | EmitOnce only emits one droplet per call. 79 | 80 | This is useful when you want to control the emission 81 | loop externally. 82 | ]] 83 | function BloodEngine:Emit(Origin: Vector3 | BasePart, Direction: Vector3, Data: Settings.Class?) 84 | -- Class definitions 85 | local Engine: Operator.Class = self.ActiveEngine 86 | 87 | -- Variable definitions 88 | Origin = typeof(Origin) == "Instance" and Origin.Position or Origin 89 | Direction = Direction or Functions.GetVector({ -10, 10 }) / 10 90 | 91 | -- Emit a single droplet 92 | Engine:Emit(Origin, Direction, Data) 93 | end 94 | 95 | --[[ 96 | GetSettings, returns all the settings of the 97 | current class instance. 98 | 99 | Use this function when you want to access 100 | the settings for external handling of the system. 101 | ]] 102 | function BloodEngine:GetSettings(): Settings.Class 103 | -- Class definitions 104 | local Handler: Settings.Class = self.ActiveHandler 105 | 106 | -- Export settings 107 | return Handler 108 | end 109 | 110 | --[[ 111 | UpdateSettings, updates the settings of the 112 | current class instance. 113 | 114 | It uses the `Handler:UpdateSettings()`, which 115 | uses the given `Data` array/table to update individual settings. 116 | ]] 117 | function BloodEngine:UpdateSettings(Data: Settings.Class) 118 | -- Class definitions 119 | local Handler: Settings.Class = self.ActiveHandler 120 | 121 | -- Update the settings 122 | Handler:UpdateSettings(Data) 123 | end 124 | 125 | --[[ 126 | Destroy, destroys anything associated 127 | with the settings/handler and the operator/engine. 128 | 129 | Use this function when there is no longer a use 130 | for the created engine. (Like when a character dies, etc.) 131 | ]] 132 | function BloodEngine:Destroy() 133 | -- Class definitions 134 | local Handler: Settings.Class = self.ActiveHandler 135 | local Engine: Operator.Class = self.ActiveEngine 136 | 137 | -- Destroy the handler & engine 138 | self.Handler = nil 139 | Engine:Destroy() 140 | self.Engine = nil 141 | 142 | -- Nullify/replace all the Engine methods 143 | self.UpdateSettings = Functions.Replacement 144 | self.GetSettings = Functions.Replacement 145 | 146 | self.EmitAmount = Functions.Replacement 147 | self.Emit = Functions.Replacement 148 | end 149 | 150 | -- Exports the class 151 | return BloodEngine 152 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rotntake/blood-engine" 3 | version = "0.1.3" 4 | realm = "shared" 5 | registry = "https://github.com/upliftgames/wally-index" 6 | licence = "MIT" 7 | authors = ["rotntake"] 8 | 9 | description = "A droplet emitter system." 10 | 11 | exclude = ["**"] 12 | include = ["src", "src/*", "default.project.json", "wally.toml"] 13 | 14 | [dev-dependencies] 15 | 16 | [dependencies] 17 | --------------------------------------------------------------------------------