{"id":"hurreman/roblox-core","name":"roblox-core","scope":"hurreman","platform":"roblox","description":"Shared building blocks for hurreman's Roblox games: ordered bootstrap, Maid, Signal, number formatting and validation, daily-reset clock, Heartbeat registry, microprofiler labels, buffs, collision groups and a sound manager","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"4f16381e51548996d08ee3cb3483e6a31b208918a8eb9bea17785f5536b124da","likes":0,"downloads":0,"install":"forest install hurreman/roblox-core","url":"https://forest.dev/p/roblox/hurreman/roblox-core","files":"https://api.forest.dev/ai/package/roblox/hurreman/roblox-core/files","readme":"# roblox-core\n\nSmall shared building blocks for hurreman's Roblox games. Each module is taken from the game whose copy was best, and generalized so it needs no game modules.\n\n```toml\n# wally.toml\n[dependencies]\nCore = \"hurreman/roblox-core@0.1.0\"\n```\n\n```lua\nlocal Core = require(ReplicatedStorage.Packages.Core)\nlocal Format = Core.Format -- modules load on first access\n```\n\nModules load lazily: requiring the package doesn't run the client-only SoundManager on a server, and doesn't connect Buffs' server listeners on a client.\n\n## Modules\n\n| Module | Where it runs | From | What it does |\n|---|---|---|---|\n| `Bootstrap` | server, client | Magic Neighborhood `init.server` / `init.client` | Explicit ordered service init; lifecycle hooks in their own ordered lists, checked both ways; client start list with slow-start warnings |\n| `Maid` | any | Slime Nebula `Maid.lua` | Teardown of connections, instances, threads, functions, disposables and promises; labels; debug dump |\n| `Signal` | any | Slime Nebula's GoodSignal (stravant, MIT), plus `Once` | Yield-safe signal with RBXScriptSignal semantics |\n| `Format` | any | Magic Neighborhood `Format` | `Number` (truncating suffixes up to Vg, then scientific), `Rate`, `Money`, `Robux`, `Multiplier`, `Clock`, `Stopwatch` |\n| `Numbers` | any | Slime Nebula `NumberUtils` | NaN-safe `IsFinite`, `ToFiniteInteger`, `SanitizeCount`, `RoundToNearest` |\n| `ResetClock` | any | Slime Nebula `StockholmTime`, generalized | Daily-reset calendar math for a UTC offset plus an EU or US daylight-saving rule; `ResetClock.Stockholm`, `ResetClock.UTC` |\n| `Heartbeat` | any | Slime Nebula `RunServiceHandler` | One Heartbeat connection for all per-frame callbacks, with disposers and slow-callback warnings |\n| `Profile` | any | Magic Neighborhood `Profile` | `Profile.Wrap(label, fn)`: microprofiler scopes that survive yielding handlers; `Profile.prefix` |\n| `Buffs` | server | Slime Nebula `BuffManager`, groups configurable | Stacking timed buffs published as `<prefix><Stat>` and `<prefix><Stat>_Duration` attributes |\n| `CollisionGroups` | server | Magic Neighborhood `CollisionGroups` | `Setup({ groups, noCollide })` |\n| `SoundManager` | client | Slime Nebula + Magic Neighborhood | Registry-driven playback: categories, mute, cooldowns, sequences, random pools, random pitch, 3D roll-off, looped music that follows volume, Studio keepalive, preload |\n\n### Bootstrap\n\n```lua\n-- src/server/init.server.luau\nlocal Bootstrap = require(ReplicatedStorage.Packages.Core).Bootstrap\n\nlocal app = Bootstrap.Server({\n\tfolder = script.Services,\n\tinit = { \"DataService\", \"PlotService\", \"SyncService\", \"ShopService\" },\n\thooks = {\n\t\tonPlayerReady = { \"PlotService\", \"ShopService\" }, -- (player, profile)\n\t\tonPlayerRemoving = { \"PlotService\" },             -- (player)\n\t},\n})\napp:Init() -- each service's init(app.Services), in init order\n\nPlayers.PlayerAdded:Connect(function(player)\n\tlocal profile = app.Services.DataService.load(player)\n\tapp:Run(\"onPlayerReady\", player, profile)\nend)\n```\n\n```lua\n-- src/client/init.client.luau\nBootstrap.Start({ script.Store, script.Controllers.SoundController, function() ... end })\n```\n\n`Server()` fails at boot in these cases:\n- a service has a hook method but isn't in that hook's list;\n- a listed service lacks the hook;\n- a name is listed twice;\n- a module is missing.\n\nThe player lifecycle itself (data load, plot assignment, spawning) stays in the game's entry script.\n\n### Buffs\n\n```lua\nlocal buffs = Core.Buffs.new({ prefixes = { Food = \"Food_\", GamePass = \"GamePass_\" }, default = \"Food\" })\nbuffs:Add(player, \"Speed\", 4, 30)                       -- Food_Speed = 4, Food_Speed_Duration = 30\nbuffs:Add(player, \"Speed\", 2, 60, { group = \"GamePass\", onExpired = fn })\nbuffs:Split(player, \"Speed\")                            -- { Food = 4, GamePass = 2, total = 6 }\n```\n\n### SoundManager\n\n```lua\nlocal SoundManager = Core.SoundManager\nSoundManager.Configure({\n\tregistry = require(ReplicatedStorage.Shared.Audio.SoundRegistry),\n\tstartMuted = false,                 -- true: muted until saved settings unmute it\n\tvolumes = { Music = 0.5 },          -- over Master 1, Music 0.6, Ambient 0.7, SFX 1\n\tdefaultParent = SoundService,       -- where 2D sounds go\n})\nSoundManager.Play(\"Coin\")\nSoundManager.Play(\"Hit\", nil, { Parent = part })        -- 3D, with the entry's rollOffMin/Max\n```\n\nThese are the registry entry fields:\n\n| Field | Meaning |\n|---|---|\n| `id`, or `ids` with `random` / `sequence` (plus `sequenceTimeout`, `reverse`) | Which asset plays |\n| `volume`, `category` | Base volume and its volume category |\n| `looped` | Loops, following later volume and mute changes |\n| `cooldown` | Minimum seconds between plays of this entry |\n| `playbackSpeed` / `pitch` | Fixed playback speed |\n| `randomPitch` with `minPitch` / `maxPitch` (or `pitchRange`) | Random pitch within a range |\n| `rollOffMin` / `rollOffMax` | 3D distance falloff |\n\n## Moving the games over\n\n- **Slime Nebula:** most of these become one-line re-exports of the package:\n  - `Maid.lua` and `ClientModules/UI/Signal.lua` re-export `Core.Maid` and `Core.Signal`. Leave ZonePlus's own `Zone/Signal` alone.\n  - `StockholmTime.lua` becomes `return Core.ResetClock.Stockholm`. This was checked hour by hour across 2025–2027 by `tests/specs/ResetClock.spec.luau`.\n  - `RunServiceHandler.lua` becomes `return Core.Heartbeat`.\n  - `NumberUtils.lua` re-exports `Core.Numbers` and keeps its own `FormatNumber`. Note that `Core.Format.Number` truncates while `FormatNumber` rounds (1999 shows \"1.99K\", not \"2K\"), so switching it is a separate decision.\n  - `BuffManager.lua` keeps its API as a thin shim over a `Core.Buffs` set with SN's four prefixes. `AddTempBuff` maps to `Add` (`opts.prefix` becomes `opts.group`), `RemoveBuff` to `Remove`, `GetCurrentBuffs` to `Current`, `GetStatSplit` to `Split`, and `_tick` to `Tick`.\n  - `ClientModules/Audio/SoundManager.lua` becomes `Core.SoundManager`, plus `Configure({ registry = SoundRegistry, startMuted = true, defaultParent = workspace })` before the loading screen applies settings.\n  - The bootstrap doesn't change: SN keeps ModuleRegistry.\n- **Magic Neighborhood:**\n  - `init.server.luau` and `init.client.luau` build their lists with `Bootstrap.Server` / `Bootstrap.Start`.\n  - In `Format`, rename `number`→`Number`, `rate`→`Rate`, `cash`→`Money`, `mult`→`Multiplier`, `clock`→`Clock`, `raceTime`→`Stopwatch`, `robux`→`Robux`. `candy` stays as a local one-liner.\n  - `Profile` becomes `Core.Profile` with `Profile.prefix = \"TOTD.\"`.\n  - `CollisionGroups.setup()` becomes `Core.CollisionGroups.Setup({...})`.\n  - `shared/Audio/SoundManager.luau` becomes `Core.SoundManager` with `Configure({ registry = SoundRegistry })`.\n- **Steal an Egg:** the same moves as Magic Neighborhood for `init.server` / `init.client` and `Format`. Its `client/Sounds.luau` has a different API (`play`, `at`, `on`, `attach`, `loop`); move its cue table into a SoundManager registry when that file is next touched.\n\n## Developing\n\n```bash\nlune run tests/run    # specs under tests/specs, on hurreman/luau-spec (DevPackages, else ../luau-spec)\n```\n\nThe ResetClock spec also checks against `../SlimeNebula`'s StockholmTime when that checkout exists. SoundManager needs real Sound instances, so it was verified in Studio rather than under Lune. That run covered: start-muted, cooldown, looped music following SetVolume and mute immediately (even under deferred SignalBehavior), sequences and ping-pong, random pools, the random pitch range, `PlayAssetId`, 3D roll-off and `defaultParent`.\n","readmeTruncated":false}