14 KiB
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 thetownstable. - 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_yin 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_countenemies of a specified type (or any type iftarget_enemy_typeis NULL). - Tracking: Increment
hero_quests.progresseach 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 = 1when 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_countof 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.
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.
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.
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.
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
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
{
"towns": [
{
"id": 1,
"name": "Willowdale",
"biome": "meadow",
"worldX": 50,
"worldY": 15,
"radius": 8,
"levelMin": 1,
"levelMax": 5
}
]
}
GET /hero/quests
{
"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
plazaterrain 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
-
Town entry: When the game tick detects
distance(hero, town_center) <= town.radius, the backend sets a transientin_townflag (not persisted, computed from position). The frontend receives the town ID via the state push. -
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_townquest completion.
-
Town exit: When the hero walks past the town radius, NPCs disappear from the tappable area, combat spawning resumes.
-
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).
-
Quest progress tracking (backend, per game tick):
- On enemy kill: check active
kill_countandcollect_itemquests. Updatehero_quests.progress. - On town entry: check active
visit_townquests. Update progress if target matches. - On
progress >= target_count: setstatus = 'completed',completed_at = now(). Push WebSocket event.
- On enemy kill: check active
-
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_repeatableflag andcooldown_hourstoquests. Not in MVP. - Quest chains: Add
prerequisite_quest_idtoquests. Not in MVP. - Dialogue: Add
dialogue_textJSON array toquestsfor NPC speech bubbles. Not in MVP. - Merchant inventory: Separate
merchant_inventorytable. MVP uses hardcoded potion prices. - Healer scaling: MVP uses flat gold cost. Later: cost scales with level.