# DFlike A multiplayer browser game inspired by Dwarf Fortress. Watch autonomous NPCs with needs-driven AI wander, eat, drink, rest, forage, craft, and build in a shared world. Each NPC has unique stats (physical and personality) that influence their behavior, needs, and social interactions. NPCs form asymmetric relationships, invent new recipes via LLM-driven "eureka" moments, and develop backstories and thoughts. Connect as an observer or take control of an avatar. ## Architecture - **Server:** Node.js + Socket.io, server-authoritative ECS simulation at 10 ticks/sec - **Client:** Phaser 3 + TypeScript, Vite bundler - **Shared:** TypeScript types and constants used by both server and client ``` dflike/ shared/ -- Types and constants (no runtime code) server/ -- Node.js + Socket.io game server with ECS src/llm/ -- LLM integration (backstory, narration, invention, thoughts) src/systems/ -- ECS systems (brain, social, industry, crafting, etc.) src/config/ -- Tuning values (relationship, industry, runtime constants) src/persistence/ -- SQLite save/load client/ -- Phaser 3 + TypeScript renderer chars/ -- Character sprite assets saves/ -- SQLite save files (gitignored) docs/ -- Design and implementation docs ``` ## Prerequisites - **Node.js** >= 20 (tested with v22) - **npm** >= 10 ## Quick Start (Development) ```bash npm install # Copy character assets into the client public directory cp -r chars client/public/assets/ # (Optional) Set up LLM features — see "LLM Configuration" section below cp server/.env.example server/.env # Terminal 1: Start server (port 3001) npm -w server run dev # Terminal 2: Start client dev server (port 3000) npm -w client run dev ``` Open http://localhost:3000. Open multiple tabs for multiplayer. ## LLM Configuration (Optional) NPC backstories, thoughts, narration, and recipe invention are powered by LLM via [OpenRouter](https://openrouter.ai). These features degrade gracefully when no API key is set. Copy the example env file and add your key: ```bash cp server/.env.example server/.env # Edit server/.env with your OpenRouter API key ``` | Variable | Default | Purpose | |----------|---------|---------| | `OPENROUTER_API_KEY` | _(none)_ | OpenRouter API key ([get one here](https://openrouter.ai/keys)) | | `LLM_MODEL` | `arcee-ai/trinity-large-preview:free` | Primary model (free tier) | | `LLM_FALLBACK_MODEL` | _(none)_ | Optional fallback on rate limit (HTTP 429) | When the primary model is rate-limited, the system automatically switches to the fallback model (if configured) and reverts at the next UTC midnight. ## Controls - **WASD / Arrow Keys** -- pan camera (camera mode), move avatar (avatar mode), or cycle NPCs (follow mode) - **TAB** -- toggle between camera, avatar, and follow modes - **1** -- toggle NPC highlight in follow mode ### Follow Mode In follow mode, the camera tracks an NPC and a panel slides in from the right showing: - Composited character portrait (layered from skin + accessories + facial features) - NPC name (randomly generated fantasy names) - Current activity (Wandering, Eating, Drinking, Sleeping, Foraging, Gathering, Crafting, Building, Idle) - Live-updating needs bars (Hunger, Thirst, Energy, Productivity) with color-coded fill levels - NPC stats in a two-column grid (STR, DEX, CON, INT, PER / SOC, COU, CUR, EMP, TMP) - **Relations tab** showing relationships grouped by tier with slider visualization The panel uses an EarthBound/SNES RPG-inspired aesthetic with doubled borders, pixel font, and smooth slide transitions. ## Deployment on Debian LXC These instructions cover running DFlike on a headless Debian (or Ubuntu) LXC container, serving the game on your local network. ### 1. System Setup ```bash apt update && apt upgrade -y apt install -y curl git ``` ### 2. Install Node.js Install Node.js 22.x via NodeSource: ```bash curl -fsSL https://deb.nodesource.com/setup_22.x | bash - apt install -y nodejs node --version # should print v22.x.x ``` ### 3. Clone and Install ```bash cd /opt git clone dflike cd dflike npm install ``` ### 4. Copy Character Assets The character sprite assets in `chars/` need to be copied into the client's public directory. They are excluded from the client build via `.gitignore`. ```bash cp -r chars client/public/assets/ ``` ### 5. Build the Client By default the client connects to the game server on the same hostname it was loaded from (port 3001). To override, set the `VITE_SERVER_URL` environment variable before building: ```bash # Default: auto-detects from browser hostname (works for most setups) npm -w client run build # Or specify an explicit server URL VITE_SERVER_URL=http://192.168.1.50:3001 npm -w client run build ``` For development, you can also create a `client/.env.local` file: ``` VITE_SERVER_URL=http://192.168.1.50:3001 ``` ### 6. Serve the Client Build Install a simple static file server to serve the built client: ```bash npm install -g serve ``` Or use nginx (recommended for production): ```bash apt install -y nginx ``` **nginx config** (`/etc/nginx/sites-available/dflike`): ```nginx server { listen 80; server_name _; root /opt/dflike/client/dist; index index.html; location / { try_files $uri $uri/ /index.html; } } ``` Enable and restart: ```bash ln -s /etc/nginx/sites-available/dflike /etc/nginx/sites-enabled/ rm -f /etc/nginx/sites-enabled/default nginx -t && systemctl restart nginx ``` ### 7. Run the Game Server Quick start: ```bash cd /opt/dflike npm -w server run start ``` The server listens on port 3001 by default. Override with the `PORT` environment variable: ```bash PORT=4000 npm -w server run start ``` ### 8. Run as a systemd Service Create `/etc/systemd/system/dflike.service`: ```ini [Unit] Description=DFlike Game Server After=network.target [Service] Type=simple WorkingDirectory=/opt/dflike ExecStart=/usr/bin/npx tsx server/src/main.ts Restart=on-failure RestartSec=5 Environment=PORT=3001 Environment=NODE_ENV=production [Install] WantedBy=multi-user.target ``` Enable and start: ```bash systemctl daemon-reload systemctl enable dflike systemctl start dflike systemctl status dflike # verify it's running journalctl -u dflike -f # view logs ``` ### 9. Firewall If you have a firewall enabled, open the necessary ports: ```bash # Game server (Socket.io) ufw allow 3001/tcp # Web server (nginx) ufw allow 80/tcp ``` ### Deploying with Traefik (Reverse Proxy) If you're exposing the game through Traefik on a domain (e.g. `game.example.com`), you need two routes: one for the static client and one for the Socket.io game server. **Traefik dynamic config** (e.g. `game.yaml`): ```yaml http: routers: game-ws: rule: "Host(`game.example.com`) && PathPrefix(`/socket.io`)" entryPoints: - websecure service: game-ws tls: certResolver: letsencrypt game: rule: "Host(`game.example.com`)" entryPoints: - websecure service: game tls: certResolver: letsencrypt services: game: loadBalancer: servers: - url: "http://:3000" game-ws: loadBalancer: servers: - url: "http://:3001" ``` Set `VITE_SERVER_URL` to the public domain so Socket.io connects through the proxy: ```bash # In client/.env.local VITE_SERVER_URL=https://game.example.com ``` **Security note:** Do not expose `vite dev` to the internet. Use `vite preview` or serve `client/dist/` with a static file server (nginx, serve, etc.) for production. ### Summary of Ports | Service | Port | Protocol | |---------------|------|----------| | Game server | 3001 | TCP | | Client (dev) | 3000 | TCP | | Client (prod) | 80 | TCP | ## NPC Stats Each NPC is generated with 10 stats using 3d6 rolls (range 3-18, bell curve centered at ~10.5): | Physical | Personality | |----------|-------------| | **Strength** -- physical tasks | **Sociability** -- social initiation, cooldown | | **Dexterity** -- movement, precision | **Courage** -- exploration, threat response | | **Constitution** -- needs decay rate | **Curiosity** -- wander variety, distance | | **Intelligence** -- learning/drift rate | **Empathy** -- social outcome bias | | **Perception** -- awareness radius | **Temperament** -- emotional stability | Stats influence gameplay systems: - **Constitution** scales hunger/energy decay rates - **Perception** extends or shrinks the social awareness radius - **Sociability** reduces social interaction cooldowns - **Empathy** biases social interactions toward positive outcomes - **Courage** influences willingness to interact with disliked NPCs - **Curiosity** extends awareness radius for NPCs not yet encountered Stats also have **transient modifiers** (e.g., low hunger debuffs Intelligence) and **baseline drift** (tiny permanent shifts from experience over time). ## Relationships NPCs form relationships through social interactions. Each relationship is tracked independently per NPC (asymmetric — A's view of B can differ from B's view of A). **How it works:** - Relationships are lazy-initialized on first encounter (NPCs don't "know" each other until they interact) - Each interaction updates relationship values (-100 to 100) based on outcome, with **diminishing returns** on repeated interactions - Deltas are **blended asymmetrically**: 30% shared outcome + 70% individual perception - Stats influence relationships: **Empathy** boosts positive gains, **Temperament** amplifies negative losses, **Sociability** adds a general bonus and determines friend capacity **Classification tiers** (derived from value): | Range | Tier | |-------|------| | 80 to 100 | Partner | | 50 to 79 | Close Friend | | 20 to 49 | Friend | | 5 to 19 | Acquaintance | | -4 to 4 | Stranger | | -19 to -5 | Wary | | -49 to -20 | Rival | | -79 to -50 | Enemy | | -100 to -80 | Nemesis | **Caps:** Partner cap defaults to 1 (2 for high sociability + empathy NPCs). Friend cap scales with sociability. **Despawn behavior:** Strong relationships (|value| >= 20) persist as **memories** when an NPC is removed. Weaker relationships fade toward zero and are cleaned up. All relationship tuning values are config-driven in `server/src/config/relationshipConfig.ts`, designed for future admin UI integration. ## Industry & Crafting NPCs autonomously gather resources, craft items, and build structures. **Workflow:** gather raw materials -> craft tools/items -> build structures -> deposit items at stockpiles - **Gathering** -- NPCs forage for raw materials (wood, stone, berries) from map tiles - **Crafting** -- Consumes input items, produces output items (40 tick base, modified by dexterity) - **Building** -- Creates structure entities with build progress 0->1 (stockpiles, workbenches) - **Dropoff/Pickup** -- NPCs deposit crafted items at stockpiles and retrieve materials for crafting **Seed recipes:** `craft_wooden_axe`, `craft_hammer`, `build_stockpile`, `build_workbench` **LLM Inventions:** NPCs with high intelligence and curiosity can have "eureka" moments, where the LLM generates novel recipes based on available materials and existing knowledge. Inventions are persisted and shared across all NPCs. ## Persistence World state is saved to SQLite (`saves/default.db`) via better-sqlite3. - **Event data** (narration, memory, stockpile, inventions) written to DB immediately on occurrence - **Entity state** (NPCs, structures, relationships, bonds) batch-saved every 30 seconds - **Graceful shutdown** (Ctrl-C) triggers a final save before exit - **On startup**, loads from existing save or generates fresh world; `--new-world` flag forces fresh - **Schema versioning** via `schema_version` in metadata table with migration runner Transient state (movement paths, active interactions, goals) resets on load; NPCs resume from idle. ## Game Constants All tunable game constants live in `shared/src/constants.ts`: | Constant | Default | Purpose | |----------|---------|---------| | `MAX_NPC_COUNT` | 50 | Maximum NPCs in the world | | `TICK_RATE` | 10 | Server ticks per second | | `WORLD_WIDTH` / `WORLD_HEIGHT` | 64 | World size in tiles | | `MOVE_SPEED` | 0.75 | NPC movement speed (tiles/tick) | | `HUNGER_DECAY_PER_TICK` | 0.05 | Hunger drain rate | | `THIRST_DECAY_PER_TICK` | 0.05 | Thirst drain rate | | `ENERGY_DECAY_PER_TICK` | 0.03 | Energy drain rate | | `PRODUCTIVITY_DECAY_PER_TICK` | 0.02 | Productivity drain rate | | `HUNGER_THRESHOLD` | 30 | Below this, NPC seeks food | | `THIRST_THRESHOLD` | 30 | Below this, NPC seeks water | | `ENERGY_THRESHOLD` | 20 | Below this, NPC seeks rest | | `PRODUCTIVITY_THRESHOLD` | 40 | Below this, NPC seeks productive work | | `AWARENESS_RADIUS` | 5 | Base social detection range (tiles) | | `SOCIAL_GLOBAL_COOLDOWN` | 75 | Ticks between social interactions | | `SOCIAL_PAIR_COOLDOWN` | 450 | Ticks before same pair can interact again | **Day/Night Cycle:** | Constant | Default | Purpose | |----------|---------|---------| | `DAY_HOURS` | 12 | Length of daytime | | `NIGHT_HOURS` | 6 | Length of nighttime | | `DAY_NIGHT_RATIO` | 2 | Day-to-night ratio | | `SLEEP_WAKE_THRESHOLD` | 85 | Energy level at which sleeping NPCs wake | | `SLEEP_VOLUNTARY_ENERGY_THRESHOLD` | 60 | Energy below which NPCs voluntarily sleep at night | | `NAP_BUFFER` | 15 | Extra energy margin for daytime nap decisions | ## Development Commands | Command | Description | |--------------------------------|------------------------------------| | `npm install` | Install all workspace dependencies | | `npm -w server run dev` | Start server with hot reload | | `npm -w server run start` | Start server (production) | | `npm -w server run test` | Run server tests | | `npm -w server run test:watch` | Run server tests in watch mode | | `npm -w client run dev` | Start client dev server | | `npm -w client run build` | Build client for production | | `npx -w shared tsc` | Rebuild shared types |