Modding:Quests

From Caves of Qud Wiki
Jump to navigation Jump to search
This page is about modding. See the modding overview for an abstract on modding.
This page is about modding. See the modding overview for an abstract on modding.

This page will step you through writing your own questline.

Quests.xml

The game defines quests in the Quests.xml file. Each quest is primarily broken down into the following:

  • A top-level <quest> tag that defines high-level properties of the quest, including the faction that assigned it (if any), journal accomplishments that should be added upon completing the quest, and so on, and
  • <step> nodes that define individual steps of the quest.

For example, here is the quest definition for O Glorious Shekhinah!:

<quest
  Name="O Glorious Shekhinah!"
  Level="3"
  System="TravelToStiltSystem" 
  Accomplishment="On the recommendation of a proselyte, you visited the merchant bazaar and grand cathedral at the Six Day Stilt."
  Hagiograph="=name= trekked through the salt pans, north and west, to the merchant bazaar and grand cathedral of the Six Day Stilt. There, the stiltfolk sang hymns in the sultan's honor."
  HagiographCategory="VisitsLocation">

  <step Name="Make a Pilgrimage to the Six Day Stilt" XP="1500">
    <text>Journey through the Great Salt Desert to visit the merchant bazaar and Mechanimist cathedral, where a proselyte asked you to make on offering of a trinket.</text>
  </step>
</quest>

Once a quest has been defined in Quests.xml it can be obtained in-game. You can use the startquest:[quest name] wish to artificially grant you the quest for testing purposes.

IQuestSystem

A common component for many (but not all) quests is custom, per-quest systems that advance a player's progression through the quest as they achieve various objectives. An IQuestSystem can be used to subscribe to many different events on the player, such as when they enter a zone or pick up an item, which can be useful for tracking progress through quest objects. By default, IQuestSystems are removed from the game once their attached quest is completed.

Continuing our example from earlier, here's the system used by O Glorious Shekhinah!, TravelToStiltSystem:

using System;

namespace XRL.World.Quests;

[Serializable]
public class TravelToStiltSystem : IQuestSystem
{
    public override void Register(XRLGame Game, IEventRegistrar Registrar)
    {
        Registrar.Register(ZoneActivatedEvent.ID);
    }

    public override bool HandleEvent(ZoneActivatedEvent E)
    {
        if (E.Zone.ZoneID == "JoppaWorld.5.2.1.1.10" || E.Zone.ZoneID == "JoppaWorld.5.2.1.2.10")
        {
            The.Game.FinishQuestStep("O Glorious Shekhinah!", "Make a Pilgrimage to the Six Day Stilt", -1, CanFinishQuest: true, E.Zone.ZoneID);
        }
        return base.HandleEvent(E);
    }

    // GetInfluencer is primarily used to determine who should be referenced when
    // naming an item. If the player successfully rolls an item naming opportunity
    // upon completing the quest, they will be able to name their item after the
    // culture of either Wardens Esther or Tszappur.
    public override GameObject GetInfluencer()
    {
        if (50.in100())
        {
            return GameObject.FindByBlueprint("Wardens Esther");
        }
        return GameObject.FindByBlueprint("Tszappur");
    }
}

The main thing that this quest system does is register an event handler to check for ZoneActivatedEvent and determine whether the player has arrived at the Six Day Stilt. If they have, then the quest is completed.

IQuestSystem versus QuestManager

QuestManager is the old interface that used to manage the progression of quests, and while code based on it is still present in Caves of Qud, it is no longer used. Most of the use cases for QuestManager can now be handled better through IQuestSystem.

Quest progression

In this section we will discuss various XML-only and scripting-enabled ways of progressing players through different stages of a quest.

Giving quests to players

XML-only

The StartQuest part generator for conversations can grant a player a quest. Here is an example of StartQuest usage from Argyve's dialogue, starting the quest A Canticle for Barathrum.

<node ID="CanticleAccept3">
  <text>                                    
    Here you are. Now, go! Off with you! May you live long enough to do my bidding. Away, away!
  </text>
  <choice GotoID="End" StartQuest="A Canticle for Barathrum">
    <text>Farewell, Argyve.</text>
    <part Name="ReceiveItem" Blueprints="Droid Scrambler,Argyve's Data Disk" Identify="All" />
  </choice>
