Files
brass-and-sigil/docs/economy-system.md
T
Matt 95b0d1564d docs: economy architecture lockdown (v0.2)
Final architecture for the economy + quest system. No more V1/V2/V3
iteration cycles. Resolved against actual mod source code and verified
APIs via research spike.

Key decisions locked:

- Bounty UI: custom (bossbar + /quests commands + Easy NPC dialog).
  NOT FTB Quests -- it lacks per-player visibility, has no abandon
  event, and cooldown is per-team. Confirmed via FTBQ source review.

- Progression UI: FTB Quests (right tool for long-form content where
  per-team cooldowns and one-shot rewards are fine).

- Numismatics: direct Java.loadClass access (no events shipped, no
  KubeJS bindings). Documented exact API surface and call patterns.

- FTB Chunks claim cost: FTBChunksEvents.before('claim') with
  event.setResult() to refuse. Requires adding FTB XMod Compat.

- Tier system: FTB Ranks (CurseForge only) + permission-driven chunk
  limits + KubeJS-mediated upgrades via runCommandSilent('ftbranks add').

- Per-player HUD: bossbars, not scoreboards. Vanilla scoreboards are
  global; bossbars natively per-player.

- Custom mod for V1: NOT NEEDED. Re-evaluate if blocked.

Mods to add in Phase 0a (one PR before any implementation):
  - FTB Quests 2101.1.25
  - FTB XMod Compat (latest)
  - FTB Ranks 2101.1.3

Phasing updated: Phase 0a (mod additions) blocks the rest. Phase 0b
(spawn build) is non-blocking and user-paced; all systems can be
built and verified before spawn exists.

KubeJS file layout documented for clean implementation:
  pack/overrides/kubejs/server_scripts/economy/{01..99}_*.js

11 open decisions resolved. 3 remain non-blocking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:09:45 +00:00

