Guides

Questing & Storylines with Quest scripting engines

Implementing branching quests in MUDs requires managing player state through text interactions. This guide covers building a state-driven fetch quest with dialogue trees using standard MUD trigger systems, applicable to CircleMUD, ROM, and similar C-based codebases.

3-4 hours6 steps
Pixel art quest scroll icon
1

Define quest state storage in player data

Add quest state tracking to your player structure or use an external quest log system. For a simple fetch quest, track quest_started, has_item, and quest_completed states. In C-based MUDs, add these to struct player_data or use a bitvector flag system to ensure persistence across reboots.

structs.h
/* In structs.h */
#define QUEST_OLD_MAN_HEALING  1

struct player_data {
  /* existing fields */
  int quest_flags[32]; /* Bitvector for active quests */
  int quest_state[32]; /* 0=inactive, 1=active, 2=complete */
};

⚠ Common Pitfalls

  • Do not use global variables for quest state
  • Ensure state persists through player logout
2

Create the quest giver NPC trigger procedure

Implement a special procedure (spec_proc) or mob program that activates when a player speaks specific keywords to the NPC. Parse the player's input for 'quest' or 'help' and check current quest state before responding to prevent duplicate quest acceptance.

mob_progs.c
SPECIAL(quest_giver_old_man) {
  char buf[MAX_INPUT_LENGTH];
  
  if (CMD_IS("say") || CMD_IS("'")) {
    skip_spaces(&argument);
    strcpy(buf, argument);
    
    if (!str_cmp(buf, "quest")) {
      if (GET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING) == 0) {
        act("The old man says, 'Please bring me a healing potion.'", 
            FALSE, mob, 0, ch, TO_VICT);
        SET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING, 1);
        return TRUE;
      }
    }
  }
  return FALSE;
}

⚠ Common Pitfalls

  • Avoid hardcoding player names in trigger logic
  • Check for NULL character pointers before string comparison
3

Build branching dialogue state machine

Structure the NPC's responses using a switch statement or if-else chain based on quest_state. State 0 shows initial greeting, State 1 shows reminder text for active quests, State 2 handles item acceptance dialogue, and State 3 prevents re-completion.

quest_procs.c
void handle_quest_dialogue(struct char_data *ch, struct char_data *npc) {
  int state = GET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING);
  
  switch(state) {
    case 0:
      send_to_char(ch, "The old man looks sick and pale.\r\n");
      break;
    case 1:
      send_to_char(ch, "The old man asks, 'Did you bring the potion?'\r\n");
      break;
    case 2:
      send_to_char(ch, "The old man smiles weakly.\r\n");
      break;
    case 3:
      send_to_char(ch, "The old man ignores you, looking healthier now.\r\n");
      break;
  }
}

⚠ Common Pitfalls

  • Missing state transitions can lock players out of completion
  • Text output must respect 80-character line limits for standard MUD clients
4

Implement quest item handling and validation

Create an object with a special procedure that triggers when given to the NPC via the 'give' command. Validate that the giver is the quest holder and state equals 1 (accepted). Atomically remove the item from inventory and update state to 2 before triggering completion.

obj_procs.c
SPECIAL(healing_potion_quest) {
  char arg1[MAX_INPUT_LENGTH], arg2[MAX_INPUT_LENGTH];
  struct obj_data *obj;
  
  if (CMD_IS("give")) {
    two_arguments(argument, arg1, arg2);
    
    if (!(obj = get_obj_in_list_vis(ch, arg1, ch->carrying)))
      return FALSE;
      
    if (isname("potion", arg1) && isname("old man", arg2)) {
      if (GET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING) == 1) {
        obj_from_char(obj);
        extract_obj(obj);
        SET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING, 2);
        act("You give $p to the old man.", FALSE, ch, obj, 0, TO_CHAR);
        complete_quest(ch); /* Call reward function */
        return TRUE;
      }
    }
  }
  return FALSE;
}

⚠ Common Pitfalls

  • Players may attempt to exploit by giving item multiple times if state check fails
  • Ensure item is removed from inventory before giving reward to prevent duplication bugs
5

Add completion validation and reward distribution

Finalize the quest by setting completion flags and delivering rewards. Use atomic operations: verify state equals 2, set state to 3 (completed), then distribute experience points or items. This prevents race conditions in multi-threaded environments and stops double-dipping.

quest_rewards.c
void complete_quest(struct char_data *ch) {
  if (GET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING) != 2) 
    return;
  
  /* Atomic: Check, Set, Reward */
  SET_QUEST_STATE(ch, QUEST_OLD_MAN_HEALING, 3);
  GET_EXP(ch) += 500;
  
  send_to_char(ch, "Quest complete! You gain 500 experience.\r\n");
  act("$n looks more experienced.", TRUE, ch, 0, 0, TO_ROOM);
}

⚠ Common Pitfalls

  • Race conditions in multi-threaded MUDs can cause double rewards
  • Always validate player level or prerequisites before final reward distribution
6

Test state transitions and edge cases

Walk through every state permutation using a test character: accepting quest, logging out mid-quest, attempting to complete without item, trying to restart after completion, and verifying database persistence across copyover or reboot. Check that quest logs display correctly to players.

⚠ Common Pitfalls

  • Incomplete testing leads to stuck quest states in production
  • Player file corruption can reset quest progress causing duplicate completions

What you built

State-driven quest systems require rigorous validation at each transition point. Deploy changes to a test port first, verify persistence across reboots by checking player file saves, and monitor for player reports of stuck states before promoting to production. Consider implementing a 'quest reset' admin command for emergency fixes.