Files
Dota-Zombie-Invasion-Backend/docs/superpowers/specs/2026-05-29-zombie-invasion-backend-design.md
T
2026-05-29 20:04:31 +07:00

414 lines
15 KiB
Markdown

# Zombie Invasion — Backend & Admin Panel Design
## Overview
A lightweight Next.js application that serves as both:
- The **REST API backend** that the Zombie Invasion Dota 2 custom game client talks to
- An **admin panel** (served under `/admin`) for managing all player data
Deployed as a single Docker container with SQLite for storage. All payment-related endpoints auto-accept (no real payment integration).
## Tech Stack
- **Framework:** Next.js 14 (App Router)
- **Database:** SQLite via `better-sqlite3`
- **Language:** TypeScript
- **Container:** Docker (single container, multi-stage build)
- **Port:** 3000 (same as the original game server)
## Architecture
```
nextjs-app/
├── Dockerfile # Multi-stage: build → run
├── docker-compose.yml # Single service
├── docker-entrypoint.sh # DB init + seed + start
├── next.config.js
├── package.json
├── src/
│ ├── app/
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Redirects to /admin
│ │ ├── admin/
│ │ │ ├── layout.tsx # Admin sidebar + auth check
│ │ │ ├── page.tsx # Dashboard (counts, quick-stats)
│ │ │ ├── login/page.tsx # Simple password login
│ │ │ ├── players/[steamId]/page.tsx # Single player editor
│ │ │ ├── players/page.tsx # Player list + search
│ │ │ ├── battlepass/[steamId]/page.tsx
│ │ │ ├── battlepass/page.tsx # BP overview
│ │ │ ├── matches/page.tsx # Match history browser
│ │ │ ├── promocodes/page.tsx # Manage promo codes
│ │ │ ├── store/page.tsx # Purchases, currencies
│ │ │ ├── contracts/page.tsx # Death sentence contracts
│ │ │ └── arsenal/page.tsx # Arsenal & marketplace
│ │ └── api/
│ │ └── [...path]/
│ │ └── route.ts # Catch-all: dispatches game client requests
│ └── lib/
│ ├── db.ts # SQLite singleton + schema init
│ ├── seed.ts # Initial data (promo codes, sample quests)
│ ├── auth.ts # Simple admin auth helpers
│ └── handlers/
│ ├── player.ts # Profile, currency, history, purchases
│ ├── battlepass.ts # BP data, quests, claim rewards
│ ├── game.ts # Match tracking, heartbeat
│ ├── payments.ts # Auto-accept mock payments
│ ├── leaderboard.ts # Leaderboard queries
│ ├── cards.ts # Card levels, decks
│ ├── equipment.ts # Equipment state
│ ├── arsenal.ts # Arsenal loadouts + inventory
│ ├── marketplace.ts # Marketplace listings + sales
│ └── contracts.ts # Death sentence contracts
├── data/ # SQLite DB file (Docker volume mount)
└── Dockerfile
```
## Database Schema
### `players`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT PK | |
| player_name | TEXT NOT NULL | |
| profile_level | INTEGER | Default 1 |
| free_currency | INTEGER | Default 0 |
| donate_currency | INTEGER | Default 0 |
| dust_currency | INTEGER | Default 0 |
| arcade_pack_credits | TEXT | JSON `{standard, premium}` |
| sounds_wheel | TEXT | JSON object of `sound_id → true` |
| created_at | TEXT | ISO datetime |
| updated_at | TEXT | ISO datetime |
### `game_sessions`
| Column | Type | Notes |
|--------|------|-------|
| game_id | TEXT PK | |
| match_id | INTEGER | Shared across party |
| session_id | TEXT | |
| status | TEXT | `active` / `completed` |
| created_at | TEXT | |
### `game_history`
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK AUTO | |
| steam_id | TEXT NOT NULL | |
| game_id | TEXT | |
| match_id | INTEGER | |
| result | TEXT | `win` / `loss` |
| hero | TEXT | |
| hero_level | INTEGER | |
| difficulty | TEXT | |
| duration | INTEGER | seconds |
| kills | INTEGER | |
| deaths | INTEGER | |
| score | INTEGER | net worth |
| outgoing_damage | REAL | |
| incoming_damage | REAL | |
| items | TEXT | comma-separated |
| modifiers | TEXT | comma-separated |
| aghanim_scepter | INTEGER | 0/1 |
| aghanim_shard | INTEGER | 0/1 |
| gold_earned | INTEGER | |
| session_id | TEXT | |
| created_at | TEXT | |
### `battle_passes`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT PK | |
| level | INTEGER | Default 0 |
| experience | INTEGER | Default 0 |
| has_premium | INTEGER | 0/1 |
| claimed_rewards | TEXT | JSON array of level numbers |
| claimed_premium_rewards | TEXT | JSON array of level numbers |
| created_at | TEXT | |
| updated_at | TEXT | |
### `battle_pass_quests`
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK AUTO | |
| steam_id | TEXT NOT NULL | |
| quest_id | TEXT NOT NULL | |
| type | TEXT | `kill_zombies`, `survive_time`, etc. |
| name | TEXT | |
| description | TEXT | |
| progress | INTEGER | |
| target | INTEGER | |
| completed | INTEGER | 0/1 |
| claimed | INTEGER | 0/1 |
| reward_exp | INTEGER | |
| reward_free_currency | INTEGER | |
| quality | TEXT | nullable |
| npc | TEXT | nullable |
| target_item | TEXT | nullable |
| created_at | TEXT | |
| updated_at | TEXT | |
### `purchases`
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK AUTO | |
| steam_id | TEXT NOT NULL | |
| item_id | TEXT NOT NULL | |
| item_category | TEXT | `items`, `cards`, `chat_wheel_sound`, etc. |
| card_id | INTEGER | nullable |
| price_free | INTEGER | |
| price_donate | INTEGER | |
| price_dust | INTEGER | |
| created_at | TEXT | |
### `active_effects`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT PK | |
| effects | TEXT | JSON: `{effect_type: effect_id}` |
| updated_at | TEXT | |
### `promo_codes`
| Column | Type | Notes |
|--------|------|-------|
| code | TEXT PK | |
| free_currency | INTEGER | reward amount |
| donate_currency | INTEGER | reward amount |
| dust_currency | INTEGER | reward amount |
| max_uses | INTEGER | default 1 |
| current_uses | INTEGER | |
| expires_at | TEXT | nullable, ISO datetime |
### `promo_redemptions`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT | PK (composite) |
| code | TEXT | PK (composite) |
| redeemed_at | TEXT | |
### `card_levels`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT PK | |
| card_levels | TEXT | JSON: `{card_id: level}` |
| updated_at | TEXT | |
### `decks`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT | PK (composite) |
| deck_index | INTEGER | PK (composite) |
| name | TEXT | |
| cards | TEXT | JSON array of card IDs |
| updated_at | TEXT | |
### `equipment`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT PK | |
| equipment | TEXT | JSON: `{weapon, armor, ...}` |
| updated_at | TEXT | |
### `arsenal_loadouts`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT | PK (composite) |
| hero_name | TEXT | PK (composite) |
| loadout | TEXT | JSON: `{weapon, armor}` |
| updated_at | TEXT | |
### `arsenal_inventory`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT | PK (composite) |
| instance_id | TEXT | PK (composite) |
| item_name | TEXT | |
| quality | TEXT | |
| upgrade_level | INTEGER | |
| serial | INTEGER | |
| global_serial | INTEGER | |
| owner_name | TEXT | |
| pinned | INTEGER | 0/1 |
| favorite | INTEGER | 0/1 |
| stats | TEXT | JSON array |
### `arsenal_market_listings`
| Column | Type | Notes |
|--------|------|-------|
| listing_id | TEXT PK | |
| steam_id | TEXT NOT NULL | |
| instance_id | TEXT | |
| item_name | TEXT | |
| quality | TEXT | |
| upgrade_level | INTEGER | |
| serial | INTEGER | |
| global_serial | INTEGER | |
| price_free | INTEGER | |
| status | TEXT | `active` / `sold` / `cancelled` |
| created_at | TEXT | |
### `arsenal_market_sales`
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER PK AUTO | |
| listing_id | TEXT | |
| seller_steam_id | TEXT | |
| buyer_steam_id | TEXT | |
| item_name | TEXT | |
| price_free | INTEGER | |
| created_at | TEXT | |
### `death_sentence_contracts`
| Column | Type | Notes |
|--------|------|-------|
| steam_id | TEXT PK | |
| contracts | TEXT | JSON roster |
| updated_at | TEXT | |
## API Endpoints (Game Client)
All under `/api/`. The catch-all route looks at the URL path and HTTP method to dispatch.
### Player (`/api/player/:steamId`)
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/player` | Create profile |
| GET | `/api/player/:steamId` | Get profile + currencies + stats |
| GET | `/api/player/:steamId/history` | Match history (limit, offset) |
| GET | `/api/player/:steamId/currency` | Get currency balances |
| PUT | `/api/player/:steamId/currency` | Save currency |
| POST | `/api/player/:steamId/currency/give` | Grant currency (BP rewards) |
| POST | `/api/player/:steamId/purchases` | Record a purchase |
| POST | `/api/player/:steamId/promo/redeem` | Redeem promo code |
| GET | `/api/player/:steamId/sounds_wheel` | Get chat wheel sounds |
| PUT | `/api/player/:steamId/sounds_wheel` | Save chat wheel sounds |
| POST | `/api/player/:steamId/deal-purchase` | Buy a deal |
| GET | `/api/player/:steamId/active_effects` | Get equipped effects |
| PUT | `/api/player/:steamId/active_effects` | Save equipped effects |
| GET | `/api/player/:steamId/card-levels` | Get card levels |
| PUT | `/api/player/:steamId/card-levels` | Update card levels |
| GET | `/api/player/:steamId/decks` | Get all decks |
| GET | `/api/player/:steamId/decks/:index` | Get one deck |
| PUT | `/api/player/:steamId/decks/:index` | Save one deck |
| GET | `/api/player/:steamId/equipment` | Get equipment |
| PUT | `/api/player/:steamId/equipment` | Save equipment |
| POST | `/api/player/:steamId/equipment/drop` | Equipment drop |
| GET | `/api/player/:steamId/arsenal_loadouts` | Get arsenal loadouts |
| PUT | `/api/player/:steamId/arsenal_loadouts` | Save arsenal loadouts |
| GET | `/api/player/:steamId/arsenal_inventory` | Get arsenal inventory |
| PUT | `/api/player/:steamId/arsenal_inventory` | Save arsenal inventory |
| GET | `/api/player/:steamId/arsenal_market/my_listings` | My active listings |
| GET | `/api/player/:steamId/arsenal_market/slots` | Market slot info |
| GET | `/api/player/:steamId/arsenal_market/sales` | My sales history |
| POST | `/api/player/:steamId/arsenal_market/create` | Create listing |
| POST | `/api/player/:steamId/arsenal_market/buy` | Buy from listing |
| POST | `/api/player/:steamId/arsenal_market/cancel` | Cancel listing |
| GET | `/api/player/:steamId/death_sentence_contracts` | Get contracts |
| PUT | `/api/player/:steamId/death_sentence_contracts` | Save contracts |
### Battle Pass (`/api/battlepass`)
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/battlepass` | Create BP |
| GET | `/api/battlepass/:steamId` | Get BP data |
| POST | `/api/battlepass/:steamId/hero-played` | Record hero played |
| GET | `/api/battlepass/:steamId/quests` | Get quests |
| POST | `/api/battlepass/:steamId/quests/progress` | Sync quest progress |
| POST | `/api/battlepass/:steamId/quests/claim` | Claim quest reward |
| POST | `/api/battlepass/:steamId/claim` | Claim BP level reward |
| POST | `/api/battlepass/:steamId/claim-premium` | Claim premium level reward |
| POST | `/api/battlepass/:steamId/claim-all` | Claim all rewards |
| POST | `/api/battlepass/:steamId/buy-premium` | Buy premium BP |
| POST | `/api/battlepass/:steamId/addexp` | Add BP XP |
### Game (`/api/game`)
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/game/start` | Register game start |
| POST | `/api/game/heartbeat` | Match heartbeat |
| POST | `/api/game` | Save game result |
| GET | `/api/game/:id/players` | Get match participants |
### Payments (`/api/payments`) — auto-grant (no actual payment)
| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/payments/robokassa/link` | Instantly grants purchased currency to player balance, writes to DB |
| POST | `/api/payments/bundles/link` | Instantly grants bundle items/writes purchase to DB |
| GET | `/api/payments/deals?steamId=` | Returns deal catalog (deals purchasable with in-game currency) |
### Leaderboard (`/api/leaderboard`)
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/leaderboard?limit=&offset=&board=` | Leaderboard by rating/wealth |
### Marketplace (`/api/arsenal_market`)
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/arsenal_market/listings` | Public listings (with optional stat filters) |
## Response Format
The game client (Lua) expects JSON responses. The catch-all handler wraps each response:
- Success (2xx): returns the JSON body directly
- 404: returns `{error: "Not found"}`
- The game handles both wrapped responses `{ok: true, data: ...}` and unwrapped objects
Since different game modules expect different response shapes (some expect arrays, some expect objects, some look for specific keys), each handler returns the exact shape the game code expects.
## Admin Panel
### Authentication
- Single password set via `ADMIN_PASSWORD` env var
- Cookie-based session (simple, no JWT library needed)
- Login page at `/admin/login`
- All `/admin/*` routes check auth middleware
### Pages
| Path | Content |
|------|---------|
| `/admin` | Dashboard with quick stats (player count, games played, active BPs) |
| `/admin/players` | Searchable player list with currency/level/bp overview |
| `/admin/players/[steamId]` | Edit all player fields, view purchases, effects |
| `/admin/battlepass` | Overview of all BPs with search |
| `/admin/battlepass/[steamId]` | Edit BP level/XP/premium, add/manage quests |
| `/admin/matches` | Browse match history, filter by player/hero/difficulty |
| `/admin/promocodes` | List, create, edit, delete promo codes |
| `/admin/store` | View player purchases and active effects |
| `/admin/contracts` | View/edit death sentence contracts |
| `/admin/arsenal` | View inventory, loadouts, marketplace listings |
## Docker Setup
```dockerfile
# Multi-stage build: node:20-alpine
# Stage 1: Install deps + build Next.js
# Stage 2: Run with production deps + SQLite data volume
```
```yaml
# docker-compose.yml
services:
app:
build: .
ports:
- "6100:3000" # Host:6100 → Container:3000
volumes:
- ./data:/app/data # Persist SQLite DB
environment:
- ADMIN_PASSWORD=admin123
```
## Seed Data
On first run (empty DB), the entrypoint seeds:
- A few promo codes (e.g. `WELCOME100`, `ZOMBIE500`)
- The DB schema itself (via `db.ts` CREATE TABLE IF NOT EXISTS)
- A test player if none exist
## Non-Goals
- No user registration / multi-tenant support (single personal server)
- No real payment processing
- No WebSocket / real-time features
- No metrics / logging beyond basic requests
- No automated test suite (manual testing via game client + admin panel)