396 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Brass and Sigil — Economy & Quest System Design
> Status: architecture locked (v0.2). Implementation ready to begin.
> Owner: matt
> Last updated: 2026-06-06
---
## 1. Goals
- **One unified currency** — spurs (Numismatics).
- **Multiple ways to earn, each one bounded.** No infinite money printer.
- **Multiple sinks that scale with progression.** Money disappears as you advance, so wealth never dwarfs the cost of new ambitions.
- **Spatial design that gives spawn personality.** Themed guild buildings, each with a clear purpose, each tied to the same wallet.
- **Forward-compat with a proper quest line** via FTB Quests, while supporting daily/repeatable activities at spawn.
- **Anti-automation by design**, not by patches.
---
## 2. Core architecture
```
┌────── MONEY IN (bounded) ──────┐
│ │
Adventurer's Guild — Daily bounties (cooldowns, scaling)
Merchant's Bazaar — Sell shop (fixed + DR), dynamic exchange
Mint — Capped emerald → spur conversion *
First-time milestones — One-shot per player
FTB Quests — Progression milestone payouts
┌───────────────────┐
│ Player wallet │ ← Numismatics
│ (spurs) │
└───────────────────┘
┌────── MONEY OUT (scaling) ──────┐
│ │
Civic Office — Plot claim costs (FTB Chunks override)
Civic Office — Tier upgrades (FTB Ranks integration)
Merchant's Bazaar — Player-to-player job board (no minting)
Villagers — Re-priced trades, net sink
Custom services — Warps, sethomes, premium cosmetics
```
\* Only if we keep emeralds; see §5 — we're moving villagers to spurs, which probably retires the Mint as a currency-conversion station and repurposes it as a wallet/info hub.
---
## 3. Spawn — themed guild buildings
Each guild is a destination. The guild's NPC is both the **entry point** (accept work) and **exit point** (return to claim reward). This pattern applies universally — see §4.
| Building | Purpose | Primary mechanics inside |
|---|---|---|
| **Adventurer's Guild** | Combat & exploration | Bounty board (daily mob hunts), boss-kill quests, dungeon-delve quests (future) |
| **Merchant's Bazaar** | Trade & raw-mat economy | Numismatics sell shop, dynamic exchange counter, player-driven job board |
| **Civic Office** | Land & status | Plot claim office, tier upgrades, sethome/warp services |
| **Library / Academy** | Knowledge & magic | FTB Quests anchor NPC, Ars Nouveau themed quests, lore terminals |
| **Tavern** | Social & onboarding | First-time newbie quests, casual NPCs, food/buff vendor |
| **Foundry / Workshop** | Crafting & commissions | Create-themed contracts, crafting orders, raw-material market |
| **Mint** | Currency information | Wallet info, currency tutorial, possibly emerald exchange |
### Forward-compat with a full quest line
The architecture is naturally dual-track:
- **FTB Quests** = the *progression* backbone — chapter-by-chapter pack content, one-time rewards, story arcs. The campaign.
- **Spawn-building NPCs** (Easy NPC + KubeJS) = the *daily/repeatable* layer — bounties, sell shops, services. The live activities.
Both pay into the same wallet. Players run the campaign at their pace and the daily content for income/replay value. They reinforce each other rather than compete.
---
## 4. Quest & bounty flow (universal pattern)
**Core principle:** for any guild, the player **accepts** work from an NPC, **completes** it in the world, then **returns** to that NPC to **turn it in** and claim the reward. No instant payouts. This:
- Anchors guilds as destinations
- Creates risk/reward tension (die on the return, lose the reward)
- Encourages waypoint use and travel
- Lets the server centrally control payouts and detect cheating
### State machine
Each quest, per player, lives in one of these states:
```
AVAILABLE ─[player accepts]─► ACTIVE ─[objective met]─► READY ─[turned in]─► COMPLETED
▲ │
└──────────────────────[cooldown elapses]───────────────────────────────────────────┘
```
- **AVAILABLE** — NPC offers the quest in dialog. Player has no current copy of it.
- **ACTIVE** — Player accepted. KubeJS tracks objective progress via relevant events (`EntityEvents.death` for kills, `ItemEvents.firstRightClicked` for explorations, etc.).
- **READY** — Objective met. Reward is *promised but not yet paid.* NPC dialog now offers a turn-in option.
- **COMPLETED** — Reward paid out. Quest goes on cooldown for this player.
- **AVAILABLE** (again) — after the cooldown, the NPC will offer it again.
### Worked example — daily bounty
1. **Adventurer's Guild NPC** has 5 randomized daily quests (e.g. "Slay 10 Plunderers — 50 spurs").
2. Player right-clicks NPC → Easy NPC dialog → "Accept: Slay 10 Plunderers."
3. KubeJS writes `quest:plunderer_10` to the player's quest state with `count: 0`.
4. Player kills Plunderers in the world. `EntityEvents.death` for `supplementaries:plunderer` increments the count.
5. At count=10, KubeJS flips state to **READY** and notifies the player ("Return to the Adventurer's Guild to claim your reward.").
6. Player returns. Right-click NPC. Easy NPC dialog detects READY state and offers "Turn in: Slay 10 Plunderers."
7. KubeJS pays 50 spurs into the player's Numismatics wallet, flips state to **COMPLETED**, sets cooldown to 24h.
### Universal applicability
The same accept → complete → return pattern works for every guild:
| Guild | Accept | Complete | Return |
|---|---|---|---|
| **Adventurer's** | "Hunt 10 plunderers" | kill them | NPC pays bounty |
| **Merchant's** | "Procure 32 diamonds for our buyer" | gather them | NPC pays inflated price + completes |
| **Civic** | "Survey 5 unclaimed chunks" | walk through them | NPC pays survey fee |
| **Library** | "Recover this lost tome" | find the book in a structure | NPC pays + grants reputation |
| **Foundry** | "Craft 10 Andesite Casings" | craft them | NPC pays commission |
### Player-facing tracking & cancellation (locked)
Three surfaces, **all shipping together in Phase 3.** Not iterated, not optional.
| Surface | Role | Mechanism |
|---|---|---|
| **Bossbars** | Always-visible HUD — one bossbar per active quest, shows name + N/M progress bar | Vanilla `/bossbar` commands driven by KubeJS. Native per-player display (scoreboards are global; bossbars aren't). Up to 3 stacked at top of screen. |
| **`/quests` commands** | Management — list, inspect, cancel | KubeJS `ServerEvents.commandRegistry`. Works in chat and over the Telegram bridge. Clickable chat components for `[cancel]` and `[details]`. |
| **Easy NPC dialog** | Accept + turn-in at guild NPCs | Dialog action runs `/bns_quest accept <quest_id> <npc_tag>` (KubeJS-registered command). Dialog branching uses scoreboard objectives that KubeJS mirrors quest state into. |
| Command | Effect |
|---|---|
| `/quests` | Lists your ACTIVE / READY quests with progress, source guild, reward |
| `/quests info <id>` | Detail view |
| `/quests cancel <id>` | Drops an active quest, frees the slot. Applies the same cooldown as if completed (prevents accept-cancel-accept RNG-shopping). |
### Why NOT FTB Quests for bounties
FTB Quests is the right tool for **progression** (Phase 5+), not for daily bounties. Confirmed via source review:
- **No abandon/cancel event** — players can `Ctrl+Shift+R` to reset quest progress, but nothing fires that KubeJS can hook → can't enforce cancellation cooldown.
- **Cooldown is per-team, not per-player** — in a 4-player team, one member completing puts it on cooldown for all four.
- **No per-player quest visibility** — gating "5 of 20 active today" requires `CustomTask` gates rather than first-class per-player visibility.
For long-form progression questing, all three are non-issues. So: FTB Quests for the campaign, custom KubeJS system for daily bounties.
### Quest slot limit
Players have a **maximum of 3 active quests at a time.** This is the friction that makes cancel meaningful:
- Forces players to pick which quests they actually want — no "accept all 5, see what's easiest, cancel the rest" RNG-shopping.
- Keeps the state files small.
- Makes ready-to-turn-in queues meaningful (you can hold 3 unrewarded quests in your back pocket).
A quest moving to READY frees the slot for accepting a new one even before turn-in.
### KubeJS implementation — locked specifics
- **State storage:** per-player JSON in `world/data/bns_quests/<uuid>.json`. Schema:
```json
{
"active": [
{
"id": "advg_plunderer_10",
"acceptedAt": 1717684800,
"progress": 7,
"target": 10,
"rewardSpurs": 50,
"source": "adventurers_guild",
"bossbarId": "bns:quest_advg_plunderer_10_<uuid>"
}
],
"completed": {
"advg_plunderer_10": { "cooldownUntil": 1717771200, "completions": 3 }
}
}
```
- **Event tracking:** KubeJS event handlers (`EntityEvents.death`, `ItemEvents.pickedUp`, `BlockEvents.broken`) read the player's active quests and increment progress + update bossbar value.
- **Bossbar lifecycle:** on quest accept, KubeJS creates a unique per-player bossbar via `server.runCommandSilent('bossbar add <id> <name>')`, assigns it to that player only, updates `value` and `max` on progress. On turn-in or cancel: `bossbar remove`.
- **Numismatics integration (direct Java access — no events shipped, no KubeJS bindings):**
```js
const Numismatics = Java.loadClass('dev.ithundxr.createnumismatics.Numismatics');
const ReasonHolder = Java.loadClass('dev.ithundxr.createnumismatics.content.backend.ReasonHolder');
const acct = Numismatics.BANK.getAccount(player); // ServerPlayer
const balance = acct.getBalance(); // int (spurs)
acct.deposit(50); // pay quest reward
const ok = acct.deduct(100, ReasonHolder.empty()); // returns false if broke
Numismatics.BANK.markBankDirty(); // force persistence
```
- **Dialog branching:** Easy NPC dialog branches on scoreboard objective `bns.quest.<npc_tag>` that KubeJS mirrors quest state into. Dialog `Command` action triggers `/bns_quest accept <id> <npc_tag>` which routes to KubeJS.
- **Commands:** registered via `ServerEvents.commandRegistry` in KubeJS. Scoped to the player who runs them. The custom `/bns_quest` namespace handles NPC-triggered actions; `/quests` is the player-facing tracker.
- **FTB Chunks claim cost (Phase 2):** requires `FTB XMod Compat` mod. Then:
```js
FTBChunksEvents.before('claim', event => {
const player = event.source.player;
const acct = Numismatics.BANK.getAccount(player);
const cost = computeClaimCost(player);
if (acct.getBalance() < cost) {
event.setResult(ClaimResult.YOU_DO_NOT_HAVE_ENOUGH_SPURS); // custom enum value
return;
}
acct.deduct(cost, ReasonHolder.empty());
});
```
**Note:** the `.before` may fire twice (sim + real). Guard with idempotent state, or do balance check in `.before` and deduct in `.after('claim')`.
---
## 5. Villager rebalance
**Decision:** move all villagers to **spurs** (not emeralds). Thematic consistency over preserving vanilla mental model.
**Decision principles:**
- Villagers are **net money sinks** — players spend more on villagers than they earn from them.
- **Useful trades stay** — enchanted books, tools, food, decorative blocks. Reasonable spur prices.
- **Trash trades get deleted** — no one ever wanted to sell rotten flesh or buy paper.
- **Sell-to-villager trades exist but are bad** — `64 wheat → 1 spur`, `16 leather → 1 spur`. Farms still possible, but ~10× slower than any other income source.
### Price guidance (initial calibration — tune via playtesting)
#### Buy-from-villager (player pays spurs)
| Item | Tier-1 | Tier-3 | Tier-5 |
|---|---|---|---|
| Standard enchanted book (random) | 50 spurs | 120 spurs | 250 spurs |
| Mending book | — | — | 800 spurs |
| Diamond tool (unenchanted) | — | 80 spurs | — |
| Diamond tool (enchanted) | — | — | 350 spurs |
| Bookshelf | 15 spurs | — | — |
#### Sell-to-villager (player gets spurs)
| Item | Spurs per unit |
|---|---|
| Wheat | 1 per 64 |
| Paper | 1 per 32 |
| Leather | 1 per 16 |
| Iron ingot | 2 per 1 |
| Diamond | 8 per 1 *(matches sell-shop fixed price — players use whichever is closer)* |
### Scope
~60 trades × 5 profession tiers = ~300 trade entries to redesign. One-time data-design pass, implemented via `ServerEvents.villagerTrades` in KubeJS.
---
## 6. Money inflow sources (detailed)
| Source | Cap / DR mechanism | Rough rate |
|---|---|---|
| **Daily bounties** | Per-player per-quest cooldown (24h) | ~200-500 spurs/day for an active player |
| **Sell shop (fixed price + DR)** | Lifetime sales tracker per player, 10% price drop per 64 sold | ~50-200 spurs/hr while diamond stockpile lasts |
| **Dynamic exchange** | Global supply pool — price drops with sales | Self-balancing |
| **First-time milestones** | One-shot per player (`hasFlag`) | 5-50 spurs per milestone, ~10 milestones total |
| **FTB Quest progression** | One-time per quest | Varies; cluster rewards at chapter completion |
| **Villager sell trades** | Inherently slow + bad prices | ~50 spurs/hr from a serious villager farm |
---
## 7. Money outflow sinks (detailed)
| Sink | Cost curve | Why this scales right |
|---|---|---|
| **Plot claims** | Free up to 9 chunks. 50 spurs/chunk to 25. 200 spurs/chunk beyond. | New players claim free starter areas; ambitious players pay real money |
| **Tier upgrades** | Cumulative cost: 500 → 2k → 8k → 30k spurs | Endgame sink, status display |
| **Buy-from-villager** | See §5 | Constant outflow as you build up |
| **Premium cosmetics** | 100-2k spurs each | Optional sink for cash-flush players |
| **Services** | Sethome: 50 spurs, warp: 25 spurs, /back: 10 spurs | Convenience taxes |
---
## 8. Mod stack (locked)
### Already in pack
| Need | Mod | Version |
|---|---|---|
| Currency + wallets + shop blocks | Numismatics | 1.0.20 |
| NPC dialog presentation | Easy NPC | 6.16.x (6.17.0 available, minor bump) |
| Plot claims | FTB Chunks | 2101.1.14 |
| Team management | FTB Teams | 2101.1.9 |
| Core library | FTB Library | 2101.1.31 |
| Cross-mod compat | Architectury API | 13.0.8 |
| Glue, commands, state | KubeJS | 2101.7.2 |
### Adding in Phase 0a (mod-additions PR — blocks everything else)
| Mod | Version | Reason |
|---|---|---|
| **FTB Quests** | 2101.1.25 | Progression quest UI (Phase 5+) |
| **FTB XMod Compat** | latest 1.21.1 NeoForge | KubeJS hooks for FTB Chunks claim events + FTB Quests events |
| **FTB Ranks** | 2101.1.3 | Tier system (Phase 6). CurseForge only, no Modrinth listing. |
### Custom mod
**Not needed for V1.** Re-evaluate only if implementation hits a hard wall on a specific feature.
### Explicitly NOT adding
- CustomNPCs (Easy NPC covers it)
- Citizens (Bukkit-only)
- PMMO / skill mods (out of scope)
- Generic "quest giver" mods (FTB Quests + our KubeJS bounty engine cover it)
---
## 9. Phasing
Each phase is independently shippable. Earlier phases unlock value without depending on later ones.
| Phase | Deliverable | Spawn build required? |
|---|---|---|
| **0a** | **Mod additions PR:** FTB Quests + FTB XMod Compat + FTB Ranks (+ minor Easy NPC bump). New pack version. | No |
| **0b** | Spawn skeleton — 7 guild building blockouts (rough). **User does in-game when time permits.** Non-blocking for everything else. | Yes (user) |
| **1** | **Sell shop:** Numismatics shop blocks for diamonds (10 spurs) + netherite ingots (100 spurs). Lifetime-sale tracker for diminishing returns. | No — test anywhere, drop into Bazaar later |
| **2** | **Plot claim cost:** `FTBChunksEvents.before('claim')` deducts spurs. Free first 9 chunks, 50/chunk to 25, 200/chunk beyond. | No |
| **3** | **Bounty engine:** state machine + `/quests` commands + bossbar HUD + 3-slot cap. Daily quest pool with KubeJS-rotated dailies. NPC dialog hooks ready but inert until guild NPCs exist at spawn. | No (NPC layer activates once spawn is built) |
| **4** | **Villager rebalance:** strip vanilla, ship spur-priced trade table via `ServerEvents.villagerTrades`. | No |
| **5** | **Progression quest line:** FTB Quests anchor + first chapter. KubeJS rewards dispense spurs via Numismatics API. | No (anchor NPC at Library once spawn exists) |
| **6** | **Tier upgrades:** FTB Ranks integrated. `/tier upgrade` deducts spurs and promotes via `ftbranks add`. Rank perms gate FTB Chunks claim limits. | No |
| **7** | **Dynamic exchange rates:** global supply-pool pricing for raw mats. | No |
| **8+** | Lootr dungeons, Foundry crafting commissions, deeper FTB Quests chapters, player-vs-player bounties (if wanted). | TBD |
---
## 10. Risk register
| Risk | Likelihood | Mitigation |
|---|---|---|
| Bounty engine bugs out, players lose progress | Medium | Atomic state writes; logs of every state transition |
| Villager farms still profitable after rebalance | Medium | Track player sell volumes; tune prices in patches |
| KubeJS-EasyNPC dialog branching has limits | Low-Med | Verify hooks in a test world before Phase 3; fall back to command-driven NPCs if needed |
| FTB Chunks `/claim` command override gotchas | Low | Test in dev world; FTB Chunks API is stable |
| Plot cost feels punishing | Med | First 9 chunks free is the safety valve; tune from playtest |
| Players bypass the system via creative / `/give` | Op-only | Document op responsibility; consider audit logs for op spur grants |
---
## 11. Decisions made
Resolved before implementation:
| Question | Decision |
|---|---|
| Quest UI for bounties | Custom (bossbars + commands + Easy NPC) — NOT FTB Quests |
| Quest UI for progression | FTB Quests |
| State storage | Per-player JSON in `world/data/bns_economy/` |
| Numismatics access | Direct Java via `Java.loadClass` |
| Chunk claim refusal | `FTBChunksEvents.before('claim')` with `event.setResult(...)` |
| Tier system | FTB Ranks + permission-driven chunk limits |
| Custom mod for V1 | No — only if a hard wall appears |
| Sethomes / warps | Paid services (`/sethome` 50 spurs, `/warp` 25 spurs, `/back` 10 spurs) |
| Daily quest count | 5 active at a time, refresh per-player 24h after last accept |
| Emeralds | Retired as currency, retain as a trade material for villager-redirect trades |
### Still to decide (non-blocking — pick during implementation)
- **PvP bounties** — out of scope for V1, revisit after V1 ships
- **Lootr** — add only when structured dungeons exist (Phase 8+)
- **Default-rank starting permissions** — to be set with the first FTB Ranks config pass
---
## 12. Implementation notes
- KubeJS scripts organized under `pack/overrides/kubejs/server_scripts/economy/`:
- `01_numismatics_api.js` — Java.loadClass wrapper helpers (read/deposit/deduct)
- `02_quest_state.js` — load/save `world/data/bns_economy/<uuid>.json`
- `03_bossbar_hud.js` — per-player bossbar lifecycle
- `04_commands.js` — `/quests`, `/bns_quest`, `/tier` registrations
- `05_bounty_engine.js` — Phase 3 quest engine
- `06_sell_shop.js` — Phase 1 shop block tracking + DR
- `07_plot_costs.js` — Phase 2 FTB Chunks event handler
- `08_villager_trades.js` — Phase 4 rebalance
- `09_tier_upgrades.js` — Phase 6 rank upgrades
- `99_dynamic_exchange.js` — Phase 7
- State data under `world/data/bns_economy/<uuid>.json` per player. Readable, easy to debug. Migration to SQLite considered if dataset grows beyond ~100 players.
- All NPC dialog trees authored in Easy NPC's in-game editor, exported as preset JSONs into `pack/overrides/world/datapacks/bns-fuel/data/easy_npc/`. Dialog `Command` actions call into `/bns_quest` namespace which routes to KubeJS handlers.
- Trade table for villager rebalance: single source file `08_villager_trades.js`. Each profession × tier has its own block. One file = one diff to review during balance passes.
---
## See also
- `MEMORY.md` entries:
- `project-mc-player-economy.md` — earlier higher-level notes on Numismatics + plot system
- `feedback-kubejs-rhino-gotchas.md` — KubeJS scripting constraints
- `feedback-bns-pack-version-sync.md` — pack version + sync workflow
- Existing tasks #20-#26 — to be reshuffled to mirror the phase plan above