# Quest System Design (MVP) Status: Draft Date: 2026-03-28 --- ## 1. Town Definitions Towns are fixed locations along the hero's walking road, one per biome. They replace the procedural `isCityMarket` plaza clusters with authored, named settlements. The hero visits each town automatically while auto-walking through the biome. | # | Town Name | Biome | Level Range | World X (approx) | Description | |---|-----------|-------|-------------|-------------------|-------------| | 1 | Willowdale | Meadow | 1-5 | 50 | Starting village, tutorial NPCs | | 2 | Thornwatch | Forest | 5-10 | 200 | Logging camp turned outpost | | 3 | Ashengard | Ruins | 10-16 | 400 | Crumbling fortress with survivors | | 4 | Redcliff | Canyon | 16-22 | 650 | Mining settlement on canyon edge | | 5 | Boghollow | Swamp | 22-28 | 900 | Stilt village above murky waters | | 6 | Cinderkeep | Volcanic | 28-34 | 1200 | Forge town in cooled lava flows | | 7 | Starfall | Astral | 34-40 | 1550 | Floating platform outpost | Each town occupies a rectangular region roughly 15x15 tiles centered on the road, rendered as `plaza` terrain. The hero enters when `position_x` falls within the town's bounding box. ### Town radius and entry - Town center is at `(world_x, world_y)` stored in the `towns` table. - Entry radius: 8 tiles (configurable). Hero is "in town" when `distance(hero, town_center) <= radius`. - While in town the hero's state remains `walking` -- no combat spawns inside the radius. --- ## 2. NPC Types Three NPC archetypes for MVP. Each NPC belongs to exactly one town. | Type | Role | Interaction | |------|------|-------------| | `quest_giver` | Offers and completes quests | Tap to see available/active quests | | `merchant` | Sells potions for gold | Tap to open shop (buy potions) | | `healer` | Restores HP for gold | Tap to heal (costs gold, instant) | ### NPC placement - Each town has 2-3 NPCs (1 quest giver always, +1-2 of merchant/healer). - NPCs have a fixed offset from the town center (`offset_x`, `offset_y` in tiles). - NPCs are non-interactive during combat (hero auto-walks past if fighting). ### NPC data model NPCs are seeded, not player-created. Each has a `name`, `type`, `town_id`, and position offset. --- ## 3. Quest Types (MVP) Three quest types, all trackable with a single integer counter. ### 3.1 `kill_count` -- Kill N enemies - **Objective**: Kill `target_count` enemies of a specified type (or any type if `target_enemy_type` is NULL). - **Tracking**: Increment `hero_quests.progress` each time the hero kills a matching enemy. - **Example**: "Slay 10 Forest Wolves" (target_enemy_type = 'wolf', target_count = 10). ### 3.2 `visit_town` -- Visit a specific town - **Objective**: Walk to the target town. - **Tracking**: Set `progress = 1` when the hero enters the target town's radius. - **Example**: "Deliver a message to Ashengard" (target_town_id = 3, target_count = 1). ### 3.3 `collect_item` -- Collect N item drops - **Objective**: Collect `target_count` of a quest-specific drop from enemies. - **Tracking**: When the hero kills an enemy in the quest's level range, roll a drop chance. On success, increment `progress`. - **Drop chance**: 30% per eligible kill (configurable per quest in `drop_chance`). - **Example**: "Collect 5 Spider Fangs" (target_count = 5, target_enemy_type = 'spider', drop_chance = 0.3). ### Quest lifecycle ``` available -> accepted -> (progress tracked) -> completed -> rewards claimed ``` - `available`: Quest is offered by an NPC; hero has not accepted it. - `accepted`: Hero tapped "Accept". Progress begins tracking. - `completed`: `progress >= target_count`. Rewards are claimable. - `claimed`: Rewards distributed. Quest removed from active log. A hero can have at most **3 active (accepted) quests** at a time. This keeps the mobile UI simple. ### Level gating Each quest has `min_level` / `max_level`. NPCs only show quests appropriate for the hero's current level. --- ## 4. Reward Structure Rewards are defined per quest template. MVP rewards are additive (all granted on claim). | Reward Field | Type | Description | |--------------|------|-------------| | `reward_xp` | BIGINT | XP granted | | `reward_gold` | BIGINT | Gold granted | | `reward_potions` | INT | Healing potions granted (0-3) | ### Reward scaling guidelines | Quest Difficulty | XP | Gold | Potions | |------------------|-----|------|---------| | Trivial (kill 5) | 20-50 | 10-30 | 0 | | Normal (kill 15) | 80-150 | 50-100 | 1 | | Hard (collect 10) | 200-400 | 100-250 | 2 | | Journey (visit distant town) | 100-300 | 50-150 | 1 | Reward values scale with quest `min_level`. Rough formula: `base_reward * (1 + min_level * 0.1)`. --- ## 5. Database Schema All tables use the same conventions as the existing schema: `BIGSERIAL` PKs, `TIMESTAMPTZ` timestamps, `IF NOT EXISTS` guards. ### 5.1 `towns` Stores the 7 fixed town definitions. ```sql CREATE TABLE IF NOT EXISTS towns ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, biome TEXT NOT NULL, world_x DOUBLE PRECISION NOT NULL, world_y DOUBLE PRECISION NOT NULL, radius DOUBLE PRECISION NOT NULL DEFAULT 8.0, level_min INT NOT NULL DEFAULT 1, level_max INT NOT NULL DEFAULT 100, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` ### 5.2 `npcs` Non-hostile NPCs, each tied to a town. ```sql CREATE TABLE IF NOT EXISTS npcs ( id BIGSERIAL PRIMARY KEY, town_id BIGINT NOT NULL REFERENCES towns(id) ON DELETE CASCADE, name TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('quest_giver', 'merchant', 'healer')), offset_x DOUBLE PRECISION NOT NULL DEFAULT 0, offset_y DOUBLE PRECISION NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` ### 5.3 `quests` Quest template definitions. These are authored content, not player data. ```sql CREATE TABLE IF NOT EXISTS quests ( id BIGSERIAL PRIMARY KEY, npc_id BIGINT NOT NULL REFERENCES npcs(id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', type TEXT NOT NULL CHECK (type IN ('kill_count', 'visit_town', 'collect_item')), target_count INT NOT NULL DEFAULT 1, target_enemy_type TEXT, target_town_id BIGINT REFERENCES towns(id), drop_chance DOUBLE PRECISION NOT NULL DEFAULT 0.3, min_level INT NOT NULL DEFAULT 1, max_level INT NOT NULL DEFAULT 100, reward_xp BIGINT NOT NULL DEFAULT 0, reward_gold BIGINT NOT NULL DEFAULT 0, reward_potions INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` ### 5.4 `hero_quests` Per-hero quest progress tracking. ```sql CREATE TABLE IF NOT EXISTS hero_quests ( id BIGSERIAL PRIMARY KEY, hero_id BIGINT NOT NULL REFERENCES heroes(id) ON DELETE CASCADE, quest_id BIGINT NOT NULL REFERENCES quests(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'accepted' CHECK (status IN ('accepted', 'completed', 'claimed')), progress INT NOT NULL DEFAULT 0, accepted_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ, claimed_at TIMESTAMPTZ, UNIQUE (hero_id, quest_id) ); ``` ### Indexes ```sql CREATE INDEX IF NOT EXISTS idx_npcs_town ON npcs(town_id); CREATE INDEX IF NOT EXISTS idx_quests_npc ON quests(npc_id); CREATE INDEX IF NOT EXISTS idx_hero_quests_hero ON hero_quests(hero_id); CREATE INDEX IF NOT EXISTS idx_hero_quests_status ON hero_quests(hero_id, status); ``` --- ## 6. API Endpoints All under `/api/v1/`. Auth via `X-Telegram-Init-Data` header (existing pattern). ### 6.1 Towns | Method | Path | Description | |--------|------|-------------| | `GET` | `/towns` | List all towns (id, name, biome, world_x, world_y, radius, level range) | | `GET` | `/towns/:id/npcs` | List NPCs in a town | ### 6.2 Quests | Method | Path | Description | |--------|------|-------------| | `GET` | `/npcs/:id/quests` | List available quests from an NPC (filtered by hero level, excluding already accepted/claimed) | | `POST` | `/quests/:id/accept` | Accept a quest (hero must be in the NPC's town, max 3 active) | | `POST` | `/quests/:id/claim` | Claim rewards for a completed quest | | `GET` | `/hero/quests` | List hero's active/completed quests with progress | | `POST` | `/hero/quests/:id/abandon` | Abandon an accepted quest | ### Response shapes **GET /towns** ```json { "towns": [ { "id": 1, "name": "Willowdale", "biome": "meadow", "worldX": 50, "worldY": 15, "radius": 8, "levelMin": 1, "levelMax": 5 } ] } ``` **GET /hero/quests** ```json { "quests": [ { "id": 1, "questId": 3, "title": "Slay 10 Forest Wolves", "description": "The wolves are terrorizing Thornwatch.", "type": "kill_count", "targetCount": 10, "progress": 7, "status": "accepted", "rewardXp": 100, "rewardGold": 50, "rewardPotions": 1, "npcName": "Guard Halric", "townName": "Thornwatch" } ] } ``` **POST /quests/:id/accept** - 200: Quest accepted, returns hero_quest record. - 400: Already at max active quests / not in town / level mismatch. - 409: Quest already accepted. **POST /quests/:id/claim** - 200: Rewards granted, returns updated hero stats. - 400: Quest not completed yet. --- ## 7. Frontend UI Needs ### 7.1 Town markers on the map - Render a flag/banner sprite at each town's world position, visible at all zoom levels. - Town name label appears when the hero is within 20 tiles. - Reuse existing `plaza` terrain for the town area; add a distinct border or glow. ### 7.2 NPC sprites - Small colored circles or simple character sprites at the NPC's position (town center + offset). - Quest giver: yellow `!` icon above head when they have an available quest. - Quest giver: yellow `?` icon when the hero has a completed quest to turn in. - Merchant: bag/potion icon. Healer: cross icon. - Tap an NPC sprite to open the interaction popup. ### 7.3 NPC interaction popup (React overlay) - Appears when hero is in town and player taps an NPC. - **Quest giver popup**: Lists available quests with title, short description, rewards, and "Accept" button. Shows in-progress quests with progress bar and "Claim" button if complete. - **Merchant popup**: List of purchasable items (potions) with gold cost and "Buy" button. - **Healer popup**: "Heal to full" button with gold cost shown. ### 7.4 Quest log panel - Accessible via a small scroll icon in the HUD (always visible, bottom-right area). - Shows up to 3 active quests in a compact list. - Each entry: quest title, progress bar (`7/10`), quest type icon. - Tap a quest entry to expand: full description, rewards, abandon button. - Toast notification when a quest completes ("Quest Complete: Slay 10 Forest Wolves"). ### 7.5 Quest progress toast - Lightweight toast at top of screen: "Wolf slain (7/10)" on kill_count progress. - Only show every 1-2 increments to avoid spam (show on first kill, then every 3rd, then on completion). --- ## 8. Hero Travel Flow -- Town Integration with Auto-Walk ### Current flow ``` Hero auto-walks along road -> encounters enemies -> fights -> continues walking ``` ### Updated flow with towns ``` Hero auto-walks -> enters town radius -> combat paused, NPCs visible -> (player can interact or do nothing) -> hero continues walking -> exits town radius -> combat resumes -> next enemy encounter ``` ### Key behaviors 1. **Town entry**: When the game tick detects `distance(hero, town_center) <= town.radius`, the backend sets a transient `in_town` flag (not persisted, computed from position). The frontend receives the town ID via the state push. 2. **In-town state**: While in town: - No enemy spawns within the town radius. - Hero continues walking at normal speed (idle game -- no forced stops). - NPC sprites become tappable. - The quest log auto-checks `visit_town` quest completion. 3. **Town exit**: When the hero walks past the town radius, NPCs disappear from the tappable area, combat spawning resumes. 4. **Auto-interaction**: If the hero has a completable quest for a quest giver in this town, show a brief highlight/pulse on the NPC. The player must still tap to claim (keeps engagement without blocking idle flow). 5. **Quest progress tracking** (backend, per game tick): - On enemy kill: check active `kill_count` and `collect_item` quests. Update `hero_quests.progress`. - On town entry: check active `visit_town` quests. Update progress if target matches. - On `progress >= target_count`: set `status = 'completed'`, `completed_at = now()`. Push WebSocket event. 6. **Offline simulation**: When processing offline ticks, quest progress increments normally. Kill-based quests advance with simulated kills. Visit-town quests advance if the hero's simulated path crosses a town. Quest completions are batched and shown on reconnect. ### WebSocket events (additions to existing protocol) | Event | Direction | Payload | |-------|-----------|---------| | `quest_progress` | server -> client | `{ questId, progress, targetCount }` | | `quest_completed` | server -> client | `{ questId, title }` | | `town_entered` | server -> client | `{ townId, townName }` | | `town_exited` | server -> client | `{ townId }` | --- ## 9. Seed Data The migration includes seed data for all 7 towns, ~15 NPCs, and ~20 starter quests. See `000006_quest_system.sql` for the full seed. --- ## 10. Future Considerations (Post-MVP) These are explicitly out of scope for MVP but noted for schema forward-compatibility: - **Repeatable quests**: Add a `is_repeatable` flag and `cooldown_hours` to `quests`. Not in MVP. - **Quest chains**: Add `prerequisite_quest_id` to `quests`. Not in MVP. - **Dialogue**: Add `dialogue_text` JSON array to `quests` for NPC speech bubbles. Not in MVP. - **Merchant inventory**: Separate `merchant_inventory` table. MVP uses hardcoded potion prices. - **Healer scaling**: MVP uses flat gold cost. Later: cost scales with level.