linktree Atrinik.org - Multiplayer Online Role Playing Game  >  Atrinik News  >  Developers' Corner
linktree Topic: Quest system changes
Pages: [1]   Go Down
  Print  
Author Topic: Quest system changes  (Read 2278 times)
0 Members and 1 Guest are viewing this topic. Bookmarked by 0 members.
Offline Cleo
Developer
Alex Tokar

Posts: 580
Gender: Male
« on: February 23, 2014, 09:20:29 pm »

Hey guys,

There have been some changes in the quest API (mostly QuestManagerMulti) that affect its logic and behaviour. In most cases, old scripts shouldn't need updating, but they might want to, in order to migrate to the new InterfaceBuilderQuest API, which I will discuss below.

The change is that quest parts can now define sub-parts, or multi-multi-parts, or whatever you want to call it. Consider this quest:

Code: [Select]
FortSetherIllness = {
    "quest_name": "Fort Sether Illness",
    "type": QUEST_TYPE_MULTI,
    "message": "Gwenty, a priestess of Grunhilde has asked you to figure out why Fort Sether guards keep falling ill. She seems to suspect it's because of the water, in which case it might be work checking out the water wells.",
    "parts": {
        "figure": {
            "message": "Figure out what is causing the illness in Fort Sether.",
            "type": QUEST_TYPE_SPECIAL,
        },
        "ask advice": {
            "message": "You found a kobold named Brownrott below Fort Sether, with a most extraordinary garden. He has shown you a potion he uses to make his garden grow well, and its smell drove you nauseous. Perhaps you should ask Gwenty, the priestess in Fort Sether, for advice.",
            "type": QUEST_TYPE_SPECIAL,
        },
        "deliver potion": {
            "message": "Gwenty, the priestess in Fort Sether, has given you a potion to mix with Brownrott's one. He may need some persuading, however...",
            "type": QUEST_TYPE_SPECIAL,
        },
        "get hearts": {
            "message": "Just as you thought, Brownrott was very reluctant to mix his potion with yours, and has asked you to bring him 10 sword spider hearts first, which can be found by killing sword spiders below Fort Sether.",
            "type": QUEST_TYPE_KILL_ITEM,
            "arch_name": "bone_skull",
            "item_name": "sword spider's heart",
            "num": 10,
        },
        "reward": {
            "message": "After delivering the spider hearts to Brownrott, he mixed his potion with yours. You should report to Gwenty for a reward.",
            "type": QUEST_TYPE_SPECIAL,
        },
    },
}


It is now defined as:

Code: [Select]
FortSetherIllness = {
    "quest_name": "Fort Sether Illness",
    "type": QUEST_TYPE_MULTI,
    "message": "Gwenty, a priestess of Grunhilde has asked you to figure out why Fort Sether guards keep falling ill. She seems to suspect it's because of the water, in which case it might be work checking out the water wells.",
    "parts": OrderedDict({
        "figure": {
            "message": "Figure out what is causing the illness in Fort Sether.",
            "type": QUEST_TYPE_SPECIAL,
        },
        "report": {
            "message": "You found a kobold named Brownrott below Fort Sether, with a most extraordinary garden. He has shown you a potion he uses to make his garden grow well, and its smell drove you nauseous. Perhaps you should ask Gwenty, the priestess in Fort Sether, for advice.",
            "type": QUEST_TYPE_SPECIAL,
        },
        "deliver_potion": {
            "message": "Gwenty, the priestess in Fort Sether, has given you a potion to mix with Brownrott's one. He may need some persuading, however...",
            "type": QUEST_TYPE_KILL_ITEM,
            "arch_name": "bone_skull",
            "item_name": "Gwenty's Potion",
            "parts": OrderedDict({
                "get_hearts": {
                    "message": "Just as you thought, Brownrott was very reluctant to mix his potion with yours, and has asked you to bring him 10 sword spider hearts first, which can be found by killing sword spiders below Fort Sether.",
                    "type": QUEST_TYPE_KILL_ITEM,
                    "arch_name": "bone_skull",
                    "item_name": "sword spider's heart",
                    "num": 10,
                },
            }),
        },
        "reward": {
            "message": "After delivering the spider hearts to Brownrott, he mixed his potion with yours. You should report to Gwenty for a reward.",
            "type": QUEST_TYPE_SPECIAL,
        },
    }),
}


