{"id":"ch1kz/bloodfx","name":"bloodfx","scope":"ch1kz","platform":"roblox","description":"Part-based blood: drops that fly and raycast, marks that merge into pools, pools that drip","version":"0.1.3","latest":"0.1.3","versions":["0.1.1","0.1.2","0.1.3"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"6d40b56c3fda8026bdc8feb272e9bc8d02d9ba7e52d3c6aacca4a703aea40a5d","likes":0,"downloads":0,"install":"forest install ch1kz/bloodfx","url":"https://forest.dev/p/roblox/ch1kz/bloodfx","files":"https://api.forest.dev/ai/package/roblox/ch1kz/bloodfx/files","readme":"# BloodFX\n\nPart-based blood for Roblox. Drops fly under gravity and raycast their own path; where they land\nthey leave marks, stretched along the way they came in, and marks that land on each other merge\ninto pools that stay on the surface they lie on. Pools on ceilings and overhangs drip. \n\nRun it on the client: it is a local effect, and parts made on the server replicate to everyone.\n\n## Getting started\n\nWith [Wally](https://wally.run), add it to the dependencies in `wally.toml` and run `wally install`:\n\n```toml\n[dependencies]\nBloodFX = \"ch1kz/bloodfx@0.1.3\"\n```\n\nWithout Wally, put the `BloodFX` module in `ReplicatedStorage`. Then require it from a `LocalScript`:\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal BloodFX = require(ReplicatedStorage:WaitForChild(\"BloodFX\"))\n```\n\nA module installed with Wally sits in the `Packages` folder instead, so the path there is\n`ReplicatedStorage.Packages.BloodFX`.\n\nThen call it wherever blood should appear, for example where a raycast hit something:\n\n```lua\nBloodFX.spray(result.Position, result.Normal, \"Hit\")\n```\n\nThe module sets itself up on the first `require`: it builds its part pool, starts its loop on\n`Heartbeat` and, with `DynamicSettings` on, puts its settings on `workspace.BloodFX.Config`. Every\nside that requires it gets a copy of its own, with its own folder, settings and blood.\n\n## Layout\n\n```\nsrc/ReplicatedStorage/BloodFX           the module\n    init          entry point: initialisation, public API, runtime loop\n    Config        every setting in one table\n    Types         public types, re-exported by the module\n    Pool          the parts drops and marks are drawn from\n    Grid          spatial hash that finds the mark a drop landed in\n    Marks         blood on surfaces: merging, fitting to the surface, dripping, fading\n    Drops         blood in flight: spray cone, gravity, raycast, landing\n    Emitter       a continuous source on a part, attachment or CFrame\n    Debugger      reports a pool that runs out and limits that are hit\n    Presets       every preset in one table\nsrc/ReplicatedStorage/Tests             BloodFX.spec, the tests; BloodFX.perf, benchmarks and profiler\nsrc/StarterPlayer/StarterPlayerScripts  BloodDemo, the demo for the test place; BloodDebug, a\n                                        debugger listener that warns\ndefault.project.json                    the module alone, as Wally ships it\ndev.project.json                        the test place: rojo serve dev.project.json\n```\n\n## API\n\n```lua\nlocal BloodFX = require(ReplicatedStorage.BloodFX)\n\nBloodFX.spray(result.Position, result.Normal, \"Hit\")\nBloodFX.spray(result.Position, result.Normal, \"Hit\", 10)\nBloodFX.spray(result.Position, result.Normal, \"Hit\", { count = NumberRange.new(12, 20) })\n\nlocal wound = BloodFX.emitter(character.UpperTorso, \"Fountain\", { beats = 2 })\nwound:destroy()\n```\n\n| Call | Does |\n| --- | --- |\n| `spray(position, direction, preset?, overrides?)` | Throws a cone of drops. `preset` is a preset name, a table of the same shape, or nil for `Hit`; `overrides` replaces some of its values for this call, and a number there is the drop count. |\n| `mark(position, normal, radius?)` | Puts a mark straight onto a surface, with a radius of 0.25 studs if none is given. |\n| `emitter(origin, preset?, overrides?)` | Starts a continuous source and returns its `Emitter`. `origin` is a `PVInstance`, an `Attachment` or a `CFrame`; nil preset is `Drip`, and `overrides` works as for `spray`. |\n| `get(key)` / `set(key, value)` | Reads or changes a setting by name. `set` needs `Config.DynamicSettings` and checks the type. |\n| `clear()` / `fade()` | Removes all blood at once, or starts every mark fading out now. |\n| `freeze()` / `unfreeze()` / `isFrozen()` | Switches blood off and back on. Freezing clears what is there and silences emitters without removing them. It is the `Frozen` setting, so the attribute on the Config shows it and can flip it too. |\n| `stats()` | `drops`, `marks`, `emitters`, `parts` and `spare` right now; `parts` counts every pooled part, in use or spare, and `spare` the ones waiting to be used. |\n| `debugger` | Reports when the pool runs out of spare parts and when a limit pushes out a drop or a mark; see [Debugger](#debugger). |\n\nAn `Emitter` carries every field of its preset, plus `origin`, `enabled`, `strength` (a multiplier on\nthe speed of each emission) and `age` in seconds, and each can be changed while it runs. `emit()`\nfires once regardless of the rate, `frame()` is where it sits now, and `destroy()` removes it. An\nemitter on an instance is destroyed together with that instance.\n\nPreset names and setting names are typed as `PresetName` and `SettingName`, so Studio offers them in\nautocomplete wherever the API asks for one. The module exports those two alongside `Config`,\n`DebugEvent`, `Debugger`, `Emitter`, `Origin`, `Overrides`, `Preset`, `Spray` and `Stats`.\n\n## Presets\n\nA preset is a spray shape - `count`, `speed`, `spread` in degrees, `radius` - and, for an emitter,\nhow it runs: `direction` in the origin's space, `rate` per second and `rateJitter`. `count`, `speed`\nand `radius` take a number or a `NumberRange` to draw each value from. Every preset\nworks with both `spray` and `emitter`; an emitter fills what a preset leaves out (one emission a\nsecond, upwards).\n\n| One-off | | Emitter | |\n| --- | --- | --- | --- |\n| `Hit` | a round landing in something | `Drip` | slow drops straight down |\n| `Burst` | more of it, thrown wider | `Jet` | a tight pressure jet |\n| | | `Fountain` | an arterial jet with a pulse |\n\nAll of them live in one table in `Presets.luau`; to add a preset, add an entry there.\n\n### Overrides\n\nAny call that takes a preset also takes overrides: a table with some of the preset's fields, which\nreplace the preset's values for that call only. The preset itself never changes. A number in place of\nthe table is the drop count.\n\n```lua\nBloodFX.spray(position, normal, \"Hit\", 10)\nBloodFX.spray(position, normal, \"Burst\", { spread = 90, speed = 30 })\nBloodFX.emitter(wound, \"Drip\", { rate = 14 })\n```\n\n### Logic in a preset\n\nA preset can carry its own logic and values. `update(emitter, time)` runs every frame for an emitter,\nwith the seconds since it started, and may change anything on it: rate, direction, strength, even\n`enabled`. Any other field of the preset lands on the emitter for `update` to read, and overrides\nreach those fields like any other.\n\n`Fountain` keeps its heartbeat that way. It has `beats` a second and a `swing`, and its `update` turns\nthem into `strength` on a sine, so `{ beats = 2 }` gives a faster pulse and `{ swing = 0 }` none.\nBecause that `update` writes `strength` every frame, change the pulse through `beats` and `swing`\nrather than through `strength` itself.\n\nThe same way a preset can do things no built-in field covers. This one is a bleed that slows to a stop\nover `fade` seconds:\n\n```lua\nBloodFX.emitter(part, \"Drip\", {\n\tfade = 8,\n\tupdate = function(emitter, time)\n\t\temitter.rate = 5 * math.max(0, 1 - time / emitter.fade)\n\tend,\n})\n```\n\n## How it works\n\n### Drops in flight\n\n`spray` throws `count` drops from a point, times `DropMultiplier`: at 0.5 a spray of 10 throws 5. A\ncount that does not come out whole is rounded up or down at random, so 7 drops at 0.5 are 3 or 4 and\n3.5 on average, and an emitter keeps its average flow.\n\nEach drop leaves in a random direction inside a cone of `spread` degrees around `direction`: turned\nat random around the aim, then tilted off it by an angle drawn evenly between 0 and `spread`. Even in\nangle is not even in area, so drops crowd the middle of the cone and thin out towards its rim, the way\na real splash does. Speeds are drawn from `speed` and multiplied by the emitter's `strength`; sizes\nare drawn from `radius`, leaning towards the small end, so most drops are fine and the odd one is big.\n\nEvery frame a drop falls by `Gravity` and slows by `Drag`: it keeps e^-Drag of its speed each second,\nworked out exactly for the length of the frame, so the flight does not change with the frame rate.\nIt then casts a ray along the stretch it is about to cover, and wherever that ray hits is where it\nlands. A drop that hits nothing is removed after `DropLifetime` seconds.\n\n`MaxDrops` caps how many drops are in the air at once, and the oldest makes room for a new one. Drops\nthat stay up longer - low `Gravity`, high sprays - fill the cap sooner, so when drops start vanishing\nin mid-air, the cap is the first thing to raise.\n\n### Landing\n\nWhat a drop does when it lands depends on what it hit: an absorbing surface swallows it, a part that\ncan move gets a mark welded to it (see [Marks on moving parts](#marks-on-moving-parts)), and anything\nelse gets a plain anchored mark. The mark's radius is the drop's radius times a random `MarkScale`, and it opens from about a\nthird of its size over `MarkOpenTime`.\n\nA drop that comes in at a slant smears. A real stain's width over its length is the sine of the angle\nit hit at, so BloodFX stretches the mark along the drop's path by that much, up to three times as\nlong as it is wide. `SlantStretch` scales that: 0 keeps marks round, 1 is true to life, more\noverdoes it. On top of it every mark gets a random length against width from `MarkStretch`, so no\ntwo are the same circle; `SlantStretch` 0 with `MarkStretch` 1 to 1 draws exact circles.\n\nMarks are flattened sphere meshes rather than cylinders: a cylinder part is always round at the\nsmaller of its two widths, so it could never be oval.\n\n### Merging into pools\n\nWhen a drop lands, BloodFX first looks for a mark it can join instead of making a new one. A mark\nqualifies if\n\n- it faces the same way, within about 45 degrees,\n- it lies in the same plane, within a quarter of a stud, so a drop on a shelf never joins the floor\n  below it, and\n- the drop lands close enough to its centre: within the mark's radius times `MergeReach`.\n\n`MergeReach` is measured in pool radii. At 1 only drops that land on a pool join it. Above 1 a pool\nalso catches drops that land just past its rim - at 1.35, up to a third of its radius beyond - so\nblood that lands next to a pool runs into it. Below 1 a drop has to land well inside the pool to\njoin, and drops near the rim start marks of their own that overlap it, which gives the pool a bumpy\nedge. At 0 nothing merges.\n\nA drop that joins makes the pool bigger. Areas add up, so a pool of radius R that takes in a drop\nwhose own mark would have had radius r grows to\n\n```\n√(R² + (r × MergeGrowth)²)\n```\n\n`MergeGrowth` is how much of the drop's stain the pool gains, as a share of its radius. At 1 the pool\ngains the stain's whole area, as if the two lay side by side. Lower values stand for blood that sinks\ninto a deeper pool instead of spreading thin: at 0.3 the pool gains 0.3² = 9% of the stain's area, so\na pool of radius 1 needs about 70 drops with 0.4-stud stains to double its area. At 0 pools never grow\nand joining drops only keep them fresh.\n\nEvery joining drop restarts the pool's lifetime, so a pool that is still being fed never fades. A pool\nstops growing at `MarkMaxRadius` - less on walls and ceilings, below - or where its rim would hang\nover an edge.\n\n### Walls and ceilings\n\nBlood runs down a wall and falls off a ceiling instead of pooling, so the tilt of a surface decides\nhow much blood merges on it. Each surface gets a pooling factor: 1 on level ground, `WallPooling` on\nan upright wall and `CeilingPooling` on a ceiling. In between it blends by the cosine of the tilt, so\nslopes land between the floor and the wall and overhangs between the wall and the ceiling. There is\nno threshold: a 30° ramp pools almost like the floor and a 150° overhang almost like the ceiling.\n\nThe factor scales both halves of merging on that surface. The reach becomes `MergeReach × pooling` and\nthe largest pool `MarkMaxRadius × pooling`. At 0 every drop on that surface stays a splatter of its\nown; at 1 it pools like the floor.\n\nFor example, with `WallPooling` 0.6, `CeilingPooling` 0.2, `MergeReach` 1.35 and `MarkMaxRadius` 2.5:\n\n| Surface | Tilt | Pooling | Reach, in pool radii | Largest pool radius |\n| --- | ---: | ---: | ---: | ---: |\n| floor | 0° | 1 | 1.35 | 2.5 |\n| ramp | 45° | 0.88 | 1.19 | 2.21 |\n| wall | 90° | 0.6 | 0.81 | 1.5 |\n| overhang | 135° | 0.32 | 0.43 | 0.79 |\n| ceiling | 180° | 0.2 | 0.27 | 0.5 |\n\nOn that wall the reach is under 1, so drops merge only when they land well inside a pool, and a jet\nleaves a few larger pools among separate splatters. On that ceiling pools barely grow at all.\n\n### Fitting to the surface\n\nBefore a mark is placed, and every time a pool grows, eight short rays probe points around its rim.\nIf any of them finds nothing within a quarter of a stud of the surface, that edge would hang in the\nair: a new mark halves its size until it fits, and a pool stops growing. That keeps blood on a\npedestal's top instead of floating past its edge. Eight probes keep the worst overshoot, along the\ndiagonals of a square top, to about 8% of the radius.\n\n### Dripping from ceilings\n\nWith `CeilingDrip` on, pools on surfaces tilted past `CeilingDripAngle` let drops fall. The angle is\nmeasured from the floor, so 90 is a wall and 180 a ceiling. A pool on a flat ceiling drips\n`CeilingDripRate` drops a second on average; the less its surface overhangs, the slower it drips,\ndown to nothing at `CeilingDripAngle`. Drips come at random intervals, so neighbouring pools do not\ntick in step.\n\nEach drip is a merge in reverse: the pool gives back the area a joining drop of that size would have\nadded, which is where `MergeGrowth` comes in again. A pool shrinks as it drips and stops once it is\ndown to the stain one drip would leave, so small splatters never drip at all. On an overhang the drop\nleaves from the low side of the pool, where the blood would run to, and it falls, lands and pools like\nany other drop.\n\n### Lifetime and limits\n\nA mark lasts `MarkLifetime` seconds, restarted by every drop that joins it, and then fades out over\n`MarkFadeTime`. `fade()` starts that fade for every mark at once. `MaxMarks` caps the number of marks;\npast it, the mark left alone longest makes room.\n\nDrops and marks draw their parts from a pool of `PoolSize` spare parts made at start. If more are\nneeded, extras are made on the spot and destroyed when they come back to a full pool; with `PoolSize`\nat `MaxDrops` plus `MaxMarks`, as by default, that never happens. No part casts a shadow or takes\npart in collisions, touches or raycasts, and all but the welded marks are anchored.\n\nEvery part is drawn with the mesh in `Mesh`, by default a public BloodFX mesh: a low-poly sphere of\n96 triangles against the 378 of the engine's own. Flattened into a mark it looks the same, an oval\nof 24 sides. The mesh loads in the background: until it arrives the pool hands out the engine's\nspheres, and then swaps its spare parts over a few frames and each sphere in use as it comes back.\nIf the mesh cannot load, blood stays spheres and a warning says why; `Mesh = \"\"` keeps them from\nthe start.\n\n`PoolSize` can change while the game runs, to make room before a big fight or give the memory back\nin a lobby. The pool then builds or destroys spare parts over the following frames, a millisecond's\nwork per frame at most, so even a big change causes no hitch. Parts in use are never taken: when the\npool shrinks below them, they are destroyed as they come back.\n\n## Tags\n\nThree CollectionService tags change what happens when a drop reaches something. A tag works on a\npart or on a whole model, folder or character: whatever sits inside a tagged instance counts too.\n\n| Tag | Drops that reach it |\n| --- | --- |\n| `BloodIgnore` | fly straight through, as if it was not there |\n| `BloodAbsorb` | vanish without leaving a mark |\n| `BloodAttach` | leave marks welded to the part they hit, so the marks move with it. Loose parts do that without the tag; see [Marks on moving parts](#marks-on-moving-parts) |\n\nThe tag names are settings too, `IgnoreTag`, `AbsorbTag` an","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/ch1kz/roblox/bloodfx/0.1.3/readme"}