Skip to main content

Overview

Hyperscape features an OSRS-style quest system with multi-stage quests, progress tracking, and rewards. Quests are defined in JSON manifests and tracked server-side with database persistence.
Quest definitions are stored in packages/server/world/assets/manifests/quests.json.

Quest Structure

Quests are defined with the following structure:
interface QuestDefinition {
  id: string;                    // Unique quest identifier
  name: string;                  // Display name
  description: string;           // Quest description
  difficulty: "novice" | "intermediate" | "experienced" | "master";
  questPoints: number;           // Quest points awarded on completion
  replayable: boolean;           // Can be repeated
  
  requirements: {
    quests: string[];            // Required completed quests
    skills: Record<string, number>; // Required skill levels
    items: string[];             // Required items
  };
  
  startNpc: string;              // NPC that starts the quest
  
  stages: QuestStage[];          // Quest stages
  
  onStart?: {
    items: Array<{ itemId: string; quantity: number }>;
  };
  
  rewards: {
    questPoints: number;
    items: Array<{ itemId: string; quantity: number }>;
    xp: Record<string, number>; // Skill XP rewards
  };
}

Quest Stages

Quests consist of multiple stages that must be completed in order:

Stage Types

TypeDescriptionExample
dialogueTalk to an NPC”Talk to the Cook”
killKill specific mobs”Kill 15 goblins”
gatherGather resources”Collect 10 copper ore”
interactInteract with objects”Light 5 fires”
craftCraft items”Smith a bronze sword”

Stage Definition

interface QuestStage {
  id: string;                    // Unique stage identifier
  type: "dialogue" | "kill" | "gather" | "interact" | "craft";
  description: string;           // Stage description shown to player
  target?: string;               // Target entity/item (for kill/gather/interact)
  count?: number;                // Required count (for kill/gather/interact)
}

Quest Status

Quests have four possible statuses:
StatusDescription
not_startedQuest not yet started
in_progressQuest active, objectives incomplete
ready_to_completeAll objectives met, return to quest NPC
completedQuest finished, rewards claimed
ready_to_complete is a derived status computed when status === "in_progress" AND the current stage objective is met.

Progress Tracking

Quest progress is tracked per-player in the database:

Database Schema

CREATE TABLE quest_progress (
  id SERIAL PRIMARY KEY,
  playerId TEXT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
  questId TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'not_started',
  currentStage TEXT,
  stageProgress JSONB DEFAULT '{}',
  startedAt BIGINT,
  completedAt BIGINT,
  UNIQUE(playerId, questId)
);

Stage Progress Format

Progress is stored as JSON with stage-specific counters:
{
  "kills": 7,              // For kill stages
  "copper_ore": 5,         // For gather stages (by item ID)
  "tin_ore": 3,
  "fires_lit": 2           // For interact stages
}

Quest Flow

1. Quest Request

Player talks to quest NPC → Server emits QUEST_START_CONFIRM event:
world.emit(EventType.QUEST_START_CONFIRM, {
  playerId,
  questId,
  questName,
  description,
  difficulty,
  requirements,
  rewards,
});
Client shows quest accept screen with requirements and rewards.

2. Quest Start

Player accepts → Client sends questAccept packet → Server starts quest:
await questSystem.startQuest(playerId, questId);
Actions:
  • Creates quest_progress row with status in_progress
  • Sets currentStage to first non-dialogue stage
  • Grants onStart items if defined
  • Emits QUEST_STARTED event
  • Logs to audit trail

3. Progress Tracking

Quest system subscribes to game events:
this.subscribe(EventType.NPC_DIED, (data) => this.handleNPCDied(data));
this.subscribe(EventType.INVENTORY_ITEM_ADDED, (data) => this.handleGatherStage(data));
this.subscribe(EventType.FIRE_CREATED, (data) => this.handleInteractStage(data));
this.subscribe(EventType.COOKING_COMPLETED, (data) => this.handleInteractStage(data));
this.subscribe(EventType.SMITHING_COMPLETE, (data) => this.handleInteractStage(data));
On progress:
  • Updates stageProgress JSON
  • Emits QUEST_PROGRESSED event
  • Sends chat message when objective complete
  • Saves to database

4. Quest Completion

When all stages complete, player returns to quest NPC:
await questSystem.completeQuest(playerId, questId);
Actions:
  • Marks quest as completed with timestamp
  • Awards quest points (atomic transaction)
  • Grants reward items
  • Grants skill XP
  • Emits QUEST_COMPLETED event
  • Shows completion screen
  • Logs to audit trail

Security Features

HMAC Kill Token Validation

Prevents spoofed NPC_DIED events from granting quest progress:
// Server generates token when mob dies
const killToken = generateKillToken(mobId, killedBy, timestamp);

// Quest system validates token
if (!validateKillToken(mobId, killedBy, timestamp, killToken)) {
  logger.warn("Invalid kill token - possible spoof attempt");
  return; // Reject spoofed progress
}
Implementation:
  • Uses HMAC-SHA256 for cryptographic validation
  • Tokens include: mobId, killedBy, timestamp
  • Validates timestamp within 5-second window
  • Server-only (uses Node.js crypto module)