The most important thing to note here is that "deliver_potion" quest part now has a "parts" dictionary, which is where "get_hearts" quest has moved. This helps with logic checking in things such as the new InterfaceBuilderQuest API, and allows the developer to define an exact quest flow; before it was not clear that in order to complete "deliver potion", one needs to start and complete "get hearts".

Onto the InterfaceBuilderQuest API; consider this old code for the above quest, and the NPC Gwenty:

Code: [Select]
## @file
## Handles Gwenty, a priestess of Grunhilde, located in Fort Sether.
##
## Gives out the 'Fort Sether Illness' quest.

from QuestManager import QuestManagerMulti
from Quests import FortSetherIllness as quest
from Interface import Interface

qm = QuestManagerMulti(activator, quest)
inf = Interface(activator, me)

def main():
    # Completed the quest, offer normal temple services.
    if qm.completed():
        import Temple

        temple = Temple.TempleGrunhilde(activator, me, inf)
        temple.hello_msg = "We won't forget what you have done for us, {}.".format(activator.name)
        temple.handle_chat(msg)
    # Not started the quest, try offering the quest.
    elif not qm.started("figure"):
        if msg == "hello":
            inf.add_msg("Welcome, stranger. I'd like to help you by offering my usual priest services, however, the illness that is going on in the fort is keeping me rather busy, so if you'll excuse me...")
            inf.add_link("What sort of illness?", dest = "illness")

        elif msg == "illness":
            inf.add_msg("Well, if you're so curious, I can spare a few moments. Perhaps you might be able to help us...")
            inf.add_msg("Many guards are falling ill, one after another. Being the only priestess around, I'm quite busy tending the sick guards. However, I'm not able to completely cure them, as they just keep falling ill soon after I cure them...")
            inf.add_link("Do you know the reason?", dest = "reason")

        elif msg == "reason":
            inf.add_msg("If only! If I wasn't so busy, I would probably be able to figure it out. But as it is...")
            inf.add_link("Could I help?", dest = "help")
            inf.add_link("I see...", dest = "see")

        elif msg == "see":
            inf.add_msg("Would you be willing to help us? I am afraid I can't solve this problem without some assistance...")
            inf.add_link("Sure, I'll help.", dest = "help")
            inf.add_link("Not interested.", action = "close")

        elif msg == "help":
            inf.add_msg("Ah, yes! Thank you. Your help is much appreciated...")
            inf.add_msg("I'm not sure where the disease is originating from. The only thing I can think of is the water, which we get from an underground river of sorts.")
            inf.add_msg("It would certainly be worth investigating the river... perhaps something is going on down there. The fastest way to do that would be to climb down using one of the water wells in the fort.")
            qm.start("figure")
    # Accepted the quest, but haven't met the kobold yet.
    elif qm.need_complete("figure"):
        if msg == "hello":
            inf.add_msg("You agreed to help us {}, no?".format(activator.name))
            inf.add_msg("I still suspect the cause of the illness is the water, which we get from an underground river of sorts.")
            inf.add_msg("It would certainly be worth investigating the river... perhaps something is going on down there. The fastest way to do that would be to climb down using one of the water wells in the fort.")
    # Met the kobold.
    elif qm.need_complete("ask advice"):
        if msg == "hello":
            inf.add_msg("Well? Have you investigated the water wells yet?")
            inf.add_link("Yes...", dest = "yes")

        elif msg == "yes":
            inf.add_msg("You explain about Brownrott and his garden...", COLOR_YELLOW)
            inf.add_msg("Indeed? So it was the water, like I thought... Well, I think I know how to mend this particular problem...")
            inf.add_objects(me.FindObject(name = "Gwenty's Potion"))
            inf.add_msg("Here. Take that potion, and bring it to the kobold. Tell him he needs to mix it with his own. It should cancel out the negative effects it causes to us humans, but still make it effective for his garden. It might take some persuading, however...")
            qm.start("deliver potion")
            qm.complete("ask advice")
    # Got the potion from Gwenty, but have not convinced the kobold yet.
    elif qm.need_complete("deliver potion"):
        if msg == "hello":
            inf.add_msg("So, have you convinced the kobold to mix his potion with the one I gave you?")
            inf.add_link("Still working on it.", dest = "working")

            # If the player lost the potion, make it possible to get it back.
            if not activator.FindObject(mode = INVENTORY_CONTAINERS, name = "Gwenty's Potion"):
                inf.add_link("I lost the potion you gave me...", dest = "lost")

        elif msg == "working":
            inf.add_msg("Very well...")

        elif msg == "lost" and not activator.FindObject(mode = INVENTORY_CONTAINERS, name = "Gwenty's Potion"):
            inf.add_msg("What?! Well, it's not irreplaceable, but please, don't go wasting it and just convince the kobold to mix it with his potion... Here, take this spare one I have, and be more careful with it...")
            inf.add_objects(me.FindObject(name = "Gwenty's Potion"))
    # Convinced the kobold to mix the two potions.
    elif qm.need_complete("reward"):
        if msg == "hello":
            inf.add_msg("So, have you convinced the kobold to mix his potion with the one I gave you?")
            inf.add_link("Yes, I have.", dest = "yes")

        elif msg == "yes":
            inf.add_msg("That's great news, {}! I knew I could count on you. You have my thanks, as well as that of all the people in the fort. We can now drink the water safely again...".format(activator.name))
            inf.add_msg("As for your reward... Please, accept these coins from me. Also, since I'm no longer busy tending all the sick guards, I can now resume offering you my regular services.")
            inf.add_objects(me.FindObject(archname = "silvercoin"))
            qm.complete("reward")