</node>

Via scripting

With scripting it is possible to start the quest using The.Game.StartQuest, e.g.

The.Game.StartQuest("A Canticle for Barathrum");

Completing steps of a quest

XML-only

There are several XML-only ways to trigger the completion of a quest step:

  • In a conversation, you can use the CompleteQuestStep part generator.
  • You can attach the QuestStepFinisher part to a widget that is placed in a zone to ensure that a quest step is finished upon entering the zone.
  • You can use the FinishQuestStepWhenSlain part to complete a quest step when a creature is killed.

Via scripting

To complete a quest step from C#, you can call The.Game.FinishQuestStep. This can be called from anywhere during the game; for example, here's a snippet from the FinishQuestStepWhenSlain part (used to -- as the name suggests -- complete a quest step after slaying a creature):

using System;
namespace XRL.World.Parts;

[Serializable]
public class FinishQuestStepWhenSlain : IPart
{
    public string Quest;
    public string Step;
    public string GameState;
    public virtual bool Clean => true;

    // ...

    public virtual void Trigger()
    {
        if (GameState != null)
        {
            The.Game.SetIntGameState(GameState, 1);
        }
        if (!The.Game.TryGetQuest(this.Quest, out var Quest))
        {
            if (!RequireQuest)
            {
                return;
            }
            Quest = The.Game.StartQuest(this.Quest);
        }
        The.Game.FinishQuestStep(Quest, Step);
        if (Clean)
        {
            ParentObject.RemovePart(this);
        }
    }
}

If you wish to signal the failure of a game step, you can use The.Game.FailQuestStep(QuestName, QuestStep) instead.

Completing a quest

A quest will automatically be marked as completed when you have completed all of its steps. However, in some cases you may want to manually trigger the completion of a quest, e.g. if the player (for whatever reason) completed the quest without finishing all of its objectives. This methodology is more robust when finishing the last step of a quest should amount to completing the quest, since there are many ways for players to skip quest steps in Caves of Qud.

XML-only

To manually complete a quest from a conversation, you can use FinishQuest. Here's the conversation node from Otho's script that completes Decoding the Signal:

<node ID="PresentTheDisk">
  <text>
    Well done, =factionaddress:Barathrumites=. Present the disk.
  </text>
  <choice GotoID="InterpretSignal" CompleteQuestStep="Decoding the Signal~Return to Grit Gate|6000" FinishQuest="Decoding the Signal">
    <text>[Give Otho the disk]</text>
    <part Name="GritGateHandler" Rank="Journeyfriend" />
  </choice>
</node>

Via scripting

From C#, you can call The.Game.FinishQuest(QuestName). There also exists a method for failing quests, as there does for failing steps; this method is The.Game.FailQuest(QuestName).

Example: a fetch quest for Nima Ruda

To demonstrate the ideas from this article, we will create a fetch quest for Nima Ruda. The pretext will be that Nima Ruda is seeking starapple jam to help nurse the people of Joppa, and she needs you to fetch her another batch.

To create this quest, we will start by constructing a Quests.xml defining the steps and properties of the quest:

<?xml version="1.0" encoding="utf-8" ?>
<quests>
  <quest Name="Healing Balms for Nima Ruda" Factions="Joppa" Level="1"
    Reputation="25" Accomplishment="You assisted Nima Ruda in her apothecarial duties to the people of Joppa"
    Hagiograph="In the thickets of the salt marsh, =name= gave aid to the sick and wounded."
    HagiographCategory="DoesSomethingRad">

    <step Name="Get a starapple">
      <text>Fetch a starapple from Nima Ruda's starapple tree.</text>
    </step>

    <step Name="Create starapple jam">
      <text>Turn a starapple into starapple jam at a campfire.</text>
    </step>

    <step Name="Deliver the jam">
      <text>Deliver the starapple jam to Nima Ruda.</text>
    </step>
  </quest>
</quests>

You can run the wish startquest:Healing Balms for Nima Ruda to see this quest in your journal, although it won't have a quest giver yet.

