skara-brae-common/docs/game/tech-stack-architecture.md
Jan-Peter von Hunnius 7a9a5a3e7b docs(game): migrate game-design canon notes from Joplin into docs/game/
Eight canon notes (Game Concept Overview, Mechanics Source Mapping,
Combat System & Stat Scaling Design, Skill & Magic System, Medals &
Honours System, Lore & World Building, Idle Fantasy Inspirations,
Tech Stack & Architecture) as markdown, note title as H1, with source
line citing the Joplin note ID. Joplin keeps status notes.
2026-10-03 17:27:43 +02:00

651 lines
27 KiB
Markdown

# Skara Brae — Tech Stack & Architecture
Source: Joplin note cecd2ef2690d4d1fba3450342aa63953, migrated 2026-10-03
## 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://58b6a5c37ba1424684e2e70eb839f1e7).
| 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://d243ae57778343fca997d46b73d0ee3b) 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://58b6a5c37ba1424684e2e70eb839f1e7).
- **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://58b6a5c37ba1424684e2e70eb839f1e7) — Canonical dev/test/prod machine roles, toolchain, workspace layout, services glossary
- [Game Concept Overview](joplin://9cf8a2e701654ffbbd6512cc837c7e2f) — Full game design with crafting, pets, inn, guilds
- [Combat System & Stat Scaling Design](joplin://0ef630db4e3e4d6dab18ccaefccaaa5a) — Crit tables, BIGINT stats, combat formula
- [Idle Fantasy Inspirations](joplin://48b3a3c76f734c8dafbbf9e8b3b5f80a) — Offline sessions, workers, pets, blessings
- [Lore & World Building](joplin://867bdc9845624908a90928887a71c4c9) — Districts, NPCs, storyline
- [Project Plan](joplin://d243ae57778343fca997d46b73d0ee3b) — Roadmap, MVP scope, repo structure, decisions log