414 lines
15 KiB
Markdown
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)
|