data schema

Guide for data schema

Quest Data Schema

quests.json Structure

The quest data file uses a HashMap structure where keys are quest IDs and values are quest definitions.

Root Structure

{
  "quest_id_1": { /* QuestDefinition */ },
  "quest_id_2": { /* QuestDefinition */ },
  ...
}

QuestDefinition Schema

interface QuestDefinition {
  id: string;                    // Quest identifier (matches key)
  name: string;                  // Display name
  description: string;           // Quest description
  category?: string;             // e.g., "tutorial", "main", "side"
  difficulty?: string;           // e.g., "novice", "intermediate", "master"
  start_npc?: string;           // NPC ID who gives the quest
  rewards?: QuestRewards;       // Completion rewards
  steps: { [stepId: string]: QuestStep };  // Step definitions
}

interface QuestRewards {
  xp?: number;                  // Experience points
  items?: ItemReward[];         // Item rewards
}

interface ItemReward {
  item_id: string;              // Item identifier
  quantity: number;             // Amount to grant
}

interface QuestStep {
  description: string;          // Step objective description
  type?: string;                // "dialogue", "interaction", "kill"
  target_npc?: string;          // For dialogue steps
  target_object?: string;       // For interaction steps
  target_creature?: string;     // For kill steps
  required_count?: number;      // For kill steps (how many to kill)
  next_step?: number;           // Next step ID (null for terminal)
  finish?: boolean;             // True if this completes the quest
}

Step Types

1. Dialogue Steps

Completed when player talks to specified NPC.
{
  "0": {
    "description": "Speak to Guard Captain at the training grounds.",
    "type": "dialogue",
    "target_npc": "npc_custom_1",
    "next_step": 10
  }
}
Listener: track_quest_dialogue in quest_listeners.rs
  • Listens to NPCInteractionEvent
  • Matches event.npc_id with step.target_npc

2. Interaction Steps

Completed when player interacts with object/entity.
{
  "10": {
    "description": "Search the Ancient Bookshelf for the scroll.",
    "type": "interaction",
    "target_object": "obj_bookshelf_ancient",
    "next_step": 20
  }
}
Listener: track_quest_dialogue (reused)
  • Uses NPC component on objects with npc_id matching target_object
  • Triggered by NPCInteractionEvent

3. Kill Steps

Completed when player defeats specified creatures.
{
  "10": {
    "description": "Defeat 5 goblins in the forest.",
    "type": "kill",
    "target_creature": "goblin",
    "required_count": 5,
    "next_step": 20
  }
}
Listener: track_quest_kills in quest_listeners.rs
  • Listens to EnemyDefeatedEvent
  • Increments kill count (tracked separately)
  • Advances when count reaches required_count

Step Progression

Linear Progression

{
  "steps": {
    "0": { "next_step": 10 },
    "10": { "next_step": 20 },
    "20": { "finish": true }
  }
}

Terminal Steps

Two ways to mark a step as terminal:
  1. Using finish flag:
{
  "20": {
    "description": "Return to quest giver.",
    "type": "dialogue",
    "target_npc": "npc_id",
    "finish": true
  }
}
  1. Using null next_step:
{
  "20": {
    "description": "Return to quest giver.",
    "type": "dialogue",
    "target_npc": "npc_id",
    "next_step": null
  }
}

Step ID Conventions

Use increments of 10 for step IDs to allow insertion of intermediate steps:
{
  "steps": {
    "0": { "next_step": 10 },
    "10": { "next_step": 20 },
    "20": { "next_step": 30 },
    "30": { "finish": true }
  }
}
If you need to add a step between 10 and 20 later:
{
  "steps": {
    "0": { "next_step": 10 },
    "10": { "next_step": 15 },   // Modified
    "15": { "next_step": 20 },   // New step inserted
    "20": { "next_step": 30 },
    "30": { "finish": true }
  }
}

Alternative: Semantic IDs

{
  "steps": {
    "intro": { "next_step": "gather_items" },
    "gather_items": { "next_step": "return" },
    "return": { "finish": true }
  }
}
Note: Current implementation expects numeric IDs for stage_id conversion in Plugin API.

Complete Example

{
  "the_lost_scroll": {
    "id": "the_lost_scroll",
    "name": "The Lost Scroll",
    "category": "tutorial",
    "difficulty": "novice",
    "description": "Scholar Vyasa seeks an ancient scroll lost in the palace library.",
    "start_npc": "npc_vyasa",
    "rewards": {
      "xp": 100,
      "items": [
        {
          "item_id": "ancient_scroll",
          "quantity": 1
        },
        {
          "item_id": "coins",
          "quantity": 50
        }
      ]
    },
    "steps": {
      "0": {
        "description": "Speak to Scholar Vyasa at the Palace.",
        "type": "dialogue",
        "target_npc": "npc_vyasa",
        "next_step": 10
      },
      "10": {
        "description": "Search the Ancient Bookshelf for the scroll.",
        "type": "interaction",
        "target_object": "obj_bookshelf_ancient",
        "next_step": 20
      },
      "20": {
        "description": "Return the scroll to Scholar Vyasa.",
        "type": "dialogue",
        "target_npc": "npc_vyasa",
        "finish": true
      }
    }
  }
}

NPC Integration

Quest NPCs must be defined in npcs.json:
{
  "npc_vyasa": {
    "id": "npc_vyasa",
    "name": "Scholar Vyasa",
    "type": "npc",
    "examine": "A wise scholar studying ancient texts.",
    "left_click_action": "talk",
    "context_menu_options": ["Talk-to", "Examine"],
    "model_id": "npcs/scholar"
  }
}
And spawned in world with appropriate components:
commands.spawn((
    Name::new("Scholar Vyasa"),
    NPC {
        npc_id: "npc_vyasa".to_string(),
        dialogue_id: "dialogue_vyasa_intro".to_string(),
    },
    QuestTarget {
        target_name: "npc_vyasa".to_string(),
    },
    // ... other components
));

Validation Rules

  1. Step IDs must be unique within a quest
  2. next_step must reference existing step or be null
  3. At least one terminal step (finish: true or next_step: null)
  4. No circular references in step progression
  5. target_npc/target_object/target_creature must exist in respective data files
  6. Step type should match target field (dialogue→target_npc, kill→target_creature)

Migration from Old Schema

Old Format (Objective-based)

{
  "quests": [
    {
      "id": "quest_1",
      "objectives": [
        {"type": "talk", "target": "npc_1", "count": 1},
        {"type": "kill", "target": "goblin", "count": 5}
      ]
    }
  ]
}

New Format (Step-based)

{
  "quest_1": {
    "id": "quest_1",
    "steps": {
      "0": {
        "description": "Talk to NPC",
        "type": "dialogue",
        "target_npc": "npc_1",
        "next_step": 10
      },
      "10": {
        "description": "Defeat 5 goblins",
        "type": "kill",
        "target_creature": "goblin",
        "required_count": 5,
        "finish": true
      }
    }
  }
}

Best Practices

  1. Use descriptive step descriptions - Players see these in the HUD
  2. Increment step IDs by 10 - Allows future insertions
  3. Always specify step type - Helps with debugging and validation
  4. Include quest category - Useful for filtering/organization
  5. Test quest flow - Ensure all steps are reachable and complete properly
  6. Document complex quests - Add comments in separate design docs