Skip to content
Max S edited this page Jun 16, 2016 · 11 revisions

Quests

Overview

The quests module in Snipe server core has a fairly large set of features. Parts of these are moved into the user quests and chains subclasses (all of the actual logic for quest progress and completion). This article describes the basics of quests structure and manipulation. In the editor the quests block is located in the center menu on the right. It has three items - quests, quest counter types and quest condition types. The quests link actually leads to the quest chains list page.

Each quest in the project has to be in a quest chain. Quest chain is a linear chain of quests with a starting quest and finishing quest. Player progress in a quest chain is stored in user quests block, as well as current quest state. There is no limit to the amount of quests in a chain or chains on a server. When the player completes the finishing quest the chain is also completed.

Each quest has a number of parameters that are grouped into four blocks titled requires, vars, checks and results. The first group lists quest requirements. The second group lists local quest variables. The third group is called checks and contains a list of things that need to checked for successful quest completion. The fourth group contains the quest completion results - different rewards and opening new quests up. All four parameter groups can be customized for your project - you can register new parameter types through a special API in the editor.

There is a checker for common quest errors available in the editor. It checks for things like broken quest links, links to non-existing items and such. It is recommended to run it once in awhile.

Quest chains

The quest chain is a collection of quests. The basic quest chain has a beginning and an end. There are a couple of important chain parameters that can only be set on chain creation. The chain can be a collection of daily quests. At the start of each day (server time) the server will select a random quest out of each daily quests chain and will allow every user to receive and complete it. You cannot change the "daily" flag after the chain is created. The next three flags do not have any meaning for daily quest chains.

The second flag that can only be set on creation is "tutorial" flag. This is currently unused in the core.

The third flag is called "Gives badge" and is used to mark the chain as the chain that gives some level of a badge to user. The quests in the chains marked with this flag will be added to the badges structures on the server. The idea here is to use texts from these quests as badge descriptions and quest state as badge progress.

The fourth flag can be changed later and it sets the quest chain as repeatable. You can set the repeat timeout in the chain parameters.

Another thing worth mentioning is chain parent. You can set it in chain parameters and setting it will require the user to complete the parent chain before he or she can start this chain.

Editor

The editor module for quest chains is called snipe.edit.modules.ChainsEditCore and it provides the functionality to extend the chain parameters with custom ones editable through the editor. There are two methods: registerParams() and registerParamsDaily(). Both do the same thing but, as you've probably guessed, one method is for usual quest chains, the other one is for daily ones. These methods accept a list of form inputs and a list of update chain action parameters.

Here's the example:

server.coreChainsModule.registerParamsDaily([
  { title: "Min level", name: "minLevel", isParam: true },
  { title: "Max level", name: "maxLevel", isParam: true },
  ],
  [ "minLevel", "maxLevel" ]);

This example adds minimal and maximal user levels to receive quests from this daily quests chain. In the slave server code you can get access to the values of these parameters. The following example uses the getDailyChainID() hook of the snipe.slave.data.UserQuestsCore to limit the daily quests by user level:

class UserQuestsTest extends UserQuestsCore
{
  public function new(srv: ServerClass, u: UserClass, b: Block)
    {
      super(srv, u, b);
    }