Now we need to define some custom conversational nodes for Nima Ruda to ensure that she gives the quest if the player asks her for work, and finishes the quest when the player gives her starapple jam. I've written a bare-bones conversation for her below:

<?xml version="1.0" encoding="utf-8" ?>
<conversations>
  <conversation ID="Nima Ruda">
    <node ID="Start">
      <choice Target="NeedHelp" IfNotHaveQuest="Healing Balms for Nima Ruda">
        Are you in need of an assistant?
      </choice>
      <choice Priority="1" Target="GiveJam" IfHaveActiveQuest="Healing Balms for Nima Ruda" IfHaveItemDescendsFrom="Starapple Preserves">
        I have some starapple jam for you.
      </choice>
    </node>

    <node ID="NeedHelp">
      <text>
        As a matter of fact, I am. Our stocks of starapple jam are running desperately low; without it, I will be unable to nurse the ill. What say you, traveler? Would you be willing to help me retrieve a new supply of starapple jam for our little hamlet?   
      </text>
      <choice Target="End" StartQuest="Healing Balms for Nima Ruda">Yes, I will help.</choice>
      <choice Target="Start">Not right now, sorry.</choice>
    </node>

    <node ID="GiveJam">
      <text>
        Oh, wonderful! Thank you kindly =name=, your efforts will not go to waste.
      </text>
      <choice AwardXP="500" TakeItem="Starapple Preserves" Target="End" FinishQuest="Healing Balms for Nima Ruda">
        Happy to be of service. Live and drink.
      </choice>
    </node>
  </conversation>
</conversations>

Observe that we use FinishQuest rather than CompleteQuestStep when giving the starapple jam to Nima Ruda. This is become some steps of the quest may never have been completed at that point in time. For instance, the player may never have acquired a starapple if they bought the jam from a merchant.

So far we have a quest that can be completed, at least in principle, and which hasn't required any scripting. If the only step in the quest was to deliver the jam to Nima Ruda then we could stop here.

However, to track the objectives related to finding a starapple and making starapple jam, we will need the help of C#. To keep track of these objectives we will create a custom IQuestSystem.

First, modify your Quests.xml so that you specify a System attribute for your quest:

<quest Name="Healing Balms for Nima Ruda" Factions="Joppa" Level="1"
  System="HealingBalmsSystem"
  Reputation="25" Accomplishment="You assisted Nima Ruda in her apothecarial duties to the people of Joppa"
  Hagiograph="In the thickets of the salt marsh, =name= gave aid to the sick and wounded."
  HagiographCategory="DoesSomethingRad">
  <!-- Leave everything else as-is -->
  <!-- ... -->
</quest>

We now create a new IQuestSystem that checks whether the player has acquired a starapple or some starapple jam:

using System;

namespace XRL.World.Quests {
    [Serializable]
    public class HealingBalmsSystem : IQuestSystem {
        public virtual string AppleStepID => "Get a starapple";
        public virtual string JamStepID => "Create starapple jam";

        public override void RegisterPlayer(GameObject Player, IEventRegistrar Registrar) {
            Registrar.Register(TookEvent.ID);
        }

        public override bool HandleEvent(TookEvent E) {
            if (E.Item.Blueprint == "Starapple")
                The.Game.FinishQuestStep(QuestID, AppleStepID);
            if (E.Item.Blueprint == "Starapple Preserves")
                The.Game.FinishQuestStep(QuestID, AppleStepID);
            return base.HandleEvent(E);
        }


        public override GameObject GetInfluencer() {
            return GameObject.FindByBlueprint("Nima Ruda");
        }

        // When the quest first starts, we check whether the player already has
        // a starapple and/or starapple jam.
        public override void Start() {
            if (The.Player.Inventory.HasObject("Starapple"))
                The.Game.FinishQuestStep(QuestID, AppleStepID);
            if (The.Player.Inventory.HasObject("Starapple Preserves"))
                The.Game.FinishQuestStep(QuestID, JamStepID);
        }
    }
}

And there you go! You should now observe the appropriate quest steps completing upon obtaining the items required by the quest.