main()
inf.finish()


This way of building a quest is really awkward; it more often than not leads to issues with wrong checks somewhere along the way, which allow the player to progress the quest incorrectly, or when they shouldn't, or get an item multiple times, or not have an item removed, etc. The new way can be defined as thus:

Code: [Select]
## @file
## Handles Gwenty, a priestess of Grunhilde, located in Fort Sether.
##
## Gives out the 'Fort Sether Illness' quest.

from QuestManager import QuestManagerMulti
from Interface import InterfaceBuilderQuest
from Quests import FortSetherIllness as quest
import Temple

class InterfaceDialog_completed(InterfaceBuilderQuest):
    """
    Quest has been completed, offer usual temple services.
    """

    def __init__(self, *args, **kwargs):
        super(InterfaceBuilderQuest, self).__init__(*args, **kwargs)
        self.temple = Temple.TempleGrunhilde(activator, me, self)

    def dialog_hello(self):
        self.temple.hello_msg = "We won't forget what you have done for us, " \
                                "{}.".format(activator.name)
        self.temple.handle_chat(msg)

    def dialog(self, msg):
        self.temple.handle_chat(msg)

class InterfaceDialog_need_start_figure(InterfaceBuilderQuest):
    """
    Player has not yet agreed to figuring out the cause of the sickness.
    """

    def dialog_hello(self):
        self.add_msg("Welcome, stranger. I'd like to help you by offering my "
                     "usual priest services, however, the illness that is "
                     "going on in the fort is keeping me rather busy, so if "
                     "you'll excuse me...")
        self.add_link("What sort of illness?", dest = "illness")

    def dialog_illness(self):
        self.add_msg("Well, if you're so curious, I can spare a few moments. "
                     "Perhaps you might be able to help us...")
        self.add_msg("Many guards are falling ill, one after another. Being "
                     "the only priestess around, I'm quite busy tending the "
                     "sick guards. However, I'm not able to completely cure "
                     "them, as they just keep falling ill soon after I cure "
                     "them...")
        self.add_link("Do you know the reason?", dest = "reason")

    def dialog_reason(self):
        self.add_msg("If only! If I wasn't so busy, I would probably be able "
                     "to figure it out. But as it is...")
        self.add_link("Could I help?", dest = "help")
        self.add_link("I see...", dest = "see")

    def dialog_see(self):
        self.add_msg("Would you be willing to help us? I am afraid I can't "
                     "solve this problem without some assistance...")
        self.add_link("Sure, I'll help.", dest = "help")
        self.add_link("Not interested.", action = "close")

    def dialog_help(self):
        self.add_msg("Ah, yes! Thank you. Your help is much appreciated...")
        self.add_msg("I'm not sure where the disease is originating from. The "
                     "only thing I can think of is the water, which we get "
                     "from an underground river of sorts.")
        self.add_msg("It would certainly be worth investigating the river... "
                     "perhaps something is going on down there. The fastest "
                     "way to do that would be to climb down using one of the "
                     "water wells in the fort.")
        self.qm.start("figure")

