mirror of
https://github.com/TemujinCalidius/SurvivorCore.git
synced 2026-08-14 00:58:01 +00:00
feat: quests + achievements — the goals & progression release (#10)
Quests (#10): a Quests registry + server runtime — objectives (gather/craft/ kill/use × count, blank target = any) and rewards, autoStart + requires chains (completing a prerequisite auto-starts dependents), optional turn-in at a tagged QuestGiver (component + prompt). Rewards are never lost: a full inventory parks the quest as ready and the grant retries on inventory change. Quests menu tab (L) with per-objective progress bars; QuestLog JSON attribute replicates state; quest:started/progress/completed/blocked via Hooks + bus. Achievements: an always-on runtime for the existing registry, ported architecturally from TCE. A shared Progression layer translates bus events into auto-derived counters (gathers_reed, crafts_total, kills_husk, …) so a def is just { key, name, counter, threshold } — flat in code and no-code. Unlock-once + toast + Achievements tab (J) with progress bars. Toasts: themed top-right notification queue (Notify remote + Toasts.show). No-code: admin plugin gains Quests (single-objective + "+ Quest giver" drop) and Achievements (counter/threshold) editors; the engine loads both from SurvivorCoreContent. Demo: a 3-quest chain + 4 achievements + a giver post. Fixed: registerPanel now ADOPTS a template-scaffolded tab (hides the placeholder, builds into the authored frame) instead of silently no-opping — this replaces the Quests/Achievements "coming soon" placeholders. EventBridge parity: gather:*, craft:* and item:use now also cross the bus. Changed: rojo 7.6.1 → 7.7.0 (rokit pin; CI follows; gate verified). Docs: quests.md + achievements.md (new), content-authoring/admin-plugin/ extending/README updated, CHANGELOG Unreleased, site feature card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0892bf948e
commit
9caa888369
@@ -0,0 +1,79 @@
|
||||
# Achievements
|
||||
|
||||
The achievement system ([`src/systems/Achievements.luau`](../src/systems/Achievements.luau)) tracks
|
||||
**milestones**: always-on counters that unlock a badge (once) when they cross a threshold — with a
|
||||
toast and an Achievements menu tab. The architecture is ported from The Counter Earth's proven
|
||||
achievement service, made content-free.
|
||||
|
||||
> Progress is **session-scoped** (Player attributes) — persistence (DataStore) is a future system.
|
||||
|
||||
## Defining an achievement
|
||||
|
||||
Defs are **flat** — the same shape in code and [no-code](content-authoring.md):
|
||||
|
||||
```lua
|
||||
SurvivorCore.Achievements.register({
|
||||
key = "husk_slayer",
|
||||
name = "Husk Slayer",
|
||||
description = "Slay 3 husks.",
|
||||
counter = "kills_husk", -- which counter unlocks it (see the catalogue below)
|
||||
threshold = 3,
|
||||
-- icon = "rbxassetid://…",
|
||||
})
|
||||
```
|
||||
|
||||
## The Progression stream
|
||||
|
||||
One shared layer ([`src/systems/Progression.luau`](../src/systems/Progression.luau)) subscribes to
|
||||
the EventBridge and translates gameplay events into `(player, kind, target, amount)` progress —
|
||||
consumed by **both** achievements (counters) and [quests](quests.md) (objectives). Built-in:
|
||||
|
||||
| Event | kind | target | amount |
|
||||
|---|---|---|---|
|
||||
| `gather:hit` | `gather` | the item id | amount granted |
|
||||
| `craft:end` | `craft` | the output item id | output count |
|
||||
| `mob:died` (with a killer) | `kill` | the mob type | 1 |
|
||||
| `item:use` | `use` | the item id | 1 |
|
||||
| `quest:completed` | `quest` | the quest id | 1 |
|
||||
|
||||
### Counter catalogue (the naming rule)
|
||||
|
||||
Every progress tick bumps `"<kind>s_total"` and `"<kind>s_<target>"`:
|
||||
|
||||
`gathers_total` · `gathers_reed` · `crafts_total` · `crafts_reed_basket` · `kills_total` ·
|
||||
`kills_husk` · `uses_total` · `uses_berry` · `quests_total` · `quests_<questId>` — author any
|
||||
achievement against any of these, no code.
|
||||
|
||||
### Custom events & counters (code)
|
||||
|
||||
```lua
|
||||
-- Teach the stream a game event (then author achievements against boss counters):
|
||||
SurvivorCore.Progression.map("myGame:bossDown", function(player, data)
|
||||
return "kill", data.bossId, 1
|
||||
end)
|
||||
|
||||
-- Or bump a bespoke counter / award directly:
|
||||
SurvivorCore.Achievements.addCount(player, "shrines_visited", 1)
|
||||
SurvivorCore.Achievements.award(player, "secret_cave")
|
||||
```
|
||||
|
||||
## Runtime API
|
||||
|
||||
`SurvivorCore.Achievements` (registry always; runtime ops after `start()`): `award(player, key)`,
|
||||
`addCount(player, counterId, n?)`, `isUnlocked(player, key)`, `getState(player)`. Unlocks fire the
|
||||
`achievement:unlocked` hook + EventBridge event and a toast (config-gated).
|
||||
|
||||
## The Achievements tab & data
|
||||
|
||||
The menu's **Achievements** tab (key **J**) lists every def with a progress bar toward its
|
||||
threshold; unlocked rows go gold. Defs replicate via `SurvivorCoreAchievementData`; per-player state
|
||||
is the `AchievementState` JSON attribute (`{ c = { [counter] = n }, u = { [key] = true } }`) — both
|
||||
readable via [`src/shared/AchievementData.luau`](../src/shared/AchievementData.luau).
|
||||
|
||||
## Tuning
|
||||
|
||||
`Config.override("Achievements", { Toasts = true })`.
|
||||
|
||||
---
|
||||
|
||||
See also: [Quests](quests.md) · [No-code content](content-authoring.md) · [Extending](extending.md).
|
||||
@@ -5,11 +5,13 @@ Attributes in the Explorer. It adds two toolbar buttons under **SurvivorCore**:
|
||||
|
||||
- **Survival Stats** — tune the survival-stat rates/thresholds/HUD on the `SurvivalStatsConfig`
|
||||
instance (the deltas-only, locked model below).
|
||||
- **Content** — create/edit/delete **items**, **weapons**, **gatherable resources** and **mobs** with
|
||||
no code (the Builder slice). It writes `SurvivorCoreContent` instances the engine loads at `start()`
|
||||
— see [content-authoring.md](content-authoring.md). Gatherables and mobs each get a **+ Add to
|
||||
World** button that drops the tagged instance in front of the camera. Unlike the stats editor,
|
||||
content is full owner-authored defs (not deltas). Every edit is one Studio undo step.
|
||||
- **Content** — create/edit/delete **items**, **weapons**, **arrows**, **gatherable resources**,
|
||||
**mobs**, **quests** and **achievements** with no code (the Builder slice). It writes
|
||||
`SurvivorCoreContent` instances the engine loads at `start()` — see
|
||||
[content-authoring.md](content-authoring.md). Gatherables/mobs get **+ Add to World**, weapons
|
||||
**+ Tool model**, quests **+ Quest giver** — each drops the tagged instance in front of the
|
||||
camera. Unlike the stats editor, content is full owner-authored defs (not deltas). Every edit is
|
||||
one Studio undo step.
|
||||
|
||||
The rest of this page covers the Survival Stats editor; both install the same way. It's the
|
||||
[Builder / Admin plugin](https://github.com/TemujinCalidius/SurvivorCore/issues/11).
|
||||
|
||||
@@ -47,6 +47,20 @@ ReplicatedStorage
|
||||
│ • faction = "hostile" ("hostile" | "passive" | "neutral")
|
||||
│ • health = 60
|
||||
│ • aggroRange = 40
|
||||
├─ Quests (Folder) ← flat single-objective quests (normalized at load)
|
||||
│ └─ gather_reeds (Configuration)
|
||||
│ • name = "Gather Reeds"
|
||||
│ • objectiveType = "gather" ("gather" | "craft" | "kill" | "use")
|
||||
│ • objectiveTarget = "reed"
|
||||
│ • objectiveCount = 3
|
||||
│ • rewardItem = "berry"
|
||||
│ • rewardCount = 2
|
||||
│ • autoStart = true
|
||||
├─ Achievements (Folder) ← flat counter + threshold defs
|
||||
│ └─ husk_slayer (Configuration)
|
||||
│ • name = "Husk Slayer"
|
||||
│ • counter = "kills_husk" (see docs/achievements.md for the counter catalogue)
|
||||
│ • threshold = 3
|
||||
├─ Tools (Folder) ← Tool templates the hotbar equips (named by item id)
|
||||
└─ MobModels (Folder) ← rigged mob templates Mobs.spawn clones (named by mob id)
|
||||
```
|
||||
@@ -57,7 +71,7 @@ same instance-config pattern the survival stats use.
|
||||
|
||||
## The admin plugin Content widget
|
||||
|
||||
Open Studio → the **SurvivorCore** toolbar → **Content**. Five builders:
|
||||
Open Studio → the **SurvivorCore** toolbar → **Content**. Seven builders:
|
||||
|
||||
- **Items** — create an item by id, then set Name / Max stack / Weight / Category / Tool type /
|
||||
Icon / Description.
|
||||
@@ -74,6 +88,13 @@ Open Studio → the **SurvivorCore** toolbar → **Content**. Five builders:
|
||||
Yield min / Yield max. **+ Add to World** drops a tagged `Gatherable` node.
|
||||
- **Mobs** — create a mob type by id, then set Faction / Health / Speeds / Aggro / Leash / Attack.
|
||||
**+ Add to World** drops a tagged `Mob` placeholder rig (swap in your own model later).
|
||||
- **Quests** — create a quest by id, then set Name / Description / Objective (type, target, count) /
|
||||
Reward (item, count) / Auto-start / Requires / Turn in. **+ Quest giver** drops a tagged
|
||||
`QuestGiver` post offering it (see [quests.md](quests.md)). Multi-objective chains stay
|
||||
code-authored, like recipes.
|
||||
- **Achievements** — create an achievement by key, then set Name / Description / Counter /
|
||||
Threshold / Icon — counters are the engine's auto-derived progression counters
|
||||
(see [achievements.md](achievements.md)).
|
||||
|
||||
Each edit is one Studio **undo** step. Behind the scenes it creates/edits the
|
||||
`SurvivorCoreContent` instances above, so pressing Play registers your content with no code.
|
||||
|
||||
+17
-8
@@ -46,10 +46,12 @@ SurvivorCore.Recipes.register({
|
||||
|
||||
| Registry | Key field | What it holds |
|
||||
|---|---|---|
|
||||
| `Items` | `id` | Item definitions (name, stack size, …). |
|
||||
| `Items` | `id` | Item definitions (name, stack size, …) — incl. weapons + ammo. |
|
||||
| `Recipes` | `id` | Crafting **and** cooking recipes — one registry, routed by `station`. |
|
||||
| `Resources` | `id` | Gatherable-resource defs (what a tagged node *is*). |
|
||||
| `Stats` | `name` | Survival/status stat models. |
|
||||
| `Achievements` | `key` | Achievement definitions. |
|
||||
| `Achievements` | `key` | Achievement defs (counter + threshold — [docs](achievements.md)). |
|
||||
| `Quests` | `id` | Quest defs (objectives + rewards — [docs](quests.md)). |
|
||||
| `Codex` | `id` | Discoverable lore / collectible entries. |
|
||||
| `Appearance` | `id` | Character appearance options. |
|
||||
| `Mobs` | `id` | Creature / hostile-mob definitions. |
|
||||
@@ -142,15 +144,22 @@ end)
|
||||
-- later: disconnect()
|
||||
```
|
||||
|
||||
Engine systems fire hooks with `Hooks.run("name", ctx)`. Today the `Gatherable` component
|
||||
fires:
|
||||
Engine systems fire hooks with `Hooks.run("name", ctx)`. The full catalogue lives in the header of
|
||||
[`src/foundation/Hooks.luau`](../src/foundation/Hooks.luau); highlights:
|
||||
|
||||
| Hook | Context |
|
||||
| Hook family | Fired by |
|
||||
|---|---|
|
||||
| `gather:hit` | `{ instance, player, values, hpLeft }` — each interaction. |
|
||||
| `gather:depleted` | `{ instance, player, values }` — final hit, before the instance is destroyed. |
|
||||
| `gather:hit` / `gather:depleted` / `gather:blocked` | harvesting ([docs](harvesting.md)) |
|
||||
| `craft:start` / `craft:end` / `craft:blocked` | crafting ([docs](crafting.md)) |
|
||||
| `item:use` · `inventory:changed` | inventory ([docs](inventory.md)) |
|
||||
| `mob:spawned` / `mob:hit` / `mob:attack` / `mob:died` | mobs & AI ([docs](mobs.md)) |
|
||||
| `combat:hit` / `combat:kill` | combat ([docs](combat.md)) |
|
||||
| `quest:started` / `quest:progress` / `quest:completed` / `quest:blocked` | quests ([docs](quests.md)) |
|
||||
| `achievement:unlocked` | achievements ([docs](achievements.md)) |
|
||||
|
||||
More hooks land as systems are extracted (`craft:start`/`craft:end`, mob lifecycle, …).
|
||||
These gameplay events ALSO cross the **EventBridge** with the same names — that bus is what quests,
|
||||
achievements, and analytics consume (via the `Progression` translation layer,
|
||||
[docs](achievements.md#the-progression-stream)).
|
||||
|
||||
### Hooks vs. EventBridge
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Quests
|
||||
|
||||
The quest system ([`src/systems/Quests.luau`](../src/systems/Quests.luau)) gives players **goals**:
|
||||
accept a quest, work its objectives, claim the reward. Progress is driven entirely by the events
|
||||
the other systems already fire (gather / craft / kill / use), so a quest needs **zero wiring** —
|
||||
register a def and the engine tracks it.
|
||||
|
||||
> Progress is **session-scoped** (Player attributes) — persistence (DataStore) is a future system.
|
||||
|
||||
## Defining a quest
|
||||
|
||||
In code (canonical, supports multiple objectives) or [no-code via the admin
|
||||
plugin](content-authoring.md) (single-objective):
|
||||
|
||||
```lua
|
||||
SurvivorCore.Quests.register({
|
||||
id = "gather_reeds",
|
||||
name = "Gather Reeds",
|
||||
description = "Pull 3 reeds from the bushes by the river.",
|
||||
objectives = {
|
||||
{ type = "gather", target = "reed", count = 3 }, -- type: "gather"|"craft"|"kill"|"use"
|
||||
},
|
||||
rewards = { { item = "berry", count = 2 } },
|
||||
autoStart = true, -- accepted automatically on join (or when `requires` completes)
|
||||
-- requires = "id", -- single prerequisite (chains)
|
||||
-- turnIn = true, -- must return to a QuestGiver to claim the reward
|
||||
})
|
||||
```
|
||||
|
||||
An objective's `target` is an item id (gather/craft/use) or a mob type (kill); **blank = any**
|
||||
("slay 3 of anything"). The flat no-code shape (`objectiveType`/`objectiveTarget`/`objectiveCount`/
|
||||
`rewardItem`/`rewardCount` attributes) is normalized to this at load — both behave identically.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
**active** → objectives met → (**ready**, if `turnIn` or the inventory was full) → **done**.
|
||||
|
||||
- Progress comes from the [Progression](achievements.md#the-progression-stream) event stream.
|
||||
- **Rewards are never lost:** if the inventory can't fit the reward, the quest parks in *ready* and
|
||||
the grant retries automatically whenever the inventory changes.
|
||||
- Completing a quest fires the `quest:completed` hook + EventBridge event, shows a toast
|
||||
(config-gated), and **auto-starts** any `autoStart` quest that `requires` it — chains flow.
|
||||
|
||||
## Quest givers (no-code)
|
||||
|
||||
Tag any part/model **`QuestGiver`** (CollectionService) and set `Quest = "<quest id>"` — the engine
|
||||
attaches a hold-**E** prompt that *accepts* the quest (or *turns it in* when its objectives are met
|
||||
and it's a `turnIn` quest). The admin plugin's Quests editor drops one via **+ Quest giver**.
|
||||
|
||||
## Runtime API
|
||||
|
||||
`SurvivorCore.Quests` (registry always; runtime ops after `start()`):
|
||||
|
||||
| Function | Behavior |
|
||||
|---|---|
|
||||
| `accept(player, id) -> (ok, reason?)` | gates: unknown / done / active / `requires` / `MaxActive` |
|
||||
| `complete(player, id) -> (ok, reason?)` | turn-in (or claim a met quest); `"full"` = no reward room |
|
||||
| `abandon(player, id) -> ok` | drop an active quest (progress lost) |
|
||||
| `getLog(player)` / `isActive` / `isCompleted` | read state |
|
||||
|
||||
Hooks (also on EventBridge): `quest:started` / `quest:progress` / `quest:completed` /
|
||||
`quest:blocked` — ctx `{ player, questId, def?, index?, count?, reason? }`.
|
||||
|
||||
## The Quests tab & data
|
||||
|
||||
The menu's **Quests** tab (key **L**) renders Active (per-objective progress bars) / Ready /
|
||||
Completed. Defs replicate via a JSON `SurvivorCoreQuestData` StringValue; per-player progress is the
|
||||
`QuestLog` JSON Player attribute (`{ active = { [id] = { p = {…} } }, ready = {}, done = {} }`) —
|
||||
both readable via [`src/shared/QuestData.luau`](../src/shared/QuestData.luau).
|
||||
|
||||
## Tuning
|
||||
|
||||
`Config.override("Quests", { MaxActive = 0 --[[0 = unlimited]], Toasts = true })`.
|
||||
|
||||
---
|
||||
|
||||
See also: [Achievements](achievements.md) · [No-code content](content-authoring.md) ·
|
||||
[Extending](extending.md).
|
||||
Reference in New Issue
Block a user