mirror of
https://github.com/TemujinCalidius/SurvivorCore.git
synced 2026-08-14 00:58:01 +00:00
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:
+1
-1
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,5 +1,6 @@
|
||||
# Rojo / Roblox
|
||||
sourcemap.json
|
||||
globalTypes.d.luau
|
||||
*.rbxl
|
||||
*.rbxlx
|
||||
*.rbxl.lock
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"languageMode": "nonstrict",
|
||||
"lintErrors": false,
|
||||
"globals": []
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user