# bnstoolkit — Architecture & Perk Specification

> Status: design draft (v0.1). Not yet implemented.
> Owner: matt
> Last updated: 2026-06-06

`bnstoolkit` is the custom companion mod for the Brass and Sigil economy
system. It provides the UI layer (GUI screens, HUD), the FTB Chunks
map mixins for spawn plot integration, and the data models the economy
needs. All gameplay logic (bounty rotation, state machine, villager
trades, Numismatics integration, etc.) lives in KubeJS and is called
into by the mod via network packets.

This doc is the source of truth for the mod design. It must be
reviewed before any Java is written.

See also: [`economy-system.md`](./economy-system.md) for the gameplay
side of the design.

---

## 1. Purpose & scope

### In scope

- GUI screens built on FTB Library's UI framework (matches FTB Quests / FTB Chunks visual language)
- Mixins into FTB Chunks' map UI for spawn-plot cost overlay, hover tooltips, and click-to-purchase popups
- Custom HUD render layer for quest progress (replaces stacked vanilla bossbars)
- Data persistence for plot ownership (server-side, saved with world)
- Network packets for client↔server state sync
- Admin commands: `/bnstoolkit reload`, debugging commands
- Config-driven tier perks (`tiers.json`) and plot definitions (`plots.json`)

### Out of scope

- Bounty quest engine (lives in KubeJS)
- Villager trade rebalance (lives in KubeJS)
- Numismatics direct integration (lives in KubeJS — mod calls KubeJS commands to debit/credit)
- FTB Quests integration (FTB Quests handles its own UI for the progression campaign)
- FTB Ranks admin (FTB Ranks handles permissions directly; mod just reads them)
- **Sethome / `/back` / fast-travel features** — explicitly removed. The pack uses **Waystones** as the only travel mechanic. Restrictive travel is a design feature for a Create Aeronautics pack: airships, trains, and vehicles are the intended way to cover distance.

---

## 2. High-level architecture

```
                 ┌──────────────────────┐
                 │   Player (client)    │
                 └──────────┬───────────┘
                            │ keybind / NPC click / chunk click
                            ▼
            ┌───────────────────────────────────┐
            │       bnstoolkit (client)          │
            │   • FTB Library UI screens         │
            │   • FTB Chunks map mixin           │
            │   • Quest progress HUD             │
            └───────────────┬───────────────────┘
                            │ network packets
                            ▼
            ┌───────────────────────────────────┐
            │       bnstoolkit (server)          │
            │   • Plot ownership data            │
            │   • Sethome data                   │
            │   • Network packet handlers        │
            │   • /bnstoolkit reload command     │
            └─────┬───────────────────────┬─────┘
                  │                       │
       reads      │                       │ calls KubeJS commands /
   FTB Ranks /    │                       │ fires events for KubeJS
   FTB Chunks /   │                       │ to react to
   Numismatics    ▼                       ▼
            ┌───────────────┐    ┌──────────────────────┐
            │ FTB / NeoForge │    │     KubeJS scripts   │
            │     stack      │    │ • Bounty engine      │
            └───────────────┘    │ • Quest state         │
                                  │ • Numismatics calls   │
                                  │ • Villager trades    │
                                  │ • Tier promotion     │
                                  └──────────────────────┘
```

Key invariant: **the mod owns UI + plot data + sethomes. KubeJS owns
gameplay logic.** They communicate via network packets and KubeJS
commands. The mod never directly mutates Numismatics balances or quest
state — it asks KubeJS to do that via commands.

---

## 3. Package layout

