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.

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

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