Skara Brae — Tech Stack & Architecture

# Skara Brae — Tech Stack & Architecture

## Why This Stack in 2026

Torn launched in 2004 as a PHP/HTML text game. It received an engine overhaul in 2014 and a major Crimes 2.0 update in 2023. Modern Torn has real-time features, mobile apps, and a rich API.

For Skara Brae, we build for **2026+ expectations**:
- Instant real-time feedback (WebSocket everywhere)
- Mobile-first (PWA, not just responsive)
- API-first design (community tools from day one)
- Type-safe full stack
- Idle/offline progression (server-side tick engine)

---

## Deployment Topology (self-hosted, no Docker)

> Resolved 2026-08-02. Canonical reference: [Development Environment & Server Topology](joplin://bbc49a3bbada4fcdb8fedc66abf5014e).

| Tier | Machine (OS) | Runtime |
|---|---|---|
| **Develop** | workhorse (Ubuntu 26.04, Ryzen 9 5950X 16C/32T, 128 GiB, RTX 5060 Ti) | Kilo + RustRover/WebStorm; local PostgreSQL (native package); `cargo run` + Vite. 32 threads → fast incremental builds. |
| **Test / Stage** | basement server (FreeBSD, LAN-only) | **Jenkins** CI (lint → test → cross-compile Linux x86_64 → transfer binary). A **bhyve Debian VM** mirrors prod: bare systemd units (PG + app binary + Caddy). Also file server + artifact store. |
| **Produce / Git** | Hetzner box (Debian, public) | **Forgejo** (primary git host — public + private repos), bare **systemd** game services (PG + app binary), **Caddy** (TLS). Planned **Codeberg** mirror for redundancy. |

**Flow:** commit on workhorse → push Forgejo (Hetzner, internet SSH) → Jenkins (basement) builds & cross-compiles via webhook → deploys to bhyve Debian staging → on green, SCPs binary to Hetzner prod → `systemctl restart`.

**Redis — deferrable for MVP.** Redis provides caching, leaderboards (sorted sets), and WebSocket pub/sub. At low player counts, the Rust process can hold session/leaderboard data in memory. Add Redis to prod when you need multi-process scaling or persistence across restarts. PostgreSQL handles all persistent data regardless.

**Sizing caveat:** the Hetzner box is "decent, not a powerhouse" and now also runs Forgejo (+ planned Codeberg mirror). A Rust binary + Forgejo is light, but watch combined RAM (Forgejo + Codeberg + PG + app). Set a threshold for upsizing or splitting.

---

## Art Direction (decided 2026-08-02)

**Torn-style graphical browser UI.** The game mechanics are text/numbers; the presentation is a fully graphical web application.

### UI characteristics
- **Location panels**: illustrated backgrounds per district (tavern, market, dungeon entrance, etc.)
- **Item/equipment icons**: sprite-style icons with quality-tier border glow (Common→Legendary)
- **Stat bars & HUD**: graphical HP/energy/PP bars, XP progress bars, skill meters
- **Themed color scheme**: dark fantasy palette, per-district tinting
- **Tab-based navigation**: Torn-style tab bar for core pages (City, Adventures, Combat, Inventory, Guild, etc.)
- **CSS animations**: transitions for page changes, stat updates, combat results

### Rendering approach
- All rendering via **HTML/CSS/SVG within Vue 3 SPA** — no canvas, no WebGL.
- Keeps it lightweight, accessible, and PWA-compatible.
- Vue component library with dark-fantasy theming.

### Art pipeline
- **Location art**: digital illustrations — ~11 district panels + dungeon room tiles
- **Item icons**: sprite sheet or SVG icon set
- **UI chrome**: Vue component library (stat bars, progress rings, modals, tooltips, tab bar)

---

## Multi-Repo Structure

The project is split into three repositories. See [Project Plan](joplin://088737f31c514f1da21cc42c0ab6acc1) for full rationale.

```
git@hetzner/skara-brae/         # Forgejo on Hetzner (public + private)
├── skara-brae-server/          # Rust backend (opened in RustRover)
├── skara-brae-client/          # Vue 3 frontend (opened in WebStorm)
└── skara-brae-common/          # Shared game data, API contract, constants
```

> Git source of truth: **Forgejo** on the Hetzner box (public-facing). Jenkins on the basement box connects via deploy key + webhook. See [Deployment Topology](#deployment-topology-self-hosted-no-docker).

### Local Checkout Layout (IDE Wiring)

The two consumer repos live in different JetBrains IDEs (RustRover, WebStorm). Keep all three as **siblings under one parent folder** — the parent is a plain folder, **not** a git repo:

```
~/Development/skara-brae/            # workspace root (plain folder, NOT a git repo)
├── skara-brae-server/             # RustRover opens THIS
├── skara-brae-client/             # WebStorm opens THIS
└── skara-brae-common/             # shared — 3rd window, or "Attach" in either IDE
```

> This follows the cross-project workspace convention `~/Development/<project>/`. On workhorse, legacy repos still live in per-IDE default folders (`~/RustroverProjects`, `~/WebstormProjects`, `~/IdeaProjects`); relocate on first touch, or clone into `~/Development/skara-brae/` and symlink the repo back into the IDE folder for discoverability — but always open the **real path** in the IDE so path/file deps resolve. Full convention in [Development Environment & Server Topology § Workspace & Repository Layout](joplin://bbc49a3bbada4fcdb8fedc66abf5014e).

- **RustRover (server)** — path dependency in `skara-brae-server/Cargo.toml`:
  ```toml
  sb-common = { path = "../skara-brae-common/rust" }
  ```
  Sibling path deps resolve fine. To edit common from inside RustRover: **Settings → Project Structure → Attach** (or add `../skara-brae-common` as a content root).
- **WebStorm (client)** — local file dependency in `skara-brae-client/package.json`:
  ```json
  "@skara-brae/common": "file:../skara-brae-common/ts"
  ```
  plus a Vite alias (`@common` → that path) so HMR picks up data edits. Attach the module likewise to edit it in-window.

### Shared-Repo Consumption Strategy (Bilingual Package)

`skara-brae-common` is a **bilingual package** whose source of truth is **language-neutral data + schema**, consumed at build time by both projects via codegen (never hand-duplicated types):

```
skara-brae-common/
├── data/                  # language-neutral source of truth
│   ├── items/ adventures/ crit-tables/ spells/ recipes/
│   ├── medals/ dungeons/ tower/ expeditions/        (*.json / *.toml)
│   └── schema/            # JSON Schema (entities) + openapi.yaml (API contract)
├── rust/                  # thin crate the server depends on
│   ├── Cargo.toml         #   name = "sb-common"; includes data via include_str! + serde
│   └── src/lib.rs
├── ts/                    # thin package the client depends on
│   ├── package.json       #   name = "@skara-brae/common"; generated types + data re-exports
│   └── src/index.ts
└── codegen/               # data/ + schema  →  rust/ and ts/ bindings
```

**How sharing actually works:**
- **Static game data** (`data/*.json`) is the single source of truth. Both sides consume at build time: Rust via `include_str!` + `serde`; Vue via Vite JSON import.
- **API contract — server is authoritative.** Expose OpenAPI via [`utoipa`](https://docs.rs/utoipa); generate the client's TS types with `openapi-typescript` (or `orval`).
- **Entity types** — define once as JSON Schema in `data/schema/`, generate both Rust structs (`typify` / `schemars`) and TS interfaces (`json-schema-to-typescript`) from `codegen/`.

**Two clarifications:**
1. Do **not** confuse `skara-brae-common` (cross-language, this third repo) with the `sb-shared` crate in the server workspace below — `sb-shared` is a **server-internal** Rust crate (types/errors/constants) and stays inside `skara-brae-server/crates/`.
2. **Alternative for self-contained CI**: add `skara-brae-common` as a **git submodule** inside both repos. Start with sibling + path; switch to submodules only if contributors/CI demand it.

### Cross-language dependency path workaround

Cargo workspaces **cannot** resolve dependency paths that point above the workspace root. When `sb-common` is declared in `[workspace.dependencies]` with `path = "../skara-brae-common/rust"`, Cargo resolves the path relative to the **consuming crate** (e.g., `crates/sb-api/`), not the workspace root — producing the error `failed to read .../skara-brae-server/skara-brae-common/rust/Cargo.toml`.

**Current workaround (local solo dev):** use an **absolute path** in each crate's `[dependencies]`:

```toml
# In crates/sb-api/Cargo.toml and crates/sb-core/Cargo.toml:
[dependencies]
sb-common = { path = "/home/jan/Development/skara-brae/skara-brae-common/rust" }
```

Do **not** declare `sb-common` in `[workspace.dependencies]` — only in each consuming crate.

**For CI / contributors:** switch to **git submodules** (`server/common/`, `client/common/`) so each repo is self-contained. This is the planned long-term solution. For local solo work, the absolute path is functional.

---

## Server Project Structure

```
skara-brae-server/
├── Cargo.toml                  # Workspace root (DO NOT put sb-common in [workspace.dependencies])
├── crates/
│   ├── sb-api/                 # Axum HTTP API server
│   │   ├── src/
│   │   │   ├── main.rs
│   │   │   ├── config.rs
│   │   │   ├── routes/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── auth.rs
│   │   │   │   ├── character.rs
│   │   │   │   ├── adventure.rs
│   │   │   │   ├── combat.rs
│   │   │   │   ├── market.rs
│   │   │   │   ├── guild.rs
│   │   │   │   ├── property.rs
│   │   │   │   ├── education.rs
│   │   │   │   ├── crafting.rs
│   │   │   │   ├── pets.rs
│   │   │   │   ├── blessings.rs
│   │   │   │   ├── inn.rs
│   │   │   │   └── slayer.rs
│   │   │   ├── middleware/
│   │   │   │   ├── auth.rs
│   │   │   │   └── rate_limit.rs
│   │   │   └── ws/
│   │   │       ├── handler.rs
│   │   │       └── channels.rs
│   │   └── Cargo.toml         # sb-common via absolute path
│   │
│   ├── sb-core/                # Game engine core
│   │   ├── src/
│   │   │   ├── lib.rs
│   │   │   ├── items.rs
│   │   │   ├── character/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── stats.rs         # BIGINT stat system, tiered display
│   │   │   │   ├── skills.rs
│   │   │   │   └── races.rs
│   │   │   ├── combat/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── engine.rs
│   │   │   │   ├── actions.rs
│   │   │   │   ├── formulas.rs
│   │   │   │   └── crit_tables.rs   # RoleMaster-inspired open-ended d100
│   │   │   ├── adventure/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── templates.rs
│   │   │   │   ├── rewards.rs
│   │   │   │   └── offline.rs       # Offline session reward calculation
│   │   │   ├── economy/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── market.rs
│   │   │   │   └── bank.rs
│   │   │   ├── guild/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── wars.rs
│   │   │   │   └── ranks.rs          # 10-rank progression system
│   │   │   ├── crafting/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── professions.rs    # 12 professions (5 gathering + 7 crafting)
│   │   │   │   ├── recipes.rs
│   │   │   │   └── quality.rs        # Quality roll system (Common→Legendary)
│   │   │   ├── pets/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── breeding.rs
│   │   │   │   └── bonuses.rs
│   │   │   ├── blessings/
│   │   │   │   ├── mod.rs
│   │   │   │   └── effects.rs
│   │   │   ├── workers/
│   │   │   │   ├── mod.rs             # Inn workers (NPC automation)
│   │   │   │   └── tasks.rs
│   │   │   ├── slayer/
│   │   │   │   ├── mod.rs
│   │   │   │   └── tasks.rs
│   │   │   ├── world/
│   │   │   │   ├── mod.rs
│   │   │   │   ├── time.rs         # Day/night cycle
│   │   │   │   ├── weather.rs
│   │   │   │   └── events.rs       # Dynamic world events
│   │   │   └── rules/
│   │   │       ├── mod.rs
│   │   │       └── formulas.rs      # All game formulas in one place
│   │   └── Cargo.toml         # sb-common via absolute path
│   │
│   ├── sb-db/                  # Database layer
│   │   ├── src/
│   │   │   ├── lib.rs
│   │   │   ├── models/
│   │   │   │   ├── user.rs
│   │   │   │   ├── character.rs
│   │   │   │   ├── item.rs
│   │   │   │   ├── guild.rs
│   │   │   │   ├── property.rs
│   │   │   │   ├── pet.rs
│   │   │   │   ├── crafting.rs
│   │   │   │   └── blessing.rs
│   │   │   ├── queries/
│   │   │   └── migrations/
│   │   └── Cargo.toml
│   │
│   ├── sb-auth/                # Authentication & authorization
│   │   ├── src/
│   │   │   ├── lib.rs
│   │   │   ├── jwt.rs
│   │   │   ├── hash.rs           # argon2 password hashing
│   │   │   └── session.rs
│   │   └── Cargo.toml
│   │
│   └── sb-shared/              # Server-internal shared types, errors, constants
│       ├── src/
│       │   ├── lib.rs
│       │   ├── types.rs
│       │   ├── errors.rs
│       │   └── constants.rs
│       └── Cargo.toml
│
├── migrations/                 # SQL migrations
│   ├── 001_initial.sql
│   ├── 002_character.sql
│   ├── 003_guilds.sql
│   ├── 004_pets.sql
│   ├── 005_crafting.sql
│   └── 006_blessings.sql
│
├── systemd/                     # systemd .service unit files for deployment
│   ├── skara-brae.service
│   ├── skara-brae.env          # environment variables (DB URL, Redis URL, etc.)
│   └── install.sh              # install script: copy binary + unit → /etc/systemd/system/
│
└── README.md
```

> Note: `sb-shared` above is the **server-internal** Rust crate. The **cross-language** shared repo is `skara-brae-common` (consumed via `sb-common` with an absolute path). See [Cross-language dependency path workaround](#cross-language-dependency-path-workaround) above.

---

## systemd Deployment (prod & staging)

### skara-brae.service
```ini
[Unit]
Description=Skara Brae Game Server
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=skara-brae
Group=skara-brae
ExecStart=/opt/skara-brae/bin/skara-brae-server
EnvironmentFile=/opt/skara-brae/skara-brae.env
Restart=always
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
```

### skara-brae.env
```env
DATABASE_URL=postgres://skara-brae:password@127.0.0.1:5432/skara_brae
REDIS_URL=redis://127.0.0.1:6379          # only when Redis is deployed
LISTEN_ADDR=127.0.0.1:8080
JWT_SECRET=changeme
RUST_LOG=info
```

### Caddy (reverse proxy + TLS)
```
git.yourdomain.tld {
    reverse_proxy 127.0.0.1:3000
}

api.skara-brae.tld {
    reverse_proxy 127.0.0.1:8080
}

play.skara-brae.tld {
    root * /srv/skara-brae-web
    file_server
    try_files {path} /index.html
}
```

### Jenkins deploy step (SSH to Hetzner)
```bash
scp target/release/skara-brae-server hetzner:/opt/skara-brae/bin/
ssh hetzner "systemctl restart skara-brae"
```

### Install script (first-time setup)
```bash
#!/bin/bash
set -euo pipefail
sudo useradd -r -s /bin/false skara-brae
sudo mkdir -p /opt/skara-brae/{bin,config}
sudo cp skara-brae-server /opt/skara-brae/bin/
sudo cp systemd/skara-brae.service /etc/systemd/system/
sudo cp systemd/skara-brae.env /opt/skara-brae/
sudo systemctl daemon-reload
sudo systemctl enable --now skara-brae
```

---

## Key Crate Dependencies

### sb-api (Axum server)
```toml
[dependencies]
axum = { version = "0.8", features = ["ws", "macros"] }
tokio = { version = "1", features = ["full"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sb-core = { path = "../sb-core" }
sb-db = { path = "../sb-db" }
sb-auth = { path = "../sb-auth" }
sb-shared = { path = "../sb-shared" }
sb-common = { path = "/home/jan/Development/skara-brae/skara-brae-common/rust" }
utoipa = { version = "5", features = ["axum_extras"] }
tracing = "0.1"
tracing-subscriber = "0.3"
```

### sb-core (Game engine)
```toml
[dependencies]
serde = { version = "1", features = ["derive"] }
rand = "0.9"
chrono = { version = "0.4", features = ["serde"] }
thiserror = "2"
sb-shared = { path = "../sb-shared" }
sb-common = { path = "/home/jan/Development/skara-brae/skara-brae-common/rust" }
```

### sb-db (Database)
```toml
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid", "migrate"] }
sb-shared = { path = "../sb-shared" }
serde = { version = "1", features = ["derive"] }
```

### sb-auth (Auth)
```toml
[dependencies]
jsonwebtoken = "9"
argon2 = "0.5"
serde = { version = "1", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
sb-shared = { path = "../sb-shared" }
```

---

## Database Schema (Core Tables)

```sql
-- Users
CREATE TABLE users (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username    VARCHAR(32) UNIQUE NOT NULL,
    email       VARCHAR(255) UNIQUE NOT NULL,
    password    VARCHAR(255) NOT NULL,  -- argon2 hash
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_login  TIMESTAMPTZ,
    is_banned   BOOLEAN NOT NULL DEFAULT FALSE,
    is_patron   BOOLEAN NOT NULL DEFAULT FALSE
);

-- Characters (one per user, or multiple for patrons)
CREATE TABLE characters (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID NOT NULL REFERENCES users(id),
    name        VARCHAR(32) UNIQUE NOT NULL,
    race        VARCHAR(16) NOT NULL,
    class       VARCHAR(16) NOT NULL,
    level       INT NOT NULL DEFAULT 1,
    experience  BIGINT NOT NULL DEFAULT 0,
    gold        BIGINT NOT NULL DEFAULT 100,
    energy      INT NOT NULL DEFAULT 100,
    max_energy  INT NOT NULL DEFAULT 100,
    hp          BIGINT NOT NULL DEFAULT 100,
    max_hp      BIGINT NOT NULL DEFAULT 100,
    pp          BIGINT NOT NULL DEFAULT 50,
    max_pp      BIGINT NOT NULL DEFAULT 50,
    strength    BIGINT NOT NULL DEFAULT 10,
    agility     BIGINT NOT NULL DEFAULT 10,
    constitution BIGINT NOT NULL DEFAULT 10,
    intelligence BIGINT NOT NULL DEFAULT 10,
    wisdom      BIGINT NOT NULL DEFAULT 10,
    charisma    BIGINT NOT NULL DEFAULT 10,
    location    VARCHAR(32) NOT NULL DEFAULT 'tavern',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Skills
CREATE TABLE character_skills (
    character_id  UUID NOT NULL REFERENCES characters(id),
    skill_name    VARCHAR(32) NOT NULL,
    level         INT NOT NULL DEFAULT 0,
    experience    BIGINT NOT NULL DEFAULT 0,
    PRIMARY KEY (character_id, skill_name)
);

-- Inventory
CREATE TABLE inventory (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id  UUID NOT NULL REFERENCES characters(id),
    item_id       UUID NOT NULL,
    quantity      INT NOT NULL DEFAULT 1,
    equipped_slot VARCHAR(16),
    quality       VARCHAR(16) NOT NULL DEFAULT 'common',
    acquired_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Guilds
CREATE TABLE guilds (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name        VARCHAR(64) UNIQUE NOT NULL,
    description TEXT,
    leader_id   UUID NOT NULL REFERENCES characters(id),
    treasury    BIGINT NOT NULL DEFAULT 0,
    tax_rate    SMALLINT NOT NULL DEFAULT 10 CHECK (tax_rate BETWEEN 0 AND 25),
    level       INT NOT NULL DEFAULT 1,
    reputation  BIGINT NOT NULL DEFAULT 0,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE guild_members (
    guild_id     UUID NOT NULL REFERENCES guilds(id),
    character_id UUID NOT NULL REFERENCES characters(id),
    rank         VARCHAR(16) NOT NULL DEFAULT 'initiate',
    reputation   BIGINT NOT NULL DEFAULT 0,
    joined_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (guild_id, character_id)
);

-- Properties
CREATE TABLE properties (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    property_type VARCHAR(32) NOT NULL,
    owner_id      UUID REFERENCES characters(id),
    guild_id      UUID REFERENCES guilds(id),
    level         INT NOT NULL DEFAULT 1,
    rent_price    BIGINT,
    sale_price    BIGINT,
    is_listed     BOOLEAN NOT NULL DEFAULT FALSE
);

-- Market listings
CREATE TABLE market_listings (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    seller_id     UUID NOT NULL REFERENCES characters(id),
    item_id       UUID NOT NULL,
    quantity      INT NOT NULL DEFAULT 1,
    price         BIGINT NOT NULL,
    listed_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at    TIMESTAMPTZ NOT NULL
);

-- Adventures log
CREATE TABLE adventure_log (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id  UUID NOT NULL REFERENCES characters(id),
    adventure_id  VARCHAR(64) NOT NULL,
    result        VARCHAR(16) NOT NULL,
    rewards_gold  BIGINT NOT NULL DEFAULT 0,
    rewards_xp    BIGINT NOT NULL DEFAULT 0,
    started_at    TIMESTAMPTZ NOT NULL,
    completed_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Education enrollment
CREATE TABLE education_enrollments (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id  UUID NOT NULL REFERENCES characters(id),
    course_id     VARCHAR(64) NOT NULL,
    enrolled_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completes_at  TIMESTAMPTZ NOT NULL,
    completed     BOOLEAN NOT NULL DEFAULT FALSE
);

-- Pets
CREATE TABLE pets (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id  UUID NOT NULL REFERENCES characters(id),
    pet_type      VARCHAR(32) NOT NULL,
    name          VARCHAR(32),
    rarity        VARCHAR(16) NOT NULL DEFAULT 'common',
    level         INT NOT NULL DEFAULT 1,
    experience    BIGINT NOT NULL DEFAULT 0,
    bonus_type    VARCHAR(32) NOT NULL,
    bonus_value   DECIMAL(5,2) NOT NULL DEFAULT 1.00,
    is_active     BOOLEAN NOT NULL DEFAULT FALSE,
    age_days      INT NOT NULL DEFAULT 0,
    is_dead       BOOLEAN NOT NULL DEFAULT FALSE,
    acquired_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE pet_equipment (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    pet_id     UUID NOT NULL REFERENCES pets(id) ON DELETE CASCADE,
    item_id    UUID NOT NULL,
    slot       VARCHAR(16) NOT NULL
);

-- Crafting professions
CREATE TABLE character_professions (
    character_id  UUID NOT NULL REFERENCES characters(id),
    profession    VARCHAR(32) NOT NULL,
    mastery_level VARCHAR(16) NOT NULL DEFAULT 'novice',
    experience    BIGINT NOT NULL DEFAULT 0,
    PRIMARY KEY (character_id, profession)
);

-- Crafting recipes (discovered)
CREATE TABLE character_recipes (
    character_id  UUID NOT NULL REFERENCES characters(id),
    recipe_id     VARCHAR(64) NOT NULL,
    discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (character_id, recipe_id)
);

-- Blessings (active)
CREATE TABLE active_blessings (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id  UUID NOT NULL REFERENCES characters(id),
    blessing_type VARCHAR(32) NOT NULL,
    effect_value  DECIMAL(5,2) NOT NULL,
    activated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at    TIMESTAMPTZ NOT NULL
);

-- Inn workers (hired NPCs)
CREATE TABLE hired_workers (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id  UUID NOT NULL REFERENCES characters(id),
    worker_type   VARCHAR(32) NOT NULL,
    quality       VARCHAR(16) NOT NULL DEFAULT 'novice',
    task          VARCHAR(64),
    hired_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at    TIMESTAMPTZ NOT NULL,
    cost_per_hour BIGINT NOT NULL
);

-- Slayer tasks
CREATE TABLE slayer_tasks (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id  UUID NOT NULL REFERENCES characters(id),
    enemy_type    VARCHAR(64) NOT NULL,
    target_count  INT NOT NULL,
    current_count INT NOT NULL DEFAULT 0,
    assigned_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed     BOOLEAN NOT NULL DEFAULT FALSE
);

-- Offline session state
CREATE TABLE offline_sessions (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id  UUID NOT NULL REFERENCES characters(id),
    session_type  VARCHAR(32) NOT NULL,
    started_at    TIMESTAMPTZ NOT NULL,
    max_duration  INTERVAL NOT NULL DEFAULT INTERVAL '4 hours',
    claimed       BOOLEAN NOT NULL DEFAULT FALSE
);
```

---

## Why Rust for a Game Like This

1. **Performance**: Torn serves millions of requests. Rust handles this with minimal resources.
2. **Type safety**: Complex game rules caught at compile time. `sb-core` becomes a verified game engine.
3. **Fearless concurrency**: Tokio + ownership model makes the tick engine correct by construction.
4. **No GC pauses**: Deterministic performance for real-time combat.
5. **Workspace architecture**: Clean crate separation — fast independent compilation.
6. **Ecosystem**: Axum, SQLx, Tokio, Serde — mature, production-grade in 2026.
7. **Single static binary**: Cross-compile, deploy, run. No Docker, no runtime deps.

---

## Why Vue.js (Not React/Svelte)

- **Composition API** maps to game UI patterns (reactive stat bars, inventory grids, combat logs)
- **Pinia** stores model game state naturally
- **Single-file components** co-locate template + logic + styles
- User preference (stated)
- **PWA support** via Vite plugin
- **SPA only** — no Nuxt/SSR. Game is behind auth; public pages generated separately.

---

## Related Notes

- [Development Environment & Server Topology](joplin://bbc49a3bbada4fcdb8fedc66abf5014e) — Canonical dev/test/prod machine roles, toolchain, workspace layout, services glossary
- [Game Concept Overview](joplin://fcd381c235694f29abf73665317a40f5) — Full game design with crafting, pets, inn, guilds
- [Combat System & Stat Scaling Design](joplin://5ff1fed180fa4b39b4cdb925f34c1008) — Crit tables, BIGINT stats, combat formula
- [Idle Fantasy Inspirations](joplin://fea2d81f0414484fa4edf126f8ca17ed) — Offline sessions, workers, pets, blessings
- [Lore & World Building](joplin://2c8e2a844da0497e8a93c820ec8901a1) — Districts, NPCs, storyline
- [Project Plan](joplin://088737f31c514f1da21cc42c0ab6acc1) — Roadmap, MVP scope, repo structure, decisions log

id: 980c3eb587294e4383474b94988f2f88
parent_id: d1892c7c531848f5a5a3ac5e1749f7cf
created_time: 2026-06-11T13:32:48.037Z
updated_time: 2026-08-02T11:31:54.287Z
is_conflict: 0
latitude: 0.00000000
longitude: 0.00000000
altitude: 0.0000
author: 
source_url: 
is_todo: 0
todo_due: 0
todo_completed: 0
source: joplin-desktop
source_application: net.cozic.joplin-desktop
application_data: 
order: 1781184768037
user_created_time: 2026-06-11T13:32:48.037Z
user_updated_time: 2026-08-02T11:31:54.287Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 0
is_locked: 0
extracted_resource_ids: 
type_: 1