```
bnstoolkit/
├── build.gradle                       NeoForge 1.21.1 + FTB Library dep
├── src/main/java/uk/sijbers/bnstoolkit/
│   ├── BnsToolkit.java                Mod entry, lifecycle
│   ├── client/
│   │   ├── ClientEvents.java          Keybinds, client setup
│   │   ├── screen/
│   │   │   ├── QuestLogScreen.java
│   │   │   ├── BountyBoardScreen.java
│   │   │   ├── TierUpgradeScreen.java
│   │   │   ├── PlotPurchaseScreen.java
│   │   │   ├── ShopBrowserScreen.java
│   │   │   └── widgets/               (subclass FTB Library widgets)
│   │   ├── hud/
│   │   │   └── QuestProgressHud.java  IGuiOverlay
│   │   └── mixin/
│   │       ├── ChunkScreenPanelMixin.java
│   │       └── ChunkButtonMixin.java
│   ├── server/
│   │   ├── command/
│   │   │   └── BnsToolkitCommand.java   /bnstoolkit reload, debug
│   │   ├── data/
│   │   │   ├── PlotRegistry.java        loads plots.json
│   │   │   └── PlotOwnership.java       SavedData, plot owner UUIDs
│   │   ├── event/
│   │   │   ├── PlayerJoinHandler.java   tier-based join broadcast
│   │   │   └── ChatColourHandler.java   tier-based chat name colouring
│   │   └── net/
│   │       └── (packet implementations — see §6)
│   ├── common/
│   │   ├── data/
│   │   │   ├── TierConfig.java          loads tiers.json
│   │   │   ├── BountyDef.java           DTO mirrored from KubeJS
│   │   │   ├── QuestStateView.java      snapshot for UI display
│   │   │   └── PlotDef.java             plot definition (region, price)
│   │   └── net/
│   │       └── BnsPacket.java           network channel + payload base
│   └── resources/
│       ├── META-INF/neoforge.mods.toml
│       ├── bnstoolkit.mixins.json
│       └── assets/bnstoolkit/lang/en_us.json
└── src/main/resources/
    └── data-config/                     packed defaults, copied on first run
        ├── tiers.json
        └── plots.json
```

---

## 4. Screen specifications

All screens extend FTB Library's `BaseScreen`. They share a common
header (player name, current tier, spur balance) so navigation feels
consistent.

### 4.1 Quest Log

**Opened by:** keybind (default Q+B?) or right-clicking a Quest Log
block placed at any guild building.

**Layout:** three tabs across the top — Active / Available / Completed.

- **Active tab:** list of currently-accepted bounties with progress
  bars. Each row clickable; click expands a detail panel with
  description, source guild, reward, and a `Cancel quest` button.
  Cancel triggers a confirmation modal warning about cooldown.
- **Available tab:** list of bounties currently offered (rotated daily
  by the KubeJS engine). Each row shows the bounty source, reward,
  cooldown remaining (if any). Click → detail panel with `Accept` button.
- **Completed tab:** historical log of completed bounties this week,
  for satisfaction value. No interaction beyond viewing.

**Network calls:**
- On open: client requests current quest state from server (`S2CQuestSnapshot`)
- On accept/cancel click: `C2SAcceptBounty(id)` / `C2SCancelQuest(id)`

### 4.2 Bounty Board

**Opened by:** right-clicking the Adventurer's Guild Quartermaster
NPC (Easy NPC dialog action runs `/bnstoolkit openboard adventurers_guild`)
OR clicking an in-world `bnstoolkit:bounty_board` block.

**Layout:** poster-board style — the day's available bounties as
visual "cards" laid out in a grid, each with mob icon, name,
description, reward, accept button. Friendly to non-tech users.

**Network calls:**
- On open: `S2CBountyBoardSnapshot(guild_id)` returns today's bounty list
- On accept: same `C2SAcceptBounty(id)` as Quest Log

### 4.3 Tier Upgrade Screen

**Opened by:** right-clicking the Civic Office Magistrate NPC.

