commit 286e2f5f9163e63e2569fcaa8ee534f66a32246a Author: Samuel Lison Date: Thu Jun 18 16:30:32 2026 +1000 Scaffold SurvivorCore foundation (v0.1.0) Foundation (Config/Assets/EventBridge/Hooks/Registry), content registry family, creator-owned component layer + Gatherable example, runnable demo place, Rojo/Wally/Rokit config, and release CI. Co-Authored-By: Claude Opus 4.8 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ab22fee --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release + +# Build the drop-in .rbxm from source on every version tag and attach it to the release. +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install toolchain (rojo via rokit) + uses: CompeyDev/setup-rokit@v0.1.2 + + - name: Build engine model + run: rojo build default.project.json --output SurvivorCore.rbxm + + - name: Attach to release + uses: softprops/action-gh-release@v2 + with: + files: SurvivorCore.rbxm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3d1fd1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Rojo / Roblox +sourcemap.json +*.rbxl +*.rbxlx +*.rbxl.lock +*.rbxlx.lock + +# Build output (the .rbxm is built in CI and attached to releases) +*.rbxm +*.rbxmx +/build/ + +# Toolchain managers +/.rokit/ + +# Wally +/Packages/ +/ServerPackages/ +/DevPackages/ + +# Editor / OS +.DS_Store +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53fd51d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Samuel Lison + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e2de85 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +
+ +# SurvivorCore + +**A batteries-included, creator-extensible survival game framework for Roblox.** + +
+ +> ⚠️ **Status: early scaffold (v0.1.0).** The foundation layer and architecture are in +> place; survival systems are being extracted from a production game ([The Counter +> Earth](https://github.com/TemujinCalidius/TheCounterEarth)) into this engine. APIs will +> change. Not yet production-ready. + +## What it is + +SurvivorCore gives you the *mechanics* of a survival game — stats (energy/hunger/thirst…), +inventory, crafting, cooking, harvesting, hostile mobs & combat, achievements, a codex, and +player persistence — without dictating your *content*. You bring your own items, world, and +art; the engine wires up the behavior. + +## Two ways to extend it + +**1. Programmatic — register content from code:** + +```lua +local SurvivorCore = require(ReplicatedStorage.SurvivorCore) + +SurvivorCore.Items.register({ id = "reed", name = "Reed", stack = 20 }) +SurvivorCore.Recipes.register({ + id = "reed_basket", station = "hand", + ingredients = { { item = "reed", count = 5 } }, + output = { item = "reed_basket", count = 1 }, +}) + +SurvivorCore.start() +``` + +**2. Creator-owned — attach behavior to your own objects:** + +Build *any* mesh, tag it `Gatherable`, and set attributes — no engine-side definition needed: + +| Attribute | Meaning | +|---|---| +| `ItemId` | what it yields | +| `Yield` | amount per full harvest | +| `HP` | hits to deplete | + +The same pattern extends to craftables, huntables, farmables, and more. For advanced, +game-specific behavior (e.g. trees that physically fall and cut into pieces), hook into +engine lifecycle events instead of forking the engine: + +```lua +SurvivorCore.Hooks.on("gather:depleted", function(ctx) + -- your custom drop / VFX / physics here +end) +``` + +## Getting started + +- **Developers** — consume via [Rojo](https://rojo.space) (clone + `rojo serve`) or, once + published, as a [Wally](https://wally.run) package. +- **Drop-in** — grab `SurvivorCore.rbxm` from a [Release](../../releases) (auto-built from + source via `rojo build`), drop it into `ReplicatedStorage`, and add a short bootstrap + script that `require`s it and calls `.start()`. + +## Project layout + +``` +src/ + init.luau -- SurvivorCore root: .start() + the public API + foundation/ -- Config, Assets, EventBridge, Hooks, Registry (the core plumbing) + registries/ -- Items, Recipes, Stats, Achievements, Codex, Appearance, Mobs + components/ -- creator-facing tag/attribute components (e.g. Gatherable) +demo/ -- a runnable demo place that consumes the engine +docs/architecture.md -- how the layers fit together +``` + +## License + +[MIT](LICENSE) © 2026 Samuel Lison diff --git a/default.project.json b/default.project.json new file mode 100644 index 0000000..5ab023c --- /dev/null +++ b/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "SurvivorCore", + "tree": { + "$path": "src" + } +} diff --git a/demo.project.json b/demo.project.json new file mode 100644 index 0000000..413e1f4 --- /dev/null +++ b/demo.project.json @@ -0,0 +1,19 @@ +{ + "name": "SurvivorCore Demo", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "SurvivorCore": { + "$path": "src" + } + }, + "ServerScriptService": { + "Demo": { + "$path": "demo/server" + } + }, + "Workspace": { + "$ignoreUnknownInstances": true + } + } +} diff --git a/demo/server/Boot.server.luau b/demo/server/Boot.server.luau new file mode 100644 index 0000000..a3d9cd8 --- /dev/null +++ b/demo/server/Boot.server.luau @@ -0,0 +1,30 @@ +--[[ + Demo boot script. Shows both extension layers: + 1. registering content from code, and + 2. reacting to creator-owned components via hooks. + + Try it: serve `demo.project.json` into a place, add a Part to Workspace, tag it + "Gatherable" (CollectionService), set attributes ItemId="reed", Yield=2, HP=3, + then play and interact with it. +]] + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local SurvivorCore = require(ReplicatedStorage:WaitForChild("SurvivorCore")) + +-- 1. Programmatic content +SurvivorCore.Items.register({ id = "reed", name = "Reed", stack = 20 }) +SurvivorCore.Items.register({ id = "reed_basket", name = "Reed Basket", stack = 1 }) +SurvivorCore.Recipes.register({ + id = "reed_basket", + station = "hand", + ingredients = { { item = "reed", count = 5 } }, + output = { item = "reed_basket", count = 1 }, +}) + +-- 2. React to creator-owned Gatherables +SurvivorCore.Hooks.on("gather:depleted", function(ctx) + print(("[demo] %s fully gathered %s"):format(ctx.player.Name, tostring(ctx.values.ItemId))) +end) + +SurvivorCore.start() +print("SurvivorCore demo booted — v" .. SurvivorCore.VERSION) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..c92a169 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,52 @@ +# SurvivorCore — Architecture + +> Companion to the boundary map in the TheCounterEarth repo +> (`docs/survivorcore-boundary-map.md`), which tracks what is being extracted from the +> production game into this engine. + +## The shape + +SurvivorCore is an **engine of mechanics, not content**. It exposes two extension layers +over a small foundation. + +``` + ┌─────────────────────────────────────────────┐ + Creators → │ Component layer (tag + attributes + UI) │ "I own the object" + ├─────────────────────────────────────────────┤ + Developers → │ Registry layer (register() from code) │ "I own the data" + ├─────────────────────────────────────────────┤ + │ Foundation: Config · Assets · EventBridge · │ + │ Hooks · Registry │ + └─────────────────────────────────────────────┘ +``` + +### Foundation +- **Config** — engine ships default tunables per section; games override via deep merge. +- **Assets** — typed asset-id registry; the engine never hardcodes ids. +- **EventBridge** — semantic event bus (`fire`/`onFire`); subscribers decouple from sources. +- **Hooks** — lifecycle extension points (`Hooks.on("craft:start", ...)`). +- **Registry** — the shared register/validate/index/query lifecycle behind every registry. + +### Registry layer (developers) +Empty registries the game populates at startup: `Items`, `Recipes` (crafting + cooking are +one registry routed by `station`), `Stats`, `Achievements`, `Codex`, `Appearance`, `Mobs`. + +### Component layer (creators) +Behaviors bound to a CollectionService tag, configured by per-instance Attributes. Creators +tag their **own** meshes (`Gatherable`, and later craftable/huntable/farmable/station +components) and fill in values — optionally through a builder UI. No engine-side definition +required. + +## Why hooks instead of baked-in behavior + +Game-specific flourish stays out of the engine. TheCounterEarth's trees physically fall and +segment into logs — SurvivorCore will **not** ship that. Instead it fires +`gather:hit` / `gather:depleted` (and similar) hooks; a creator (including TheCounterEarth +itself, going forward) implements the felling physics in their own hook handler. The engine +stays small and universal; creativity lives at the edges. + +## Status & roadmap + +v0.1.0 is the foundation scaffold. Extraction order (from the boundary map): foundations → +pure-core lift → registries → SPLIT server systems (incl. the mob/combat cluster) → UI +layer → harvesting/wildlife sub-engine. diff --git a/rokit.toml b/rokit.toml new file mode 100644 index 0000000..f9e5eea --- /dev/null +++ b/rokit.toml @@ -0,0 +1,4 @@ +# Toolchain pins (https://github.com/rojo-rbx/rokit). Run `rokit install`. +[tools] +rojo = "rojo-rbx/rojo@7.6.1" +wally = "UpliftGames/wally@0.3.2" diff --git a/src/components/Gatherable.luau b/src/components/Gatherable.luau new file mode 100644 index 0000000..a3dc688 --- /dev/null +++ b/src/components/Gatherable.luau @@ -0,0 +1,55 @@ +--[[ + Gatherable — the flagship creator-owned component. + + A creator builds ANY part/mesh, tags it "Gatherable", and sets attributes: + • ItemId (string) — what it yields + • Yield (number) — amount per full harvest + • HP (number) — interactions to deplete + + No engine-side content needed: the creator owns the object. Advanced behavior + (custom drops, VFX, falling physics) attaches via Hooks rather than editing core. +]] + +local Components = require(script.Parent) +local Hooks = require(script.Parent.Parent.foundation.Hooks) + +return Components.define({ + name = "Gatherable", + tag = "Gatherable", + attributes = { + ItemId = "unknown", + Yield = 1, + HP = 3, + }, + onSetup = function(instance, values) + instance:SetAttribute("_HP", values.HP) + + local host = if instance:IsA("BasePart") + then instance + else instance:FindFirstChildWhichIsA("BasePart") + if not host then + warn(`[Gatherable] '{instance:GetFullName()}' has no BasePart to host a prompt`) + return + end + + local prompt = Instance.new("ProximityPrompt") + prompt.ActionText = "Gather" + prompt.ObjectText = tostring(values.ItemId) + prompt.HoldDuration = 0.4 + prompt.Parent = host + + prompt.Triggered:Connect(function(player) + local hp = (instance:GetAttribute("_HP") or 1) - 1 + instance:SetAttribute("_HP", hp) + + -- TODO (extraction): grant `values.Yield` of `values.ItemId` via the + -- inventory service once persistence/inventory are extracted. + Hooks.run("gather:hit", { instance = instance, player = player, values = values, hpLeft = hp }) + + if hp <= 0 then + Hooks.run("gather:depleted", { instance = instance, player = player, values = values }) + instance:Destroy() + end + end) + end, +}) diff --git a/src/components/init.luau b/src/components/init.luau new file mode 100644 index 0000000..6205c22 --- /dev/null +++ b/src/components/init.luau @@ -0,0 +1,69 @@ +--[[ + Components — the creator-facing layer. A component binds engine behavior to a + CollectionService tag, reading per-instance Attributes (with defaults). Creators + tag their OWN objects and fill in attributes; no engine-side definition required. + + Components.define({ + name = "Gatherable", + tag = "Gatherable", + attributes = { ItemId = "unknown", Yield = 1, HP = 3 }, + onSetup = function(instance, values) ... end, + }) + + Call Components.scan() once (SurvivorCore.start does this) to bind existing tagged + instances and watch for new ones. A future builder UI can set these attributes + visually instead of by hand. +]] + +local CollectionService = game:GetService("CollectionService") + +local Components = {} + +export type Spec = { + name: string, + tag: string, + attributes: { [string]: any }?, -- attribute name -> default value + onSetup: ((instance: Instance, values: { [string]: any }) -> ())?, +} + +local defined: { [string]: Spec } = {} + +function Components.define(spec: Spec): Spec + assert(spec.name and spec.tag, "Component requires `name` and `tag`") + assert(defined[spec.name] == nil, `Component '{spec.name}' already defined`) + defined[spec.name] = spec + return spec +end + +local function readAttributes(instance: Instance, attributes: { [string]: any }?) + local values = {} + for attrName, default in attributes or {} do + local v = instance:GetAttribute(attrName) + values[attrName] = if v == nil then default else v + end + return values +end + +local function bind(instance: Instance, spec: Spec) + if instance:GetAttribute("_scBound") then + return + end + instance:SetAttribute("_scBound", true) + if spec.onSetup then + spec.onSetup(instance, readAttributes(instance, spec.attributes)) + end +end + +-- Bind everything currently tagged, then keep binding new instances as they appear. +function Components.scan() + for _, spec in defined do + for _, instance in CollectionService:GetTagged(spec.tag) do + task.spawn(bind, instance, spec) + end + CollectionService:GetInstanceAddedSignal(spec.tag):Connect(function(instance) + bind(instance, spec) + end) + end +end + +return Components diff --git a/src/foundation/Assets.luau b/src/foundation/Assets.luau new file mode 100644 index 0000000..2895ebf --- /dev/null +++ b/src/foundation/Assets.luau @@ -0,0 +1,34 @@ +--[[ + Assets — typed asset-id lookup. The engine NEVER hardcodes asset ids; games + register theirs and the engine reads them back, with a safe fallback. + + Assets.register("Sounds", "Harvest", "rbxassetid://123") + Assets.get("Sounds", "Harvest") -- "rbxassetid://123", or "" + warning if missing +]] + +local Assets = {} + +local store: { [string]: { [string]: string } } = {} + +function Assets.register(category: string, key: string, assetId: string) + store[category] = store[category] or {} + store[category][key] = assetId +end + +function Assets.registerCategory(category: string, map: { [string]: string }) + for key, id in map do + Assets.register(category, key, id) + end +end + +function Assets.get(category: string, key: string): string + local cat = store[category] + local id = cat and cat[key] + if not id or id == "" then + warn(`[SurvivorCore.Assets] missing asset '{category}.{key}' — using fallback`) + return "" + end + return id +end + +return Assets diff --git a/src/foundation/Config.luau b/src/foundation/Config.luau new file mode 100644 index 0000000..3dd4b95 --- /dev/null +++ b/src/foundation/Config.luau @@ -0,0 +1,58 @@ +--[[ + Config — engine ships default tuning per section; games override via deep merge. + + Config.defineSection("Energy", { DrainPerSecond = 16, RegenPerSecond = 14 }) + Config.override("Energy", { DrainPerSecond = 10 }) -- game tweak + Config.get("Energy.DrainPerSecond") -- 10 +]] + +local Config = {} + +local sections: { [string]: any } = {} + +local function deepCopy(t: any): any + if typeof(t) ~= "table" then + return t + end + local c = {} + for k, v in t do + c[k] = deepCopy(v) + end + return c +end + +local function deepMerge(base: any, over: any) + for k, v in over do + if typeof(v) == "table" and typeof(base[k]) == "table" then + deepMerge(base[k], v) + else + base[k] = deepCopy(v) + end + end +end + +-- Engine code calls this to declare a section with its default values. +function Config.defineSection(name: string, defaults: { [string]: any }) + assert(sections[name] == nil, `Config section '{name}' already defined`) + sections[name] = deepCopy(defaults) +end + +-- Game code calls this to override engine defaults. +function Config.override(name: string, overrides: { [string]: any }) + assert(sections[name] ~= nil, `Config section '{name}' not defined`) + deepMerge(sections[name], overrides) +end + +-- Dotted lookup: Config.get("Energy.DrainPerSecond") +function Config.get(path: string): any + local node: any = sections + for part in string.gmatch(path, "[^.]+") do + if typeof(node) ~= "table" then + return nil + end + node = node[part] + end + return node +end + +return Config diff --git a/src/foundation/EventBridge.luau b/src/foundation/EventBridge.luau new file mode 100644 index 0000000..0f2114a --- /dev/null +++ b/src/foundation/EventBridge.luau @@ -0,0 +1,34 @@ +--[[ + EventBridge — the engine's event bus. Systems fire semantic events; anything + (achievements, quests, analytics, an external webhook) can subscribe with zero + changes to the firing code. + + local disconnect = EventBridge.onFire(function(eventType, player, data) ... end) + EventBridge.fire("animal_killed", player, { species = "deer" }) +]] + +local EventBridge = {} + +type Listener = (eventType: string, player: Player?, data: { [string]: any }) -> () + +local listeners: { Listener } = {} + +function EventBridge.onFire(callback: Listener): () -> () + table.insert(listeners, callback) + return function() + local i = table.find(listeners, callback) + if i then + table.remove(listeners, i) + end + end +end + +function EventBridge.fire(eventType: string, player: Player?, data: { [string]: any }?) + local payload = data or {} + for _, cb in listeners do + task.spawn(cb, eventType, player, payload) + end + -- TODO (extraction): optional HTTP forwarding + batched queue, endpoint from Config. +end + +return EventBridge diff --git a/src/foundation/Hooks.luau b/src/foundation/Hooks.luau new file mode 100644 index 0000000..6106304 --- /dev/null +++ b/src/foundation/Hooks.luau @@ -0,0 +1,42 @@ +--[[ + Hooks — lifecycle extension points. Where EventBridge is "something happened", + Hooks is "the engine is about to / just did X — creators, do your thing here". + + Hooks.on("craft:start", function(ctx) ctx.station:lightFire() end) + Hooks.on("craft:end", function(ctx) ringBell(ctx.station) end) + -- inside the engine: + Hooks.run("craft:start", { station = station, recipe = recipe, player = player }) + + This is how game-specific flourish (tree-felling physics, station VFX, custom drops) + stays OUT of the engine while remaining first-class. +]] + +local Hooks = {} + +type Hook = (...any) -> () + +local hooks: { [string]: { Hook } } = {} + +function Hooks.on(name: string, callback: Hook): () -> () + hooks[name] = hooks[name] or {} + table.insert(hooks[name], callback) + return function() + local list = hooks[name] + local i = list and table.find(list, callback) + if i then + table.remove(list, i) + end + end +end + +function Hooks.run(name: string, ...) + local list = hooks[name] + if not list then + return + end + for _, cb in list do + task.spawn(cb, ...) + end +end + +return Hooks diff --git a/src/foundation/Registry.luau b/src/foundation/Registry.luau new file mode 100644 index 0000000..80bb739 --- /dev/null +++ b/src/foundation/Registry.luau @@ -0,0 +1,69 @@ +--[[ + Registry — the shared lifecycle behind every content registry: register a def, + validate it, index it by a key, and query it. Returns a plain table of functions + so the public API is dot-callable: SurvivorCore.Items.register({ ... }). + + local Items = Registry.new("Items", { keyField = "id" }) + Items.register({ id = "reed", name = "Reed" }) + Items.get("reed") +]] + +local Registry = {} + +export type Options = { + keyField: string?, -- unique id field (default "id") + validate: ((def: any) -> (boolean, string?))?, +} + +function Registry.new(name: string, options: Options?) + local opts = options or {} + local keyField = opts.keyField or "id" + local validate = opts.validate + + local byId: { [any]: any } = {} + local all: { any } = {} + + local self = {} + self.name = name + + function self.register(def: any): any + local key = def[keyField] + assert(key ~= nil, `[{name}] def is missing key field '{keyField}'`) + assert(byId[key] == nil, `[{name}] '{tostring(key)}' is already registered`) + if validate then + local ok, err = validate(def) + assert(ok, `[{name}] '{tostring(key)}' is invalid: {err or "?"}`) + end + byId[key] = def + table.insert(all, def) + return def + end + + function self.registerMany(defs: { any }) + for _, def in defs do + self.register(def) + end + end + + function self.get(key: any): any + return byId[key] + end + + function self.getAll(): { any } + return all + end + + function self.query(predicate: (def: any) -> boolean): { any } + local out = {} + for _, def in all do + if predicate(def) then + table.insert(out, def) + end + end + return out + end + + return self +end + +return Registry diff --git a/src/init.luau b/src/init.luau new file mode 100644 index 0000000..0cfdfad --- /dev/null +++ b/src/init.luau @@ -0,0 +1,58 @@ +--[[ + SurvivorCore — root module. + + local SurvivorCore = require(ReplicatedStorage.SurvivorCore) + SurvivorCore.Items.register({ id = "reed", name = "Reed" }) + SurvivorCore.start() + + Two extension layers: + • Programmatic registries (Items, Recipes, Stats, Mobs, ...) — register content from code. + • Creator components (tag your own object "Gatherable", set attributes) + lifecycle Hooks. +]] + +local Config = require(script.foundation.Config) +local Assets = require(script.foundation.Assets) +local EventBridge = require(script.foundation.EventBridge) +local Hooks = require(script.foundation.Hooks) +local Registries = require(script.registries) +local Components = require(script.components) + +local SurvivorCore = {} + +SurvivorCore.VERSION = "0.1.0" + +-- Foundation +SurvivorCore.Config = Config +SurvivorCore.Assets = Assets +SurvivorCore.Events = EventBridge +SurvivorCore.Hooks = Hooks + +-- Content registries +SurvivorCore.Items = Registries.Items +SurvivorCore.Recipes = Registries.Recipes +SurvivorCore.Stats = Registries.Stats +SurvivorCore.Achievements = Registries.Achievements +SurvivorCore.Codex = Registries.Codex +SurvivorCore.Appearance = Registries.Appearance +SurvivorCore.Mobs = Registries.Mobs + +-- Creator-facing component layer +SurvivorCore.Components = Components + +local started = false + +-- Boot the engine. Call once, from the server, after registering content. +function SurvivorCore.start(_options: { [string]: any }?) + assert(not started, "SurvivorCore.start() called twice") + started = true + + -- Load built-in components so their tags are recognised. + require(script.components.Gatherable) + + -- TODO (extraction): boot order — Config merge → Assets → persistence → systems. + Components.scan() + + return SurvivorCore +end + +return SurvivorCore diff --git a/src/registries/init.luau b/src/registries/init.luau new file mode 100644 index 0000000..95f8542 --- /dev/null +++ b/src/registries/init.luau @@ -0,0 +1,31 @@ +--[[ + The content registry family. Each is an empty registry the game populates at startup; + the engine ships zero concrete items/recipes/lore/ids. + + NOTE: Crafting and cooking are ONE registry — `station` ("hand", "campfire", ...) is + just a routing tag, per the boundary-map decision. +]] + +local Registry = require(script.Parent.foundation.Registry) + +local Registries = {} + +Registries.Items = Registry.new("Items", { keyField = "id" }) +Registries.Recipes = Registry.new("Recipes", { keyField = "id" }) +Registries.Stats = Registry.new("Stats", { keyField = "name" }) +Registries.Achievements = Registry.new("Achievements", { keyField = "key" }) +Registries.Codex = Registry.new("Codex", { keyField = "id" }) +Registries.Appearance = Registry.new("Appearance", { keyField = "id" }) +Registries.Mobs = Registry.new("Mobs", { keyField = "id" }) + +-- Documented alias: Stats.defineStat(model) == Stats.register(model) +Registries.Stats.defineStat = Registries.Stats.register + +-- Convenience: all recipes for a given station. +function Registries.Recipes.forStation(station: string) + return Registries.Recipes.query(function(r) + return r.station == station + end) +end + +return Registries diff --git a/wally.toml b/wally.toml new file mode 100644 index 0000000..64b79e8 --- /dev/null +++ b/wally.toml @@ -0,0 +1,11 @@ +[package] +name = "temujincalidius/survivorcore" +description = "Batteries-included, creator-extensible survival game framework for Roblox." +version = "0.1.0" +license = "MIT" +authors = ["Samuel Lison"] +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" +exclude = ["demo", "docs", "*.project.json"] + +[dependencies]