Remove pointsOfInterest from WorldState type, state serializer, and client rendering. Remove foodPositions from map generator interface and output. Update stale comment in worldSerializer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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)
- 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
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
- 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.
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 |