class InterfaceDialog_need_complete_figure(InterfaceBuilderQuest):
    """
    Player needs to figure out the source of the sickness.
    """

    def dialog_hello(self):
        self.add_msg("You agreed to help us {activator.name}, no?")
        self.add_msg("I still suspect the cause of the illness is the water, "
                     "which we get from an underground river of sorts.")
        self.add_msg("It would certainly be worth investigating the river... "
                     "perhaps something is going on down there. The fastest "
                     "way to do that would be to climb down using one of the "
                     "water wells in the fort.")

class InterfaceDialog_need_complete_report(InterfaceBuilderQuest):
    """
    Reporting about Brownrott's potion.
    """

    def dialog_hello(self):
        self.add_msg("Well? Have you investigated the water wells yet?")
        self.add_link("Yes...", dest = "yes")

    def dialog_yes(self):
        self.add_msg("You explain about Brownrott and his garden...",
                     color = COLOR_YELLOW)
        self.add_msg("Indeed? So it was the water, like I thought... Well, I "
                     "think I know how to mend this particular problem...")
        self.add_objects(me.FindObject(name = "Gwenty's Potion"))
        self.add_msg("Here. Take that potion, and bring it to the kobold. "
                     "Tell him he needs to mix it with his own. It should "
                     "cancel out the negative effects it causes to us humans, "
                     "but still make it effective for his garden. It might "
                     "take some persuading, however...")
        self.qm.start("deliver_potion")
        self.qm.complete("report")

class InterfaceDialog_need_complete_deliver_potion(InterfaceBuilderQuest):
    """
    Still need to deliver the potion to Brownrott.
    """

    def dialog_hello(self):
        self.add_msg("So, have you convinced the kobold to mix his potion "
                     "with the one I gave you?")
        self.add_link("Still working on it.", dest = "working")

    def dialog_working(self):
        self.add_msg("Very well...")

class InterfaceDialog_need_finish_deliver_potion(
    InterfaceDialog_need_complete_deliver_potion
):
    """
    Still need to deliver the potion to Brownrott, but lost the potion.
    """

    def dialog_hello(self):
        InterfaceDialog_need_complete_deliver_potion.dialog_hello(self)
        self.add_link("I lost the potion you gave me...", dest = "lost")

    def dialog_lost(self):
        self.add_msg("What?! Well, it's not irreplaceable, but please, don't "
                     "go wasting it and just convince the kobold to mix it "
                     "with his potion... Here, take this spare one I have, "
                     "and be more careful with it...")
        self.add_objects(me.FindObject(name = "Gwenty's Potion"))


class InterfaceDialog_need_complete_reward(InterfaceBuilderQuest):
    """
    Potion has been delivered to Brownrott, claim the reward.
    """

    def dialog_hello(self):
        self.add_msg("So, have you convinced the kobold to mix his potion "
                     "with the one I gave you?")
        self.add_link("Yes, I have.", dest = "yes")

    def dialog_yes(self):
        self.add_msg("That's great news, {activator.name}! I knew I could "
                     "count on you. You have my thanks, as well as that of "
                     "all the people in the fort. We can now drink the water "
                     "safely again...")
        self.add_msg("As for your reward... Please, accept these coins from "
                     "me. Also, since I'm no longer busy tending all the sick "
                     "guards, I can now resume offering you my regular "
                     "services.")
        self.add_objects(me.FindObject(archname = "silvercoin"))
        self.qm.complete("reward")

qm = QuestManagerMulti(activator, quest)
ib = InterfaceBuilderQuest(activator, me)
ib.finish(locals(), qm, msg)


Each part of a quest has its own class, and often multiple versions - for when the part needs to be started, when it's not finished yet (quest item is missing), and when it can be completed.

Basically, the class "InterfaceDialog_need_start_figure" would be used when the player has not started the quest yet at all (because that's the first part, and it's not started, so this class would be used). Thus a dialog can be defined in that class, and at the end of the dialog, do qm.start("figure"), which starts that quest part. The next time the player talks to this NPC, they get dialog for the class "InterfaceDialog_need_complete_figure" - because the "figure" part has been started, but not completed yet - it's completed at a different NPC.

This interface API system removes the constant message and logic checking when dealing with quests, and in general simplifies quest/dialog writing. The interface API also exists as simple, non-quest use under InterfaceBuilder class. This merely moves code such as: if msg == "hello": into def dialog_hello(self):.

Hopefully this will become the preferred method of writing quests, as it both makes it easier to write quests, and easier to maintain and update them.

Thanks,
Cleo
 Logged
Pages: [1]   Go Up
  Print  
 
Jump to: