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