You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

385 lines
14 KiB
Markdown

# 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.