**Layout:** full ladder shown as a vertical or horizontal track. Your
current tier highlighted (glowing badge). Next tier shows cost +
delta perks (e.g. "+50 chunks, +1 plot slot, +2 sethomes, +1 bounty,
-2% shop fee, chat colour: yellow"). Big `Upgrade to <Tier>` button
at the bottom, greyed out if you don't have the spurs.

Hovering any future tier shows its full perk list in a side panel.

**Network calls:**
- On open: `S2CTierSnapshot` returns current tier + balance
- On upgrade click: `C2SRequestTierUpgrade` → server verifies balance,
  calls KubeJS `/bnstoolkit_internal upgrade_rank <uuid>` which
  charges spurs and runs `ftbranks add <player> <tier>`

### 4.4 Plot Purchase Popup

**Opened by:** clicking an unowned plot region in the FTB Chunks map
UI (via the mod's mixin into `ChunkScreenPanel`).

**Layout:** modal overlay on top of the FTB Chunks map. Shows: plot
ID, plot type (standard/premium), region size in chunks, owner (none),
price. Your current spur balance. Your remaining plot slots (e.g.
"2 of 3 used"). `Purchase` and `Cancel` buttons. If you're at your
slot cap or don't have enough spurs, `Purchase` is greyed out with
the reason shown.

**Network calls:**
- On purchase: `C2SBuyPlot(plot_id)` → server checks slot capacity,
  asks KubeJS to charge spurs, on success records ownership in
  `PlotOwnership` SavedData.

### 4.5 Shop Browser

**Opened by:** right-clicking the Bazaar Master NPC at the Merchant's
Bazaar (provides a curated view of all currently-listed shop blocks
+ NPC sell shop).

**Layout:** searchable item list with current prices, sort by
category. Click an item to see who's selling it / where the shop
block is located. The NPC sell shop (diamonds, netherite etc.) shows
as a special "Official Bazaar" category with fixed prices and the
current diminishing-return multiplier per item.

**Note:** individual Numismatics shop blocks keep their native UI
when right-clicked — this Bazaar Master is the *curated central
view*, not a replacement for shop block interaction.

**Network calls:**
- On open: `S2CShopBrowserSnapshot` (asks KubeJS for current listings)
- Selecting an item → `S2CShopItemDetail(item_id)` for full info

### 4.6 Quest Progress HUD

**Render layer:** `IGuiOverlay`, drawn over the vanilla HUD.

**Layout:** small panel in top-right (configurable position) showing
each active quest as a row — quest name + progress bar + N/M numeric.
Fades on inactive (no progress in last 30s) so it doesn't visually
clutter when you're not actively bounty-hunting. Re-appears the
moment any quest progresses.

**State source:** KubeJS pushes updates via `S2CHudQuestUpdate` packet
when any quest's progress changes (debounced to once per second
maximum per quest). The HUD reads from a client-side cache.

---

## 5. Mixin targets

Minimal — we don't fork FTB Chunks, we inject only what we need.

| Target | Method | Purpose | Mixin file |
|---|---|---|---|
| `ChunkScreenPanel` | `drawBackground` (TAIL) | Overlay drawing — cost preview for the currently-hovered or selected chunk, "Your plot slots: N of M" text in a corner | `ChunkScreenPanelMixin.java` |
| `ChunkScreenPanel$ChunkButton` | `addMouseOverText` (TAIL) | Append "Cost: N spurs" or "Plot: <name>" to the per-chunk hover tooltip | `ChunkButtonMixin.java` |
| `ChunkScreenPanel$ChunkButton` | `onClicked` (HEAD, with cancellation if applicable) | If the clicked chunk is a plot region, open `PlotPurchaseScreen` instead of triggering a regular claim | `ChunkButtonMixin.java` |

That's it. Three injection points across two mixin files. No
modification of FTB Library, FTB Quests, FTB Ranks, or FTB Teams.

---

## 6. Network packet contract

NeoForge `PayloadRegistrar`. All packets versioned via a single
`PROTOCOL_VERSION` constant; bump on schema change.

### Server → Client

| Packet | Payload | When sent |
|---|---|---|
| `S2CQuestSnapshot` | `List<QuestStateView>` | On Quest Log open, on any quest-state change |
| `S2CBountyBoardSnapshot` | `List<BountyDef>` (5-10 entries) | On Bounty Board open |
| `S2CTierSnapshot` | `(currentTier, balance, perksByTier)` | On Tier Upgrade Screen open |
| `S2CShopBrowserSnapshot` | `List<ShopListing>` | On Shop Browser open |
| `S2CHudQuestUpdate` | `(questId, progress, target)` | On quest progress (debounced 1s) |
| `S2CPlotPurchaseResult` | `(success, message)` | After `C2SBuyPlot` resolves |
| `S2CTierUpgradeResult` | `(success, message)` | After `C2SRequestTierUpgrade` resolves |

### Client → Server

| Packet | Payload | What server does |
|---|---|---|
| `C2SAcceptBounty` | `questId` | Call `/bnstoolkit_internal accept_bounty <uuid> <questId>` (KubeJS) |
| `C2SCancelQuest` | `questId` | Call `/bnstoolkit_internal cancel_quest <uuid> <questId>` (KubeJS) |
| `C2SBuyPlot` | `plotId` | Verify slot capacity, ask KubeJS to debit spurs, record ownership |
| `C2SRequestTierUpgrade` | (none — server knows player) | Verify balance, ask KubeJS to debit + promote |
| `C2SOpenBoard` | `guildId` (optional context) | Reply with appropriate snapshot |

All C2S packets include the player implicitly (NeoForge passes
the `ServerPlayer` in the handler).

---

## 7. Data persistence

| Data | Storage | Lifetime |
|---|---|---|
| Plot ownership | `bnstoolkit:plots` — vanilla NeoForge `SavedData` attached to overworld | Persists with world. Authoritative source. |
| Quest state | KubeJS-managed JSON at `world/data/bns_economy/quests/<uuid>.json` | Not the mod's concern — the mod only reads via packets |
| Tier configuration | `config/bnstoolkit/tiers.json` (hot-reloadable) | Until edited |
| Plot definitions | `config/bnstoolkit/plots.json` (hot-reloadable via `/bnstoolkit reload`) | Until edited |

Plot ownership is server-authoritative. Clients only get views via
packets — they never write to these stores directly.

---

## 8. Configuration files

### `config/bnstoolkit/tiers.json`

Defines the 13-tier ladder. Hot-reloadable.

```json5
{
  "tiers": [
    {
      "id": "peasant",
      "displayName": "Peasant",
      "cost": 0,
      "perks": {
        "wildernessChunks": 9,
        "plotSlots": 0,
        "dailyBountySlots": 3,
        "shopFeeDiscount": 0,
        "chatColour": "grey",
        "joinBroadcast": "none"
      }
    },
    /* ... 12 more tiers ... */
  ]
}
```

### `config/bnstoolkit/plots.json`

Defines spawn plot regions. Each plot has a unique ID, region bounds
(chunk coordinates), type (standard/premium), and price.

```json5
{
  "plots": [
    {
      "id": "bazaar_stall_01",
      "displayName": "Bazaar Stall 1",
      "type": "standard",
      "price": 2000,
      "chunks": [[10, 20], [10, 21]],   // chunk coords, inclusive
      "world": "minecraft:overworld"
    },
    {
      "id": "bazaar_corner_premium",
      "displayName": "Premium Corner Plot",
      "type": "premium",
      "price": 5000,
      "chunks": [[15, 25], [15, 26], [16, 25], [16, 26]],
      "world": "minecraft:overworld"
    }
  ]
}
```

---

## 9. KubeJS integration surface

The mod calls into KubeJS via custom commands registered by KubeJS
(`ServerEvents.commandRegistry`). All have the `bnstoolkit_internal`
prefix to mark them as not-for-player-use:

| Command | Purpose |
|---|---|
| `/bnstoolkit_internal accept_bounty <uuid> <questId>` | Mark quest as ACTIVE in KubeJS state |
| `/bnstoolkit_internal cancel_quest <uuid> <questId>` | Cancel quest, apply cooldown |
| `/bnstoolkit_internal complete_quest <uuid> <questId>` | Mark quest READY (for testing/manual) |
| `/bnstoolkit_internal turn_in_quest <uuid> <questId>` | Pay reward via Numismatics |
| `/bnstoolkit_internal debit <uuid> <amount> <reason>` | Charge spurs (used by plot purchase, tier upgrade) |
| `/bnstoolkit_internal credit <uuid> <amount> <reason>` | Pay spurs |
| `/bnstoolkit_internal upgrade_rank <uuid> <tierId>` | Promote via FTB Ranks |
| `/bnstoolkit_internal get_balance <uuid>` | Returns current spur balance (used for screen header) |

All commands return a JSON result via the command output that the
mod parses. KubeJS handles the actual Numismatics + FTB Ranks calls
(it already has the `Java.loadClass` helpers from the economy KubeJS
scripts).

KubeJS in turn pushes events to the mod via custom payload packets
when quest state changes (so the HUD can update without polling).

---

## 10. Build & release

- **Repo:** new private Gitea repo `minecraft/bnstoolkit` (separate
  from the modpack repo for clean release-versioning, but mirrored
  to the modpack's `pack/overrides/mods/` on each release)
- **Build:** standard NeoForge 1.21.1 ModDevGradle template
- **Dependencies:** Architectury API (provided), FTB Library
  (provided), Numismatics (compileOnly), FTB Chunks (compileOnly)
- **CI:** Gitea Actions runs `./gradlew build` on push to main,
  uploads the jar as a release artifact
- **Modpack integration:** the modpack's bump-version flow includes
  pulling the latest bnstoolkit release jar and updating
  `pack.lock.json` / manifest like any other mod
- **Versioning:** semantic — `1.0.0` for V1 ship, `1.x.y` for
  patches, `2.0.0` if we ever change the network protocol or data
  format

---

## 11. Perk specifications (detailed)

Every perk listed in the 13-tier ladder, expanded with mechanics,
implementation, and edge cases.

### 11.1 Wilderness chunks

**What it does:** caps how many wilderness chunks (outside spawn)
a player can claim via the FTB Chunks map. Claiming within the cap
is free; over the cap is refused with an in-game message.

**Implementation:** FTB Ranks permission node
`ftbchunks.max_claimed_chunks` set per rank in the FTB Ranks config
(written by the mod on first launch from `tiers.json`). FTB Chunks
reads this permission natively — no mod-side enforcement needed.

**Edge case:** if a player downgrades (which they can't normally,
but might happen via op command), they keep already-claimed chunks
over the new cap, but can't claim more until they free some.

### 11.2 Plot slots

**What it does:** caps how many spawn plots (predefined regions at
spawn, see `plots.json`) a player can own concurrently. Independent
of wilderness chunks.

**Implementation:** stored in `PlotOwnership` SavedData. On purchase,
mod checks `playerOwnedPlots.size() < tier.plotSlots`. If at cap,
the Plot Purchase Popup shows the slot count and disables the
`Purchase` button.

**Releasing a plot:** players can sell their plot back via a "Release"
button in the plot-detail screen (opens when they click their own
plot in the map). Releases the slot. Refund: 50% of purchase price.

### 11.3 Daily bounty slots

**What it does:** how many bounties a player can have ACTIVE
simultaneously. Once the slot is freed (by completion or cancel),
the player can accept another from any guild board.

**Implementation:** enforced in KubeJS bounty engine. The mod's
Bounty Board and Quest Log screens read the slot count via packet
and grey out the Accept button when at cap.

**Edge case:** a quest in READY state still counts against your
active slots until you turn it in. This is intentional — pushes
players to actually visit the NPC to claim.

### 11.4 Shop fee discount

**What it does:** reduces the "merchant cut" deducted when you sell
items at the Bazaar's NPC sell shop. The Bazaar charges a base 25%
fee on all sales; your tier provides a discount that reduces this
toward zero.

**Worked example:** you sell a diamond worth 10 spurs at the Bazaar.
- Peasant (0% discount): 25% fee → you receive 7.5 spurs (rounded
  down to 7).
- Knight (-5% discount): 20% fee → 8 spurs.
- Sovereign (-25% discount): 0% fee → full 10 spurs.

**Implementation:** KubeJS sell-shop handler reads the seller's tier
and computes effective payout. Rounded down to nearest integer.

**Edge case:** player-to-player Numismatics shop blocks do NOT
apply this fee — those are direct peer transactions, no Bazaar
middleman. The fee only applies to the official Bazaar sell shop.

### 11.5 Chat colour

**What it does:** the player's name in chat appears in this colour,
visible to everyone. Visual rank marker.

**Implementation:** KubeJS `PlayerEvents.chat` event modifies the
chat component for the player's display name based on their tier
permission (read from FTB Ranks). The mod also colours the player's
nameplate above their head (via a small client packet on tier change
that other clients use to set the display name colour locally).

**Colour ladder:**
- 1-2 Peasant/Farmer: grey
- 3 Citizen: white
- 4 Merchant: yellow
- 5 Knight: green
- 6 Baron: cyan
- 7 Viscount: blue
- 8 Earl: purple
- 9 Marquess: gold
- 10 Duke: orange
- 11 Archduke: red
- 12 Grand Duke: bright red
- 13 Sovereign: rainbow (animated via colour-cycling text component)

**Edge case:** the Founder cosmetic `[Founder]` prefix sits before
the coloured name, in a distinct colour (probably bright yellow or
white-bold). Sovereign + Founder is visually busy but acceptable —
only one player on the server will have both.

### 11.6 Join broadcast

**What it does:** when the player logs in, this controls what message
goes to everyone else on the server.

**Levels:**

- **none:** standard vanilla `<player> joined the game`.
- **simple:** rank-prefixed message in their chat colour. Example:
  ```
  [Knight] Matt has logged in.
  ```
- **fancy:** multi-line, with a separator. Example:
  ```
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   [Viscount] Matt has arrived
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ```
- **epic:** multi-line, with a sound effect (e.g. trumpet fanfare,
  configurable per-tier). Larger ASCII separator. Possibly an ASCII
  art banner of the rank.
- **epic + particles:** as epic, plus a 5-second particle effect at
  the player's spawn location (firework spark, end-rod sparkle, etc.)
  for everyone nearby to see.
- **epic + spectacular:** as epic + particles, plus a server-wide
  effect — actionbar message visible to everyone, brief light-up of
  the sky if Sovereign joins.

**Implementation:** KubeJS `PlayerEvents.loggedIn` reads tier and
dispatches the appropriate message. Particle/sound effects are
server-side commands (`/particle`, `/playsound`).

### 11.7 Future perk dimensions (not in V1, but easy to add later)

Architecture supports adding new perk columns in `tiers.json`
without code changes (the schema is open-ended). Candidates for
later expansion:

- **Waystone slot cap** — limit how many waystones a player can place
  and/or own. Starter waystone is granted; higher tiers unlock 2nd,
  3rd, etc. Cleanest in-theme perk for a Create Aeronautics pack
  where restrictive travel is intentional.
- **Waystone XP cost reduction** — vanilla Waystones charges XP per
  teleport. Tier-based discount on this cost (or eventual exemption
  for Sovereign).
- **Particle trail** — high tiers leave a visible particle wake when
  walking
- **Custom join sound** — different sound effect per tier on login
- **Custom title prefix** — text added before chat name beyond the
  colour (e.g. `⚔ Knight Matt`)
- **Priority bounty access** — higher tiers see a larger pool of
  daily bounties to pick from
- **Tier-gated vendor stock** — specific Numismatics shops only
  visible to Baron+ etc.
- **Daily login bonus** — small spur reward for logging in, scaling
  by tier
- **Death keep-inventory chance** — RNG % to keep inventory on death
  (controversial, gate carefully)

---

## 12. Open implementation questions

These can be deferred until they become blocking, but worth flagging:

1. **Sovereign tier rainbow chat name** — Minecraft text components
   don't natively support gradient text. Implementation options:
   (a) cycle through colours character by character, or (b) animate
   via packet on a tick interval. Choose during Phase 0c scaffolding.
2. **Plot release refund percentage** — proposed 50%. Tune via
   playtest.
3. **Sethome cooldown values per tier** — proposed scaling from 60s
   (low tiers) to 5s (Sovereign). Confirm in `tiers.json` schema.
4. **`/back` interaction with dimension changes** — does dying in
   the End and `/back`ing return you to the End or to overworld?
   Default: returns to wherever you died including the dimension.
5. **Founder cosmetic prefix colour** — bright yellow? Bold white?
   Animated? Settle during implementation.
6. **Sovereign spectacular join effect** — exact effects need design
   pass (sky flash? Server-wide actionbar text?). Tune in
   `tiers.json`.

---

## 13. Implementation phases (mod-specific)

Aligned with the economy doc's Phase 0c (mod scaffolding):

| Mod phase | Deliverable |
|---|---|
| **M0** | Gradle project, mod metadata, FTB Library dep wired, network channel registered, empty placeholder for each screen class. Builds + loads cleanly into the running pack. |
| **M1** | Tier system: TierConfig loads from `tiers.json`, `TierUpgradeScreen` works, perks applied via FTB Ranks promotion command. Founder cosmetic prefix. Chat colour. Join broadcast at all tiers. |
| **M2** | Plot system: `PlotRegistry` + `PlotOwnership`, FTB Chunks mixins, `PlotPurchaseScreen`. KubeJS integration for spur debit. |
| **M3** | Quest UI: `QuestLogScreen`, `BountyBoardScreen`, `QuestProgressHud`. Network packets between KubeJS bounty engine and the mod. |
| **M4** | Shop browser: `ShopBrowserScreen`. |
| **M5** | Polish — animations, sound effects, particle effects for high tiers, theming pass on all screens. |

Each phase is independently shippable as a `1.x.0` release of the
mod jar.

---

## See also

- [`economy-system.md`](./economy-system.md) — gameplay-side design (currency, quest mechanics, villager rebalance, etc.)
- `MEMORY.md` entries — KubeJS Rhino gotchas, defaultconfigs pattern