Quest Audit Logging

All quest state changes are logged for security auditing:
CREATE TABLE quest_audit_log (
  id SERIAL PRIMARY KEY,
  playerId TEXT NOT NULL,
  questId TEXT NOT NULL,
  action TEXT NOT NULL,           -- "started", "progressed", "completed"
  questPointsAwarded INTEGER,
  stageId TEXT,
  stageProgress JSONB,
  timestamp BIGINT NOT NULL,
  metadata JSONB
);
Use cases:
  • Fraud detection and investigation
  • Debugging quest progression bugs
  • Analytics data for game design
  • Customer support inquiries

Rate Limiting

Quest network handlers are rate-limited:
getQuestListRateLimiter()    // 5 requests/sec
getQuestDetailRateLimiter()  // 10 requests/sec  
getQuestAcceptRateLimiter()  // 3 requests/sec

Quest Journal UI

Players access quests via the Quest Journal (📜 icon in sidebar):

Features

  • Color-coded status: Red (not started), Yellow (in progress), Green (completed)
  • Quest points tracking: Total quest points displayed
  • Progress visualization: Strikethrough for completed steps
  • Dynamic counters: Shows progress like “Kill goblins (7/15)”
  • Quest details: Requirements, rewards, and stage descriptions

Quest Screens

Quest Start Screen:
  • Shows quest name, description, difficulty
  • Lists requirements (quests, skills, items)
  • Displays rewards (quest points, items, XP)
  • Accept/Decline buttons
Quest Complete Screen:
  • Congratulations message
  • Quest name
  • Rewards summary
  • Parchment/scroll aesthetic
  • Click anywhere to dismiss

Example Quest Definition

{
  "goblin_slayer": {
    "id": "goblin_slayer",
    "name": "Goblin Slayer",
    "description": "Kill 15 goblins to prove your worth.",
    "difficulty": "novice",
    "questPoints": 1,
    "replayable": false,
    "requirements": {
      "quests": [],
      "skills": {},
      "items": []
    },
    "startNpc": "cook",
    "stages": [
      {
        "id": "talk_to_cook",
        "type": "dialogue",
        "description": "Talk to the Cook to start the quest."
      },
      {
        "id": "kill_goblins",
        "type": "kill",
        "description": "Kill 15 goblins.",
        "target": "goblin",
        "count": 15
      },
      {
        "id": "return_to_cook",
        "type": "dialogue",
        "description": "Return to the Cook."
      }
    ],
    "onStart": {
      "items": [
        { "itemId": "bronze_sword", "quantity": 1 }
      ]
    },
    "rewards": {
      "questPoints": 1,
      "items": [
        { "itemId": "xp_lamp_1000", "quantity": 1 }
      ],
      "xp": {
        "attack": 500,
        "strength": 500
      }
    }
  }
}

API Reference

QuestSystem Methods

class QuestSystem extends SystemBase {
  // Query methods
  getQuestStatus(playerId: string, questId: string): QuestStatus;
  getQuestDefinition(questId: string): QuestDefinition | undefined;
  getAllQuestDefinitions(): QuestDefinition[];
  getActiveQuests(playerId: string): ActiveQuest[];
  getQuestPoints(playerId: string): number;
  hasCompletedQuest(playerId: string, questId: string): boolean;
  
  // Action methods
  requestQuestStart(playerId: string, questId: string): boolean;
  async startQuest(playerId: string, questId: string): Promise<boolean>;
  async completeQuest(playerId: string, questId: string): Promise<boolean>;
}

Network Packets

Client → Server:
  • getQuestList - Request all quests for player
  • getQuestDetail - Request specific quest details
  • questAccept - Accept a quest
Server → Client:
  • questList - Quest list with status
  • questDetail - Detailed quest information
  • questStartConfirm - Quest accept confirmation screen
  • questProgressed - Progress update
  • questCompleted - Quest completion screen

Performance Optimizations

O(1) Stage Lookups

Stage lookups use pre-allocated Map caches instead of O(n) find() calls:
// Pre-allocated stage lookup caches (per quest)
private _stageCaches: Map<string, Map<string, QuestStage>> = new Map();

// Build cache on first access
private getStageCache(questId: string): Map<string, QuestStage> {
  let cache = this._stageCaches.get(questId);
  if (!cache) {
    const definition = this.questDefinitions.get(questId);
    cache = new Map(definition.stages.map(s => [s.id, s]));
    this._stageCaches.set(questId, cache);
  }
  return cache;
}

Object Spread Elimination

Direct mutation in hot paths eliminates object allocations:
// Direct mutation (safe - we own this object)
progress.stageProgress[stage.target] = currentKills;

Log Verbosity Reduction

Debug-level logging for frequent events, info-level only for milestones:
this.logger.debug(`NPC_DIED: killedBy=${killedBy}, mobType=${mobType}`);
this.logger.info(`Quest completed: ${questName}`);