An OGame/Travian/Civ 6/Stellaris inspired browser strategy game.
starbound/
├── backend/ Rust · Axum · SQLx (SQLite local / PostgreSQL prod)
├── frontend/ TypeScript · Vite · Three.js (no framework)
├── tauri-app/ Tauri desktop shell
├── package.json pnpm workspace root
└── pnpm-workspace.yaml
cd backend
cp .env.example .env # tweak if needed
cargo run
# Server at http://localhost:3000The .env.example defaults to:
- SQLite database (
starbound.dbauto-created) GAME_SPEED=5(5× real-time for local testing)ALLOW_PLAYER_SPEED=true(speed buttons shown in UI)
cd .. # workspace root
pnpm install
pnpm dev # Vite dev server at http://localhost:5173Then open http://localhost:5173 → register → play.
# Requires Tauri CLI: cargo install tauri-cli
pnpm tauri:devcd backend
cp .env.production .env
# Fill in DATABASE_URL, JWT_SECRET
cargo build --release
./target/release/serverPostgreSQL requires PostGIS and btree_gist extensions (installed automatically by the migration if the DB user has superuser rights):
CREATE EXTENSION postgis;
CREATE EXTENSION btree_gist;| Mode | .env setting |
UI speed buttons |
|---|---|---|
| Local dev | GAME_SPEED=5 |
Shown (x1–x100) |
| LAN play | GAME_SPEED=1 |
Shown |
| Public server | GAME_SPEED=1 + ALLOW_PLAYER_SPEED=false |
Hidden |
How it works:
- All timers are stored in game ticks (not wall-clock seconds).
real_tick_ms = tick_ms / game_speed— the tick fires faster at higher speed.- Changing speed mid-game is instant: a unit with 10 ticks remaining still has 10 ticks remaining, but they arrive 5× sooner at x5 speed.
- Speed is persisted in
server_configtable and restored on restart. - SSE broadcasts
speed_changedso all clients resync their countdown timers.
The game does not use WebSockets. Instead:
| Event | How the client learns |
|---|---|
| Unit arrival / fleet ETA | Poll /api/units when local timer expires |
| Construction complete | Poll /api/planets/:id when timer expires |
| Research complete | Poll /api/research when timer expires |
| Battle started | SSE battle_started event (instant) |
| Battle ended | SSE battle_ended event |
| Tick / speed change | SSE tick / speed_changed events |
| Missed SSE events (reconnect) | Poll /api/events/missed?since_id=N |
Currently using procedural placeholder geometry. To swap in real models:
- Export as
.glb(GLTF binary) with animations namedidle,walk,fight. - Place in
frontend/public/models/<unit_type>.glb. - In
src/engine/models/placeholder.ts, replacecreateUnitModel()with:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
const gltf = await loader.loadAsync(`/models/${unitType}.glb`);
const mixer = new THREE.AnimationMixer(gltf.scene);
const actions = {
idle: mixer.clipAction(THREE.AnimationClip.findByName(gltf.animations, 'idle')),
walk: mixer.clipAction(THREE.AnimationClip.findByName(gltf.animations, 'walk')),
fight: mixer.clipAction(THREE.AnimationClip.findByName(gltf.animations, 'fight')),
};| View | Style | Entry |
|---|---|---|
| Planet surface | Civ 6 hex tiles | Default on login |
| Solar system | Stellaris | Click "🚀 Solar System" button |
| Galaxy map | Star points | (future: click hyperjump target) |
- All timers in game ticks — speed changes don't invalidate existing orders.
- Lazy tile generation — only tiles players have explored exist in DB.
- Procedural universe — galaxies/systems/planets outside player activity are generated from seeds on demand, never stored.
- PostGIS on Postgres —
space_pos geometry(PointZ)enablesST_DWithinproximity queries for "find all ships near coordinate X". - btree_gist — unique constraint on
battles(tile_id)prevents double-battle race conditions. - SSE event log —
sse_eventstable lets reconnecting clients fetch alerts they missed.