diff --git a/docs/superpowers/specs/2026-05-29-zombie-invasion-backend-design.md b/docs/superpowers/specs/2026-05-29-zombie-invasion-backend-design.md new file mode 100644 index 0000000..83ccf2f --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-zombie-invasion-backend-design.md @@ -0,0 +1,413 @@ +# 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-accept mock +| Method | Path | Purpose | +|--------|------|---------| +| POST | `/api/payments/robokassa/link` | Returns mock success, grants shards | +| POST | `/api/payments/bundles/link` | Instantly unlocks bundle | +| GET | `/api/payments/deals?steamId=` | Returns mock deal catalog | + +### 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: + - "3000: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)