From 3d76332c38b5621f415aadd0d4cf2205c478335a Mon Sep 17 00:00:00 2001 From: Samuel Lison Date: Thu, 18 Jun 2026 18:05:33 +1000 Subject: [PATCH 1/5] Add StyLua/Selene/luau-lsp toolchain and make the scaffold lint-clean Pin stylua, selene, and luau-lsp in rokit.toml so local dev and CI use the same versions, and add their configs (stylua.toml, selene.toml, .luaurc). Two behavior-preserving source touches make the existing scaffold pass: guard the optional iteration in Components.readAttributes (clears a luau-lsp type error) and normalize Gatherable.luau to StyLua's canonical form. Ignore the CI-fetched globalTypes.d.luau. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 1 + .luaurc | 5 +++++ rokit.toml | 4 ++++ selene.toml | 12 ++++++++++++ src/components/Gatherable.luau | 4 +--- src/components/init.luau | 8 +++++--- stylua.toml | 12 ++++++++++++ 7 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 .luaurc create mode 100644 selene.toml create mode 100644 stylua.toml diff --git a/.gitignore b/.gitignore index e3d1fd1..c534f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Rojo / Roblox sourcemap.json +globalTypes.d.luau *.rbxl *.rbxlx *.rbxl.lock diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..fd4d760 --- /dev/null +++ b/.luaurc @@ -0,0 +1,5 @@ +{ + "languageMode": "nonstrict", + "lintErrors": false, + "globals": [] +} diff --git a/rokit.toml b/rokit.toml index f9e5eea..1a33a9f 100644 --- a/rokit.toml +++ b/rokit.toml @@ -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" diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..ea74587 --- /dev/null +++ b/selene.toml @@ -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" diff --git a/src/components/Gatherable.luau b/src/components/Gatherable.luau index a3dc688..f9935b1 100644 --- a/src/components/Gatherable.luau +++ b/src/components/Gatherable.luau @@ -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 diff --git a/src/components/init.luau b/src/components/init.luau index 6205c22..e3f8e64 100644 --- a/src/components/init.luau +++ b/src/components/init.luau @@ -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 diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..86229bf --- /dev/null +++ b/stylua.toml @@ -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 From 6b202b8b6c15a0c84fca6bba31542acd38afc1c8 Mon Sep 17 00:00:00 2001 From: Samuel Lison Date: Thu, 18 Jun 2026 18:05:33 +1000 Subject: [PATCH 2/5] Add CI + changelog workflows; auto-post releases to Discussions ci.yml (push to main/dev + all PRs): rokit install, then stylua --check, selene, luau-lsp analyze (against a Rojo sourcemap + Roblox type defs), and build both default.project.json and demo.project.json. Least-privilege permissions + concurrency cancel-in-progress, mirroring FediHome. changelog.yml: require CHANGELOG.md in a PR's files unless skip-changelog. release.yml: add discussions: write + discussion_category_name: Announcements so each tagged release cross-posts to Discussions. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/changelog.yml | 36 ++++++++++++++++++++ .github/workflows/ci.yml | 59 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 5 +++ 3 files changed, 100 insertions(+) create mode 100644 .github/workflows/changelog.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..10b14cd --- /dev/null +++ b/.github/workflows/changelog.yml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..17cff8b --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab22fee..c9642f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 From 75ecf50f6d6ccaded40ec61e6d52ade729cd282c Mon Sep 17 00:00:00 2001 From: Samuel Lison Date: Thu, 18 Jun 2026 18:05:33 +1000 Subject: [PATCH 3/5] Add PR + issue templates PR template: Summary / How to test in Studio / checklist (changelog, CI green, no secrets, no hardcoded asset IDs). Bug-report and feature-request issue forms adapted to Roblox (Studio + Rojo plugin versions, repro, which layer), plus a config.yml routing questions to Discussions. Fix stray FediHome comment in FUNDING.yml. Co-Authored-By: Claude Opus 4.8 --- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 81 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 46 ++++++++++++ .github/pull_request_template.md | 18 +++++ 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d73e413..646a9d0 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -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] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..1cb3289 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..487da66 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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.' diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..8a2140e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -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 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..99c1983 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ + + + +## Summary + + + +## How to test in Studio + + + +## 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 From 4436a4bbd624f25ab4398194dd723592329f9bc7 Mon Sep 17 00:00:00 2001 From: Samuel Lison Date: Thu, 18 Jun 2026 18:05:33 +1000 Subject: [PATCH 4/5] Add CONTRIBUTING, CHANGELOG, and getting-started/extending docs CONTRIBUTING mirrors FediHome's structure for the Roblox stack: prerequisites (Studio + Rojo plugin 7.6.1, rokit, wally), local dev, code style (no hardcoded asset IDs, content via register()/components, content-free core), the dev/main branching model, PR flow, changelog rule, and label conventions. Seed CHANGELOG.md (Keep a Changelog) with Unreleased + a 0.1.0 foundation entry. Add getting-started (Rojo+Wally and drop-in .rbxm) and extending (register() API, components, Hooks) guides; cross-link from architecture.md. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 61 +++++++++++ CONTRIBUTING.md | 193 ++++++++++++++++++++++++++++++++++ docs/architecture.md | 7 ++ docs/extending.md | 223 ++++++++++++++++++++++++++++++++++++++++ docs/getting-started.md | 114 ++++++++++++++++++++ 5 files changed, 598 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/extending.md create mode 100644 docs/getting-started.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..68bd3ea --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# 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). + +### 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9e93179 --- /dev/null +++ b/CONTRIBUTING.md @@ -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//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. diff --git a/docs/architecture.md b/docs/architecture.md index c92a169..8d1c01a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. diff --git a/docs/extending.md b/docs/extending.md new file mode 100644 index 0000000..480f35a --- /dev/null +++ b/docs/extending.md @@ -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. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..d14b1cc --- /dev/null +++ b/docs/getting-started.md @@ -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. From f407056588399d849bfe39a55fb6e15e0731e46f Mon Sep 17 00:00:00 2001 From: Samuel Lison Date: Thu, 18 Jun 2026 18:14:05 +1000 Subject: [PATCH 5/5] README: add live-demo pointer and sponsor section Point to The Counter Earth (thecounterearth.com) as the engine's live showcase (closed Alpha; contributors may get early access) and add a GitHub Sponsors section at the bottom. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 2 ++ README.md | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68bd3ea..79925bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ is promoted to the new version and `main` is tagged `vX.Y.Z`. 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** diff --git a/README.md b/README.md index 3e2de85..2eb3e10 100644 --- a/README.md +++ b/README.md @@ -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