- Fix stale test count (29 -> 86) and system order in CLAUDE.md - Add new key entry points for stat system files - Add shared build command - Document stat conventions (3d6, modifiers, drift, wire protocol) - Add NPC Stats section to README with stat descriptions and effects - Add Game Constants table pointing to shared/src/constants.ts - Update follow mode description with stats display Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.6 KiB
DFlike
A multiplayer browser game inspired by Dwarf Fortress. Watch autonomous NPCs with needs-driven AI wander, eat, rest, and socialize in a shared world. Each NPC has unique stats (physical and personality) that influence their behavior, needs, and social interactions. 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
client/ -- Phaser 3 + TypeScript renderer
chars/ -- Character sprite assets
docs/ -- Design and implementation docs
Prerequisites
- Node.js >= 20 (tested with v22)
- npm >= 10
Quick Start (Development)
npm install
# Copy character assets into the client public directory
cp -r chars client/public/assets/
# 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.
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
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, Resting, Socializing, Idle)
- Live-updating needs bars (Hunger, Energy) with color-coded fill levels
- NPC stats in a two-column grid (STR, DEX, CON, INT, PER / SOC, COU, CUR, EMP, TMP)
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
apt update && apt upgrade -y
apt install -y curl git
2. Install Node.js
Install Node.js 22.x via NodeSource:
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
cd /opt
git clone <your-repo-url> 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.
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:
# 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:
npm install -g serve
Or use nginx (recommended for production):
apt install -y nginx
nginx config (/etc/nginx/sites-available/dflike):
server {
listen 80;
server_name _;
root /opt/dflike/client/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
Enable and restart:
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:
cd /opt/dflike
npm -w server run start
The server listens on port 3001 by default. Override with the PORT environment variable:
PORT=4000 npm -w server run start
8. Run as a systemd Service
Create /etc/systemd/system/dflike.service:
[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:
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:
# 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):
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://<LXC_IP>:3000"
game-ws:
loadBalancer:
servers:
- url: "http://<LXC_IP>:3001"
Set VITE_SERVER_URL to the public domain so Socket.io connects through the proxy:
# 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
Stats also have transient modifiers (e.g., low hunger debuffs Intelligence) and baseline drift (tiny permanent shifts from experience over time).
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 |
ENERGY_DECAY_PER_TICK |
0.03 | Energy drain rate |
HUNGER_THRESHOLD |
30 | Below this, NPC seeks food |
ENERGY_THRESHOLD |
20 | Below this, NPC seeks rest |
AWARENESS_RADIUS |
5 | Base social detection range (tiles) |
SOCIAL_GLOBAL_COOLDOWN |
50 | Ticks between social interactions |
SOCIAL_PAIR_COOLDOWN |
300 | Ticks before same pair can interact again |
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 |