Merge dev: git/CI/contribution infrastructure

Brings the contribution + CI setup to main: CI and changelog-enforcement
workflows, CONTRIBUTING, CHANGELOG, issue/PR templates, toolchain configs
(stylua/selene/luau-lsp), getting-started/extending docs, the Discussions
release cross-post, and README live-demo + sponsor sections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Samuel Lison
2026-06-18 18:14:16 +10:00
co-authored by Claude Opus 4.8
21 changed files with 904 additions and 7 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
# Shows a "Sponsor" button on the FediHome repo.
# Shows a "Sponsor" button on the SurvivorCore repo.
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
github: [TemujinCalidius]
+81
View File
@@ -0,0 +1,81 @@
name: Bug report
description: Something in SurvivorCore behaves incorrectly.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for filing a bug! Please fill in as much as you can — a clear repro is
the single biggest factor in getting it fixed quickly.
Questions and "how do I…" go to [Discussions](../../discussions), not here.
- type: textarea
id: description
attributes:
label: Description
description: What went wrong?
placeholder: When I tag a part "Gatherable" and harvest it, the prompt never disappears…
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: How can a maintainer trigger this? Number the steps.
placeholder: |
1. Serve `demo.project.json` into a place
2. Add a Part, tag it "Gatherable", set ItemId="reed", HP=3
3. Play, hold the prompt 3 times
4. …
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What should have happened instead?
validations:
required: true
- type: input
id: sc-version
attributes:
label: SurvivorCore version
description: Release tag, or the commit SHA if you're on `dev`/`main`.
placeholder: v0.1.0 (or a commit SHA)
validations:
required: true
- type: input
id: studio-version
attributes:
label: Roblox Studio version
description: Studio → File → About Roblox Studio.
placeholder: 0.6XX.0.XXXXXXX
validations:
required: true
- type: input
id: rojo-plugin-version
attributes:
label: Rojo Studio plugin version
description: The Rojo plugin version in Studio (pin to 7.6.1 to match the project).
placeholder: "7.6.1"
validations:
required: true
- type: dropdown
id: consumption
attributes:
label: How are you consuming SurvivorCore?
options:
- Rojo (rojo serve / source)
- Wally package
- Drop-in SurvivorCore.rbxm from a Release
- Other (describe below)
validations:
required: true
- type: textarea
id: output
attributes:
label: Output / errors
description: Relevant Studio Output (Server + Client). Paste as text, not a screenshot.
render: text
validations:
required: false
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions & discussion
url: https://github.com/TemujinCalidius/SurvivorCore/discussions
about: 'Ask "how do I…?", share design ideas, and show off what you built — start a Discussion.'
@@ -0,0 +1,46 @@
name: Feature request
description: Suggest a mechanic, registry, component, or hook for the engine.
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
SurvivorCore ships **mechanics, not content**. The best feature requests describe a
reusable engine capability (a registry, a component, a hook, a config section) — not
a specific item/world/art asset, which belongs in your own game via `register()` or
the component layer.
Open-ended ideas and discussion are welcome in [Discussions](../../discussions).
- type: textarea
id: description
attributes:
label: Description
description: What should SurvivorCore be able to do?
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Use case
description: Why is this useful? What kind of game needs it, and how would you use it?
validations:
required: true
- type: dropdown
id: layer
attributes:
label: Which layer does this touch?
options:
- Registry layer (register() API — Items/Recipes/Stats/Mobs/…)
- Component layer (tag + attributes on creator-owned objects)
- Hooks / EventBridge (lifecycle extension points)
- Foundation (Config / Assets / Registry plumbing)
- Not sure
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Can this already be done with a hook or component today? What did you try?
validations:
required: false
+18
View File
@@ -0,0 +1,18 @@
<!-- Thanks for contributing to SurvivorCore! -->
<!-- Code PRs target `dev`. Documentation-only PRs may target `main` (apply `skip-changelog`). -->
## Summary
<!-- What does this change do, and why? -->
## How to test in Studio
<!-- Steps to verify: which project to serve (default vs demo), what to do in Studio,
and what you should see. Include a screenshot/clip for visible behavior. -->
## Checklist
- [ ] Added an entry to **`CHANGELOG.md`** under `## Unreleased` (or applied the `skip-changelog` label if no entry is warranted — e.g. a CI-only or trivial docs change)
- [ ] CI is green — `stylua --check`, `selene`, `luau-lsp analyze`, and **both** `rojo build`s pass
- [ ] No secrets or personal data added (this is a public repo)
- [ ] **No hardcoded asset IDs** — content comes via `register()` / components / `Assets`, and the core stays content-free
+36
View File
@@ -0,0 +1,36 @@
name: Changelog
# Require every PR to update CHANGELOG.md (under "## Unreleased"), unless it
# carries the "skip-changelog" label. Keeps the changelog in lockstep with the code.
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
# Least-privilege token — read the repo + the PR's file list.
permissions:
contents: read
pull-requests: read
jobs:
changelog:
name: CHANGELOG updated
runs-on: ubuntu-latest
steps:
- name: Require a CHANGELOG.md entry (unless 'skip-changelog' label)
env:
GH_TOKEN: ${{ github.token }}
LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
if printf ',%s,' "$LABELS" | grep -q ',skip-changelog,'; then
echo "skip-changelog label present — no changelog entry required."
exit 0
fi
files=$(gh api --paginate "repos/$REPO/pulls/$PR/files" --jq '.[].filename')
if printf '%s\n' "$files" | grep -qx 'CHANGELOG.md'; then
echo "✓ CHANGELOG.md updated."
else
echo "::error::This PR doesn't update CHANGELOG.md. Add an entry under '## Unreleased', or apply the 'skip-changelog' label if no entry is warranted."
exit 1
fi
+59
View File
@@ -0,0 +1,59 @@
name: CI
on:
push:
branches: [main, dev]
pull_request:
# Least-privilege token — the job only needs to read the repo.
permissions:
contents: read
# Cancel superseded runs on the same ref to save CI minutes.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Lint, analyze & build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Installs the toolchain pinned in rokit.toml (rojo, wally, stylua, selene, luau-lsp).
- name: Install toolchain (rokit)
uses: CompeyDev/setup-rokit@v0.1.2
- name: StyLua (format check)
run: stylua --check src demo
- name: Selene (lint)
run: selene .
# luau-lsp needs a Rojo sourcemap to resolve requires + Roblox API type defs.
# The demo project mounts the engine under ReplicatedStorage.SurvivorCore, so the
# sourcemap covers both the engine (src) and the demo place that consumes it.
- name: Generate Rojo sourcemap
run: rojo sourcemap demo.project.json --output sourcemap.json
- name: Fetch Roblox type definitions
run: curl -fsSL -o globalTypes.d.luau https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.luau
- name: luau-lsp (type analysis)
run: |
luau-lsp analyze \
--sourcemap sourcemap.json \
--defs globalTypes.d.luau \
--no-strict-dm-types \
--ignore "Packages/**" \
--ignore "DevPackages/**" \
--ignore "ServerPackages/**" \
src demo
# Prove both Rojo targets build: the drop-in engine model and the runnable demo place.
- name: Build engine model (default.project.json)
run: rojo build default.project.json --output SurvivorCore.rbxm
- name: Build demo place (demo.project.json)
run: rojo build demo.project.json --output demo.rbxl
+5
View File
@@ -8,6 +8,8 @@ on:
permissions:
contents: write
# Lets the release auto-post an announcement to the Discussions category below.
discussions: write
jobs:
build:
@@ -25,3 +27,6 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: SurvivorCore.rbxm
# Cross-posts each release to Discussions → Announcements (created by default
# when Discussions is enabled). The category name must match exactly.
discussion_category_name: Announcements
+1
View File
@@ -1,5 +1,6 @@
# Rojo / Roblox
sourcemap.json
globalTypes.d.luau
*.rbxl
*.rbxlx
*.rbxl.lock
+5
View File
@@ -0,0 +1,5 @@
{
"languageMode": "nonstrict",
"lintErrors": false,
"globals": []
}
+63
View File
@@ -0,0 +1,63 @@
# Changelog
All notable changes to SurvivorCore are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project aims to follow
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). At release time, `## Unreleased`
is promoted to the new version and `main` is tagged `vX.Y.Z`.
## Unreleased
### Added
- **Continuous integration** (`.github/workflows/ci.yml`) — every push to `main`/`dev` and
every PR runs `stylua --check`, `selene`, `luau-lsp analyze` (against a Rojo sourcemap +
Roblox type defs), then builds **both** `default.project.json` (the drop-in engine model)
and `demo.project.json` (the runnable demo place) to prove they compile.
- **Changelog enforcement** (`.github/workflows/changelog.yml`) — PRs must update
`CHANGELOG.md` unless they carry the `skip-changelog` label.
- **Contributor guide** (`CONTRIBUTING.md`) — prerequisites, local dev setup, code style,
the `dev`/`main` branching model, PR flow, and the engine conventions (no hardcoded asset
IDs, content via `register()`/components, content-free core).
- **Issue & PR templates** — bug-report and feature-request issue forms (Studio / Rojo
plugin versions, repro), a PR template (changelog, CI, no secrets, no hardcoded asset IDs),
and a Discussions link for questions.
- **Toolchain pins** for `stylua`, `selene`, and `luau-lsp` added to `rokit.toml`, plus
`stylua.toml`, `selene.toml`, and `.luaurc` so local dev and CI lint/format/analyze
identically.
- **Docs** — a getting-started/install guide (Rojo + Wally and the drop-in `.rbxm`) and an
"Extending SurvivorCore" guide (the `register()` API, the component/attribute model, Hooks).
- **README** — a "See it live" pointer to [The Counter Earth](https://thecounterearth.com)
(the engine's closed-Alpha showcase) and a GitHub Sponsors section.
### Changed
- The release workflow now cross-posts each tagged release to Discussions → **Announcements**
(`discussion_category_name` + `discussions: write`).
### Fixed
- `Components` attribute reading (`src/components/init.luau`) no longer iterates an optional
union; it guards the optional first. Behavior is unchanged — this clears a `luau-lsp`
type-analysis error so CI starts green.
## 0.1.0 (2026-06-18)
### Added
- **Foundation scaffold.** The core plumbing every layer builds on:
- `Config` — engine-default tuning sections that games override via deep merge.
- `Assets` — a typed asset-id registry; the engine never hardcodes IDs.
- `EventBridge` — a semantic event bus (`fire`/`onFire`) that decouples subscribers from
sources.
- `Hooks` — lifecycle extension points (`Hooks.on("gather:depleted", …)`) so game-specific
flourish stays out of the engine.
- `Registry` — the shared register/validate/index/query lifecycle behind every registry.
- **Content registries** — empty `Items`, `Recipes` (crafting + cooking, routed by
`station`), `Stats`, `Achievements`, `Codex`, `Appearance`, and `Mobs` registries the game
populates at startup. The engine ships zero concrete content.
- **Component layer** — the creator-facing `Gatherable` component (tag your own mesh, set
`ItemId`/`Yield`/`HP` attributes) over a generic `Components.define`/`scan` framework.
- **Public API** — `SurvivorCore.start()` and the dot-callable surface
(`SurvivorCore.Items.register{…}`, `SurvivorCore.Hooks.on(…)`, …).
- **Runnable demo** (`demo/`) and a `demo.project.json` Rojo target that mounts the engine
under `ReplicatedStorage.SurvivorCore` and exercises both extension layers.
- **Release pipeline** (`.github/workflows/release.yml`) — builds `SurvivorCore.rbxm` from
source and attaches it to each `v*` tag's GitHub Release.
- **Docs & project setup** — `README.md`, `docs/architecture.md`, MIT `LICENSE`, `rokit.toml`
(rojo + wally pins), and `wally.toml` package metadata.
+193
View File
@@ -0,0 +1,193 @@
# Contributing to SurvivorCore
Thanks for your interest in contributing to SurvivorCore! This guide will help you get set up
and understand how the project is organized.
SurvivorCore is an **engine of mechanics, not content**. Most of what makes a survival game
*yours* — items, world, art, lore — lives in your own game and plugs in through the engine's
two extension layers. Contributions to this repo are about the *engine*: registries,
components, hooks, and the foundation. See [Architecture Overview](#architecture-overview).
## Prerequisites
- **Roblox Studio** — to serve the project into and play-test.
- **The Rojo Studio plugin, version `7.6.1`** — pin it to match the CLI the project uses (see
`rokit.toml`). A mismatched plugin can fail to sync. Install from the
[Rojo plugin page](https://create.roblox.com/store/asset/13916111004/Rojo) or via
`rojo plugin install`.
- **[Rokit](https://github.com/rojo-rbx/rokit)** — the toolchain manager. It installs the
exact pinned versions of `rojo`, `wally`, `stylua`, `selene`, and `luau-lsp` from
`rokit.toml`.
- **[Wally](https://wally.run)** — the Roblox package manager (installed by Rokit). The engine
ships with no dependencies today, but `wally install` keeps you forward-compatible.
- **Git** — for cloning and version control.
## Local Development Setup
1. **Fork & clone the repo:**
```bash
git clone https://github.com/<you>/SurvivorCore.git
cd SurvivorCore
```
2. **Install the toolchain** (reads `rokit.toml`):
```bash
rokit install
```
3. **Install packages** (no-op until `wally.toml` gains dependencies, but get in the habit):
```bash
wally install
```
4. **Serve into Studio.** Open a place in Studio (an empty baseplate is fine), make sure the
Rojo plugin is connected, then in your terminal:
```bash
rojo serve demo.project.json
```
Click **Connect** in the Rojo plugin. This mounts the engine at
`ReplicatedStorage.SurvivorCore` and the demo boot script in `ServerScriptService`.
5. **Run the demo place.** Press **Play**. The demo registers a couple of items and a recipe,
then watches for creator-owned `Gatherable` objects. To try the component layer: add a
`Part` to the Workspace, tag it `Gatherable` (CollectionService), set the attributes
`ItemId="reed"`, `Yield=2`, `HP=3`, then play and interact with the prompt.
> Working on the engine alone (not the demo)? `rojo serve default.project.json` mounts just
> `src` as the `SurvivorCore` model.
## Code Style
- **Luau, typed where practical.** Use `--!strict` on new modules when the types are clean;
fall back to the project default otherwise. Prefer `export type` for public shapes.
- **Formatting & linting are enforced by CI.** Before you push, run the same checks CI does
(see [Making a Pull Request](#making-a-pull-request)). `stylua` owns formatting — don't
hand-format around it.
- **Never hardcode asset IDs in `.luau`.** Asset IDs are content. Register them through
`Assets` (`Assets.register("Sounds", "Harvest", "rbxassetid://…")`) and read them back, with
the empty-string fallback convention. The engine must contain **zero** concrete asset IDs.
- **Keep the core content-free.** No concrete items, recipes, lore, world strings, or
instance-name string matches (`name == "campfire"`) in the engine. Content enters two ways:
- **Registries** — developers call `register()` from code (`Items`, `Recipes`, `Stats`,
`Mobs`, …).
- **Components** — creators tag their own objects and set Attributes (`Gatherable`, and the
component family that follows).
- **Extend via Hooks, don't fork.** Game-specific flourish (felling physics, station VFX,
custom drops) belongs in a `Hooks.on(...)` handler in the *game*, not baked into the engine.
If you need a new extension point, add a `Hooks.run("…")` call and document it.
- **Mind the two layers.** Engine code lives in `src/`; `demo/` is an example consumer and may
use concrete content freely (it stands in for a game).
## Branching model
SurvivorCore uses two long-lived branches:
- **`dev`** — the active development / integration branch. **All code changes land here.**
- **`main`** — the stable, released branch. It only moves when maintainers cut a release (by
merging `dev` → `main`) or for **documentation-only** changes.
**In short: code → `dev`, docs → `main`.**
- **Code work** (anything under `src/`, `demo/`, workflows, toolchain/build config): fork,
branch from **`dev`**, and open your PR against **`dev`**.
- **Documentation only** (`README`, `docs/`, `CONTRIBUTING.md`, code comments, typos): you may
branch from **`main`** and PR against **`main`** — apply the `skip-changelog` label.
Use branch prefixes: `feat/*`, `fix/*`, `docs/*` (e.g. `feat/mob-registry`).
`main` is the default branch, so a fresh PR targets `main` — **retarget code PRs to `dev`.** A
code PR left on `main` will be asked to retarget. Releases are cut by maintainers: `dev` is
merged into `main` (a **merge commit**, not a squash, so the branches stay in sync),
`## Unreleased` is promoted to the new version, and `main` is tagged + a GitHub Release is
published (which auto-posts to Discussions → Announcements).
## Making a Pull Request
1. **Fork** the repository on GitHub.
2. **Create a branch from the right base** — `dev` for code, `main` for docs-only:
```bash
git checkout -b feat/my-feature dev # code work
# git checkout -b docs/my-fix main # documentation only
```
3. **Implement your change.** Write clear, typed Luau. Add comments where the "why" isn't
obvious. Keep the engine content-free.
4. **Test locally.** Play-test in Studio, then run the same checks CI does:
```bash
stylua --check src demo
selene .
rojo sourcemap demo.project.json --output sourcemap.json
curl -fsSL -o globalTypes.d.luau https://raw.githubusercontent.com/JohnnyMorganz/luau-lsp/main/scripts/globalTypes.d.luau
luau-lsp analyze --sourcemap sourcemap.json --defs globalTypes.d.luau --no-strict-dm-types \
--ignore "Packages/**" --ignore "DevPackages/**" --ignore "ServerPackages/**" src demo
rojo build default.project.json --output SurvivorCore.rbxm
rojo build demo.project.json --output demo.rbxl
```
(`stylua src demo` auto-formats; `sourcemap.json`, `globalTypes.d.luau`, and the build
outputs are git-ignored.)
5. **Commit** with a clear message describing what the change does and why:
```bash
git commit -m "Add Mobs registry kill-event schema"
```
6. **Push** and open a pull request against **`dev`** (or `main` for documentation-only
changes):
```bash
git push origin feat/my-feature
```
7. In the PR description, explain what the change does, why it's needed, and how to test it in
Studio. Add a clip/screenshot for anything visible.
**Changelog (required).** Every pull request must add an entry to [`CHANGELOG.md`](CHANGELOG.md)
under the `## Unreleased` heading (create it if missing), grouped under
`### Added` / `### Changed` / `### Fixed` / `### Security`, with `(#N)` referencing the issue or
PR. CI enforces this. If a change genuinely warrants no entry (a docs-only PR, a CI-config
tweak, a typo fix), apply the `skip-changelog` label to bypass the check. At release time,
`## Unreleased` is renamed to the new version.
**Tracking staged fixes (`fixed-pending-merge`).** When a PR implements the fix for an open
issue or a security alert, maintainers label it `fixed-pending-merge` (and the issue it
closes), so it's easy to see at a glance which problems are fixed and just waiting on a merge.
The label needs no cleanup: on merge the PR closes and any linked issue auto-closes via a
`Closes #N` reference.
## Issue Templates
File issues with the forms in `.github/ISSUE_TEMPLATE/`:
- **Bug report** — include a clear repro (which project to serve and what to do in Studio),
expected behavior, the SurvivorCore version, your Roblox Studio version, your Rojo plugin
version, how you consume the engine (Rojo / Wally / drop-in `.rbxm`), and any Output errors.
- **Feature request** — describe a reusable *engine* capability (a registry, component, hook,
or config section), the use case, and which layer it touches. Game-specific content belongs
in your own game, not the engine.
Open-ended questions and "how do I…?" go to
[Discussions](https://github.com/TemujinCalidius/SurvivorCore/discussions), not Issues.
## Architecture Overview
SurvivorCore exposes two extension layers over a small foundation:
- **Foundation** (`src/foundation/`) — `Config`, `Assets`, `EventBridge`, `Hooks`, `Registry`.
- **Registry layer** (`src/registries/`) — empty registries the game populates at startup
(`Items`, `Recipes`, `Stats`, `Achievements`, `Codex`, `Appearance`, `Mobs`).
- **Component layer** (`src/components/`) — behaviors bound to a CollectionService tag and
configured by per-instance Attributes (`Gatherable`, …).
For the full picture, read [docs/architecture.md](docs/architecture.md) and the companion
[Extending SurvivorCore](docs/extending.md) guide. The engine/content **boundary map** (which
systems are being extracted from the production game [The Counter
Earth](https://github.com/TemujinCalidius/TheCounterEarth) into this engine) lives in that
game's repo at `docs/survivorcore-boundary-map.md`.
## Code of Conduct
SurvivorCore is a small project, and we want to keep the community welcoming for everyone.
- **Be kind.** Assume good intent. Disagree respectfully.
- **Be inclusive.** Welcome newcomers. Avoid jargon without explanation.
- **Be constructive.** When reviewing code, suggest improvements rather than just pointing out
problems. Explain why.
- **No harassment, discrimination, or personal attacks.** This includes issues, PRs,
Discussions, and any project communication channels.
If someone's behavior makes you uncomfortable, reach out to the maintainers. We will address it.
+13
View File
@@ -18,6 +18,13 @@ inventory, crafting, cooking, harvesting, hostile mobs & combat, achievements, a
player persistence — without dictating your *content*. You bring your own items, world, and
art; the engine wires up the behavior.
## See it live
SurvivorCore powers **[The Counter Earth](https://thecounterearth.com)** — a full survival
game built on this engine, and its live showcase. The Counter Earth is in **closed Alpha**
(not yet open to beta testers), but SurvivorCore contributors and engine devs may get early
access — a good reason to [get involved](CONTRIBUTING.md). 👀
## Two ways to extend it
**1. Programmatic — register content from code:**
@@ -75,6 +82,12 @@ demo/ -- a runnable demo place that consumes the engine
docs/architecture.md -- how the layers fit together
```
## Sponsor
SurvivorCore is free and open source. If it's useful to you — or you just want to help the
engine grow — you can support development via
[GitHub Sponsors](https://github.com/sponsors/TemujinCalidius). 💛
## License
[MIT](LICENSE) © 2026 Samuel Lison
+7
View File
@@ -50,3 +50,10 @@ stays small and universal; creativity lives at the edges.
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.
## Related docs
- [Getting Started](getting-started.md) — install via Rojo + Wally or the drop-in `.rbxm`.
- [Extending SurvivorCore](extending.md) — the `register()` API, the component/attribute
model, and Hooks.
- [CONTRIBUTING](../CONTRIBUTING.md) — local dev setup, code style, and the branching model.
+223
View File
@@ -0,0 +1,223 @@
# Extending SurvivorCore
SurvivorCore ships **mechanics, not content**. You bring the items, world, art, and rules;
the engine wires up behavior. There are three ways to plug in, in rough order of how often
you'll reach for them:
1. **[Registries](#1-registries--register-content-from-code)** — register content from code
(`Items`, `Recipes`, `Stats`, `Mobs`, …).
2. **[Components](#2-components--tag-your-own-objects)** — tag your own objects and set
Attributes; no engine-side definition needed.
3. **[Hooks](#3-hooks--react-to-engine-lifecycle-events)** — react to engine lifecycle events
to add game-specific flourish without forking the engine.
Two foundation services — [`Config`](#config--tune-the-engine) and
[`Assets`](#assets--keep-ids-out-of-code) — support all three.
Everything below assumes you've required the engine and will call `SurvivorCore.start()`
**once, from the server, after** registering your content:
```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurvivorCore = require(ReplicatedStorage:WaitForChild("SurvivorCore"))
```
---
## 1. Registries — register content from code
A registry is an empty table the engine owns and your game fills. Every registry shares the
same lifecycle — `register` a definition, it's validated and indexed by a key field, and you
query it back. The engine ships **zero** concrete entries.
```lua
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", -- "hand", "campfire", … — just a routing tag
ingredients = { { item = "reed", count = 5 } },
output = { item = "reed_basket", count = 1 },
})
```
### The registries
| Registry | Key field | What it holds |
|---|---|---|
| `Items` | `id` | Item definitions (name, stack size, …). |
| `Recipes` | `id` | Crafting **and** cooking recipes — one registry, routed by `station`. |
| `Stats` | `name` | Survival/status stat models. |
| `Achievements` | `key` | Achievement definitions. |
| `Codex` | `id` | Discoverable lore / collectible entries. |
| `Appearance` | `id` | Character appearance options. |
| `Mobs` | `id` | Creature / hostile-mob definitions. |
### Shared API (every registry)
```lua
local Items = SurvivorCore.Items
Items.register(def) -- add one; errors on missing/duplicate key
Items.registerMany({ a, b }) -- add several
Items.get("reed") -- fetch by key, or nil
Items.getAll() -- array of every def
Items.query(function(d) -- filtered array
return d.stack == 1
end)
```
Some registries add convenience helpers — e.g. recipes by station:
```lua
SurvivorCore.Recipes.forStation("campfire")
SurvivorCore.Stats.defineStat({ name = "Thirst", max = 100 }) -- alias of Stats.register
```
---
## 2. Components — tag your own objects
When the *content is a Roblox object you built*, you don't need a code-side definition at all.
Tag your mesh with a [CollectionService](https://create.roblox.com/docs/reference/engine/classes/CollectionService)
tag and set per-instance **Attributes**; the engine binds behavior to it.
The flagship component is `Gatherable`. Build any part/mesh, tag it `Gatherable`, and set:
| Attribute | Type | Meaning |
|---|---|---|
| `ItemId` | string | what it yields |
| `Yield` | number | amount per full harvest |
| `HP` | number | interactions to deplete |
That's it — the engine adds a ProximityPrompt and runs the harvest loop. Attributes can be set
in Studio's Properties panel or from code; a future builder UI will set them visually.
### Defining your own component
```lua
SurvivorCore.Components.define({
name = "Campfire",
tag = "Campfire",
attributes = { -- attribute name -> default value
FuelSeconds = 60,
Lit = false,
},
onSetup = function(instance, values)
-- `values` is the resolved attributes (instance value, else the default).
-- Wire up prompts, signals, etc. here.
end,
})
```
`SurvivorCore.start()` calls `Components.scan()` for you, which binds everything currently
tagged and keeps binding new instances as they appear. Each instance is bound once (guarded by
an internal `_scBound` attribute).
---
## 3. Hooks — react to engine lifecycle events
Hooks are where game-specific flourish lives **outside** the engine. Where a registry says
"here is my content" and a component says "here is my object," a hook says "engine, when X
happens, run my code." This is how The Counter Earth's trees physically fall and segment into
logs while SurvivorCore itself ships none of that.
```lua
SurvivorCore.Hooks.on("gather:depleted", function(ctx)
-- ctx = { instance, player, values }
spawnFallingTreePhysics(ctx.instance) -- your game's flair
grantBonusDrop(ctx.player, ctx.values.ItemId)
end)
```
`Hooks.on` returns a disconnect function:
```lua
local disconnect = SurvivorCore.Hooks.on("gather:hit", function(ctx)
-- ctx = { instance, player, values, hpLeft }
flashOutline(ctx.instance)
end)
-- later: disconnect()
```
Engine systems fire hooks with `Hooks.run("name", ctx)`. Today the `Gatherable` component
fires:
| Hook | Context |
|---|---|
| `gather:hit` | `{ instance, player, values, hpLeft }` — each interaction. |
| `gather:depleted` | `{ instance, player, values }` — final hit, before the instance is destroyed. |
More hooks land as systems are extracted (`craft:start`/`craft:end`, mob lifecycle, …).
### Hooks vs. EventBridge
- **Hooks** = "the engine is about to / just did X — creators, do your thing here." Scoped,
lifecycle-shaped extension points.
- **[EventBridge](architecture.md#foundation)** = a semantic event *bus* for things that
happened (`animal_killed`, …), which any number of decoupled subscribers (achievements,
quests, analytics, a webhook) can observe:
```lua
local disconnect = SurvivorCore.Events.onFire(function(eventType, player, data)
if eventType == "animal_killed" then
analytics:track(player, "hunt", data)
end
end)
```
---
## `Config` — tune the engine
The engine declares default tunables per section; your game overrides them via deep merge.
```lua
-- (engine declares defaults internally, e.g.)
-- Config.defineSection("Energy", { DrainPerSecond = 16, RegenPerSecond = 14 })
SurvivorCore.Config.override("Energy", { DrainPerSecond = 10 }) -- your tweak
SurvivorCore.Config.get("Energy.DrainPerSecond") -- 10
```
## `Assets` — keep IDs out of code
The engine **never** hardcodes asset IDs — your game registers them and the engine reads them
back, with a safe empty-string fallback if one's missing. This is a hard rule for engine code
and the right pattern for your content too.
```lua
SurvivorCore.Assets.register("Sounds", "Harvest", "rbxassetid://123456789")
SurvivorCore.Assets.registerCategory("Animations", {
Idle = "rbxassetid://111",
Chop = "rbxassetid://222",
})
SurvivorCore.Assets.get("Sounds", "Harvest") -- "rbxassetid://123456789"
```
---
## Putting it together
```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurvivorCore = require(ReplicatedStorage:WaitForChild("SurvivorCore"))
-- content
SurvivorCore.Items.register({ id = "reed", name = "Reed", stack = 20 })
SurvivorCore.Assets.register("Sounds", "Harvest", "rbxassetid://123456789")
-- flourish
SurvivorCore.Hooks.on("gather:depleted", function(ctx)
print(ctx.player.Name, "harvested", ctx.values.ItemId)
end)
-- go
SurvivorCore.start()
```
See [Getting Started](getting-started.md) to wire the engine into your place, and
[Architecture](architecture.md) for how the layers fit together.
+114
View File
@@ -0,0 +1,114 @@
# Getting Started
SurvivorCore is the *mechanics* of a survival game — stats, inventory, crafting, harvesting,
mobs, and more — without dictating your *content*. There are two ways to bring it into your
place: as **source** (via Rojo + Wally, the recommended path for active development) or as a
**drop-in model** (`SurvivorCore.rbxm`, the fastest way to try it).
> **Status:** early scaffold (v0.1.0). APIs will change. See the
> [README](../README.md) for what's implemented today.
## Option A — Rojo + Wally (recommended)
Best if you're actively developing and want source, types, and live sync into Studio.
### 1. Install the toolchain
SurvivorCore pins its tools with [Rokit](https://github.com/rojo-rbx/rokit). In your game's
repo, add SurvivorCore's tools to your `rokit.toml` (or copy the pins) and run:
```bash
rokit install
```
This gets you `rojo` (7.6.1) and `wally`, among others. In Studio, install the **Rojo plugin
version 7.6.1** to match.
### 2. Add SurvivorCore as a Wally dependency
> Wally publishing is planned for a later release. Until then, use the
> [Git submodule / source](#3-or-vendor-the-source) approach below, or the
> [drop-in model](#option-b--drop-in-model-survivorcorerbxm).
Once published, add it to your game's `wally.toml`:
```toml
[dependencies]
SurvivorCore = "temujincalidius/survivorcore@0.1.0"
```
Then:
```bash
wally install
```
…and map the `Packages` folder into `ReplicatedStorage` in your `*.project.json`.
### 3. …or vendor the source
Clone or submodule this repo and point your Rojo project at it:
```jsonc
// your.project.json
{
"tree": {
"ReplicatedStorage": {
"SurvivorCore": { "$path": "path/to/SurvivorCore/src" }
}
}
}
```
### 4. Boot it from a server script
```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurvivorCore = require(ReplicatedStorage:WaitForChild("SurvivorCore"))
-- Register your content (see "Extending SurvivorCore")…
SurvivorCore.Items.register({ id = "reed", name = "Reed", stack = 20 })
-- …then start the engine. Call once, from the server, after registering.
SurvivorCore.start()
```
## Option B — Drop-in model (`SurvivorCore.rbxm`)
Best for a quick look with no toolchain.
1. Download `SurvivorCore.rbxm` from the latest
[Release](https://github.com/TemujinCalidius/SurvivorCore/releases) (it's built from source
by CI on every version tag).
2. In Studio, right-click **ReplicatedStorage****Insert from File…** → pick the `.rbxm`.
You'll get a `SurvivorCore` ModuleScript.
3. Add a `Script` in **ServerScriptService**:
```lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurvivorCore = require(ReplicatedStorage:WaitForChild("SurvivorCore"))
SurvivorCore.Items.register({ id = "reed", name = "Reed", stack = 20 })
SurvivorCore.start()
```
4. Press **Play**.
## Try the demo
This repo ships a runnable demo that exercises both extension layers:
```bash
rojo serve demo.project.json
```
Connect the Rojo plugin, press **Play**, then add a `Part` to the Workspace, tag it
`Gatherable` (CollectionService), and set the attributes `ItemId="reed"`, `Yield=2`, `HP=3`.
Interact with the prompt to see the component + hook flow. The boot script is
[`demo/server/Boot.server.luau`](../demo/server/Boot.server.luau).
## Next steps
- **[Extending SurvivorCore](extending.md)** — the `register()` API, the component/attribute
model, and Hooks.
- **[Architecture](architecture.md)** — how the layers fit together.
+4
View File
@@ -1,4 +1,8 @@
# Toolchain pins (https://github.com/rojo-rbx/rokit). Run `rokit install`.
# CI installs this exact set; pin versions so local dev and CI stay identical.
[tools]
rojo = "rojo-rbx/rojo@7.6.1"
wally = "UpliftGames/wally@0.3.2"
stylua = "JohnnyMorganz/stylua@2.5.2"
selene = "Kampfkarren/selene@0.31.0"
luau-lsp = "JohnnyMorganz/luau-lsp@1.68.1"
+12
View File
@@ -0,0 +1,12 @@
# Selene lint configuration — CI runs `selene .` on every push/PR.
# Uses the Roblox standard library (globals like `game`, `task`, `typeof`, `warn`).
std = "roblox"
# Generated Wally/Rojo artifacts are not ours to lint.
exclude = ["Packages", "ServerPackages", "DevPackages"]
[lints]
# Roblox code legitimately leaves some values unused (e.g. connection handles);
# keep the high-signal lints and let the formatter own style.
empty_if = "warn"
unused_variable = "warn"
+1 -3
View File
@@ -24,9 +24,7 @@ return Components.define({
onSetup = function(instance, values)
instance:SetAttribute("_HP", values.HP)
local host = if instance:IsA("BasePart")
then instance
else instance:FindFirstChildWhichIsA("BasePart")
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
+5 -3
View File
@@ -37,9 +37,11 @@ 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
if attributes then
for attrName, default in attributes do
local v = instance:GetAttribute(attrName)
values[attrName] = if v == nil then default else v
end
end
return values
end
+12
View File
@@ -0,0 +1,12 @@
# StyLua formatting rules — CI runs `stylua --check` on every push/PR.
# Tabs match the existing source; format with `stylua src demo` before committing.
column_width = 120
line_endings = "Unix"
indent_type = "Tabs"
indent_width = 4
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
collapse_simple_statement = "Never"
[sort_requires]
enabled = false