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.
651 lines
27 KiB
Markdown
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
|