  override function getDailyChainID(): Int
    {
      var level = user.attrs.get("level");
      var dailyChain = null;

      for (ch in server.coreQuestModule.iteratorChains())
        if (ch.isDaily &&
            level >= ch.getInt("minLevel") &&
            level <= ch.getInt("maxLevel") &&
            ch.isActive(user.networktype))
          {
            dailyChain = ch;
            break;
          }

      if (dailyChain == null)
        return -1;

      return dailyChain.id;
    }

When the slave server selects the quest chain ID for this user on his first login of the day this code loops through all daily quests chains finding the one appropriate for this user level and SNS type.

Quests

Each quest has a number of parameters that are grouped into four blocks titled requires, vars, checks and results. The first group lists the quest requirements - what conditions need to be met before player can receive that quest. It can be a specific user attribute, user variable value or an item in user inventory. The second group called vars lists local quest variables. They are initialized with a specified value and can be modified with quest triggers. The third group is called checks and contains a list of things that need to checked for successful quest completion, such as quest variable values, user attributes, etc. The fourth group contains the quest completion results - different rewards, user changes and opening new quests up.

The sub-sections for parameter groups show how they can be customized with new parameter types through the editor and slave server API.

Requirements

The core quest requirements contain three items - user attributes, user variables and user inventory items. This can be extended using the snipe.edit.modules.QuestsEditCore method registerParam(). This method will be used for all four groups of quest parameters. Let's take a look at the example:

server.coreQuestsModule.registerParam({
  group: "requires",
  type: "daysSinceLastLogin",
  name: "days since last login",
  printRow: function(row: { operator: String, value: Int }) {
    p("Days since last login " + row.operator + " " + row.value);
    },
  addInputs: [
    server.coreQuestsModule.operators(),
    { title: "Value", name: "value" },
    ],
  editInputs: function(p: Dynamic) {
    return [
      server.coreQuestsModule.operators(p.operator),
      { title: "Value", name: "value", value: p.value },
      ];
    },
  params: function(vars: Vars, p: Dynamic) {
    p.operator = vars.getString("operator");
    p.value = vars.getInt("value");
    }
  });

The object that is given as an argument has everything that is needed to define a new quest parameter - group, name, how to print its value and how to edit it.

For the sake of brevity we assume that the game server client has a client variable "daysSinceLastLogin" that is set each time user logs in. The remaining portion of the code is located in the user quests class of the project that extends the base snipe.slave.data.UserQuestsCore. This class provides all the hooks for special quest parameter handling. In this case we will need the requiresHook() method. When the server calls UserChainsCore.canReceiveNext() for the user chain that contains that quest and is currently on it, it will call requiresHook() for each non-core quest requirement. If the hook returns true, the user passes that requirement.

Here's the resulting code:

class UserQuestsTest extends UserQuestsCore
{
  public function new(srv: ServerClass, u: UserClass, b: Block)
    {
      super(srv, u, b);
    }

  override function requiresHook(r: { type: String, operator: String, value: Int }): Int
    {
      if (r.type == "daysSinceLastLogin")
        {
          var val = user.client.getInt("daysSinceLastLogin");
          if ((r.operator == ">=" && val < r.value) ||
              (r.operator == "<=" && val > r.value))
            return 0;

          return 1;
        }

      return -1;
    }
}

Variables

You cannot define any new quest variable types directly but this is not needed. The variables can be one of the following types: user attribute based, user variable based, counter, conditional counter and client variable. Each time the server runs a trigger, it might change the quest variable value. If the value is changed, the code checks for quest completion. The variable values are initialized when the user receives the quest.

User attributes and user variables

These variables have initial value taken from the appropriate user attribute or user variable. There are no automatic triggers on user attribute or variable changes, and for that reason, their usage is undesirable unless you track the quest completion on the client and will have a separate client request to complete the quest. The quest completion can have a check for the difference between the initial and current value of this variable.

Counters

Quest counter variables are integer counters that have a starting value of zero and can be increased through the triggers. Each counter has a type that has to be defined earlier on a separate "Quest counter types" page. The quest trigger handler (snipe.slave.data.UserQuestsCore.trigger()) has to be given the string ID of the quest counter type. It will increase or set the counter value and then check for quest completion.

Let's say you've added the number of battles counter type, made a quest that has a counter for player battles, and made a completion check that checks for that counter to be equal to three battles. You will need to put this line into the method that handles the battle finish:

c.user.quests.trigger("counter", { name: "user.battles" });

Each time a player finishes a battle, the trigger will run increasing the counter value. When the counter comes to three, the quest will be completed.

Conditional counters

The conditional counter behaves like the usual counter except it has a condition attached to it. The counter will only be increased if the given value fulfills the condition. Like the counter types, the condition types have to be created beforehand on a "Quest condition types" page. Conditional counters also have to be registered in the editor to properly edit them and display their values as strings if needed. The registration is done with the snipe.edit.modules.QuestsEditCore.registerCondCounter() method.

Let's see the example:

server.coreQuestsModule.registerCondCounter(
  {
    type: "slots.prize",
    printValue: function(row: Dynamic) {
      var res = server.query("SELECT Name FROM SlotsPrizeTypes WHERE ID = " +
        row.condValue);
      if (res.length > 0)
        p(res.next().name);
      else p("??? BUG ???");
      },
    inputs: function(condType, p: Dynamic): Array<_BlockFormInput> {
      return [
        { title: condType.condname, name: "condValue", value: p.condValue,
          type: INPUT_SELECT,
          query: "SELECT ID AS Value, Name FROM SlotsPrizeTypes ORDER BY Name" },
        ];
      },
  }
  );

This example allows the selection of the condition value from the drop-down list of values instead of inputting the number.

When triggering the conditional counter on a server, you will have to supply the value to be checked:

c.user.quests.trigger("condCounter",
  { name: "slots.prize", val: prizeID });

Client variables

The client variables do not differ that much in behavior from counters with the exception that they can be modified directly by the client. They're intended to be used in tutorials and things like that. Technically you can use counters in the exact same fashion providing the client with a handler to change a value for a specific variable types but that variant is more prone to errors. You don't have to define a client variable beforehand, just input the string ID of it in the form.

Usage example:

  public override function call(client: Client, type: String, params: Params): Dynamic
    {
      var response = null;
      if (type == "quest.incVar")
        response = incVar(client, params);
      return response;
    }

  public function incVar(c: Client, params: Params)
    {
      var name = params.getString("name");
      c.user.quests.trigger("cvar", { name: name });
      return { errorCode: "ok" };
    }

Note that there is nothing preventing a malicious client from sending that message more times than intended so the usage of client variables should be very limited.

Completion checks

The completion checks group contains various conditions which need to met before the quest can be completed. Each time the server executes a trigger, the quests that accepted it successfully will be automatically checked for completion. The core contains multiple defined condition types that can be put into a quest. You can define more, using editor API, and server hooks.

This is the list of core condition types:

  • User attributes and variables condition types check for specific values of these. Note that modifying the current values by the server code will not make an automatic quest trigger. This type is suited for using in cases when the client makes a request to complete the quest that will execute all checks.
  • Quest variable (relative) will calculate the difference between the quest variable value saved when the quest was received and the current value. This difference will then be checked against the condition.
  • Counter value checks for the value of the quest counter.
  • Conditional counter value checks for the value of conditional quest counter.
  • Client variable checks for the value of client variable.

Note that there are no automatic triggers in the core for user attribute and variable changes for performance reasons. That means that if you make a quest only with completion checks of this type the client will not be able to complete it automatically. As a workaround you can put a UserQuestsCore.trigger() call for a specific attribute in your server code. Keep in mind that each trigger call will loop through all user quests each time it is called.

For example:

  client.user.quests.trigger('attr', { name: 'reputation' });

Implementing project-specific completion checks

The first part of implementing additional completion checks consists of using the same editor API as in other cases - the method registerParam() of the class snipe.edit.modules.QuestsEditCore. Let's take a look at the example:

server.coreQuestsModule.registerParam({
  group: "checks",
  type: "joinClan",
  name: "join a clan",
  printRow: function(row: { level: Int }) {
    p("Join a clan of level " + row.level);
    },
  addInputs: [
    { title: "Level", name: "level" },
    ],
  editInputs: function(p: Dynamic) {
    return [
      { title: "Level", name: "level", value: p.level },
      ];
    },
  params: function(vars: Vars, p: Dynamic) {
    p.level = vars.getInt("level");
    }
  });

This will check that the user has joined any clan of given level.

We will need to add the trigger somewhere, where the server handles a user joining the clan:

c.user.quests.trigger("clanJoined", { amount: clan.level });

We will need both the trigger and check hooks (snipe.slave.data.UserQuestsCore class, methods triggerHook() and checkHook()):

class UserQuestsTest extends UserQuestsCore
{
  public function new(srv: ServerClass, u: UserClass, b: Block)
    {
      super(srv, u, b);
    }

