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.
This commit is contained in:
parent
2343d5e154
commit
7a9a5a3e7b
8 changed files with 2810 additions and 0 deletions
651
docs/game/tech-stack-architecture.md
Normal file
651
docs/game/tech-stack-architecture.md
Normal file
|
|
@ -0,0 +1,651 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue