Two bugs fixed: 1. NPC sprites not fully rendering: createEntitySprite is async but was called without await from handleStateUpdate. Concurrent state updates triggered multiple simultaneous creation calls for the same entity. The second call composited before textures loaded, producing empty spritesheets. Fixed with a creatingEntities guard set. 2. Interaction emojis not appearing after first use: animationend listener was attached during removal (when animation had already completed), so it never fired. Ghost entries in activeEmojis map blocked future emoji creation. Fixed by self-cleaning via animationend listener attached at creation time. 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, and rest in a shared world. 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, Idle)
- Live-updating needs bars (Hunger, Energy) with color-coded fill levels
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 |
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 |