  override function triggerHook(uq: UserQuestCore, type: String,
      params: _QuestTriggerParams): Int
    {
      if (type == "clanJoined")
        {
          var ok = false;
          for (ch in uq.quest.checks)
            if (ch.type == "joinClan" && ch.level == params.amount)
              {
                ok = true;
                break;
              }

          if (!ok)
            return 0;

          uq.setVar("$clanJoined", 1);
          return 1;
        }

      return -1;
    }

  override function checkHook(uq: UserQuestCore, r: Dynamic): Int
    {
      if (r.type == "joinClan")
        {
          var val = uq.getInt("$clanJoined");
          if (val < 1)
            return 0;

          return 1;
        }

      return -1;
    }

The trigger hook sets the special quest variable to 1. The completion check hook checks for the value of this variable and returns true if it equals 1.

Results

The quest results are rewards given to the player on completion and other actions performed on his or her record. The core defines the following result types:

  • "Give item": puts an item into user inventory. You will have to mark some shop item groups as containing inventory items in the item groups editor or the list of available items will be empty.
  • "Increase user attribute/variable": increases user attribute or variable.
  • "Give quest": Makes the user receive a new quest.
  • "Make quest available": sets the current quest of the chain containing this quest to it. When the server calls UserChainsCore.receiveNext() for this chain, the user will attempt this quest. This, and the previous result types, are used to move the user progress along the quest chain.
  • "Give badge": gives badge X with level Y to user.
  • "Give effect": puts the given effect on user.

Implementing project-specific results

If you want to implement an additional result type, this process will be close to the implementation of other types in quest parameter groups. First you will have to use the method registerParam() of the class snipe.edit.modules.QuestsEditCore. Let's take a look at the example:

server.coreQuestsModule.registerParam({
  group: "results",
  type: "premiumTime",
  name: "premium time",
  printRow: function(row: { amount: Int }) {
    p("Add premium time: " + row.amount + " days");
    },
  addInputs: [
    { title: "Amount", name: "amount" },
    ],
  editInputs: function(p: Dynamic) {
    return [
      { title: "Amount", name: "amount", value: p.amount },
      ];
    },
  params: function(vars: Vars, p: Dynamic) {
    p.amount = vars.getInt("amount");
    },
  });

On the slave server side you need to implement this result hook (snipe.slave.data.UserQuestsCore class, method resultHook()):

class UserQuestsTest extends UserQuestsCore
{
  public function new(srv: ServerClass, u: UserClass, b: Block)
    {
      super(srv, u, b);
    }

  override function resultHook(r: Dynamic): Int
    {
      if (r.type == "premiumTime")
        {
          server.premiumModule.addTime(user.client, r.amount);
          return 1;
        }

      return -1;
    }
}

Slave server API

There are some more slave server API methods that deserve a mention here.

snipe.slave.data.UserChainsCore

  • canReceiveNext(): returns true, if the user can attains the next quest from that chain.
  • receiveNext(): gives next quest from that chain to user without any checks.

These two methods are the main ways to start or advance the quest chain progress.

snipe.slave.data.UserQuestsCore

  • checkHook(): used to handle additional quest completion requirements. As an alternative you can use core/quest.check method subscription.
  • complete(): forces quest completion without any checks.
  • completePost(): post-completion hook: Can be used to notify the client that the quest was completed.
  • dump(): dumps user quests state into an object to send to client.
  • dumpCheckHook(), dumpRequiresHook(), dumpResultHook(): hooks that are called by dump() for every project-specific quest parameter type.
  • receive(): makes the user receive a given quest without any checks.
  • requiresHook(): used to handle additional project-specific quest requirements. As an alternative you can use core/quest.requires method subscription.
  • resultHook(): used to handle additional quest completion results. As an alternative you can use core/quest.result method subscription.
  • trigger(): sends a trigger to all quests, checks if the trigger does something and completes quests that accept it.
  • triggerHook(): trigger handler. As an alternative you can use core/quest.trigger method subscription.
Clone this wiki locally