Quest scripting

From TheManaWorld

Jump to: navigation, search

This article is outdated

Parts of this article are outdated and do no longer represent the current state of the project or the intentions of the development team.

This article collects information regarding the conceptualisation of the gameplay of The Mana World

This article contains information for Programmers working or interested in working for The Mana World

Contents

Current quest style

The current server uses eAthena, and quests are written in the custom eAthena scripting language. Please see EAthena Scripting Standards for details.

A new style of scripting

Motivation

The above is very convenient for simple things, but leaves for things to be desired when it comes to more complex behaviour or implementing general patterns that could be reused and plugged together with parameters. It is also imaginable that you'd want to write scripts that describe how a certain kind of quest is generated. Enlarging the possibilities in the scripting system can directly influence the amount, the diversity and the complexity of quests players encounter in the game.

This is why we're looking for a modern scripting language that's easy to pick up and in which we can make it easy to write quests like the above, but still allows for more advanced constructs.

A feeble attempt to do better

We take the same example as above, writing a quest that requires at least three keys to be present in player inventory to be opened. This is just an initial idea, probably better methods of implementing could turn out to be more convenient during implementation or trying out different situations. In the following, the placement of the treasure chest has been disconnected from the definition. If we decide to also place objects using a Ruby script, it shouldn't be a problem to define behaviour inline when preferred (for example for unique objects or situations).

class TreasureChest < Actor
  def onActivate(player)
    answer = player.ask("Would you try to open it?", ["Yup", "Nope"]);

    if (answer == "Yup") then
      if (player.inventory.count('treasure key') < 3) then
        player.inventory.remove('treasure key', 3)
        player.inventory.add('short sword')
        player.message("You opened it and found a short sword!")
      else
        player.message("It seems that this is not the right key...")
      end
    end
  end
end

The most notable difference with the previous approach is the object oriented approach. Items are counted, removed and added by an inventory instance which is referenced by a player instance. The player reference is the gateway to modifying player properties and showing messages and choices to the player. The onActivate event is called when the player activates the actor. Similarly, actions could be defined on colliding or standing on an actor, or on certain (possibly set) time intervals.

Another small difference is the usage of unique names instead of IDs for the treasure key and short sword. There is no reason why the scripting engine wouldn't be able to allow translating unique item names to their IDs.

State machine based quest scripting

Example of a state machine.
Example of a state machine.

In the following case, Laurens and Bjørn have devised a way to describe a quest to get the village idiot a maggot slime, based on a finite state machine model.

To the right you can see the state machine that was implemented. This quest is an introduction quest, the player is already in state 1 when he first enters the game. The first objective is to talk to an instructor located in Tulimshar. He tells you to give a maggot slime to the village idiot, and come back to report about it.

This approach:

  • avoids having to modify the scripts for the involved NPCs and items
  • avoids having to explicitly use global variables stored with the player character
  • allows extendability of conditions through subclassing
  • allows the scripter to use more or less chronological order
  • can hopefully be improved to be shorter and possibly more understandable

The script follows:

# An example class implementing a conditional check on a pickup event
class PickupCond < Condition
  def init(item)
    @item = item
  end
  def satisfiedBy(event)
    return (event.isa(PickupEvent) and event.item = @item)
  end
end

# The states used by this script are:
# 1 - start
# 2 - find_maggot_slime
# 3 - give_to_idiot
# 4 - finish

# Some variables to reduce typing later
giveSlimeIntro = Sequence(
       Message("tulimshar_instructor",
               "Hello %{player.name}, I heared of your coming this morning "
               "and have been anticipating your arrival. I have great "
               "expectations."),
       Message("tulimshar_instructor",
               "However, as a test of your competence I'm first going to have "
               "to ask you to get our village idiot a maggot slime, and "
               "report to me his response."),
       Message("tulimshar_instructor",
               "Good luck on your first quest!"))

pickupSlime = PickupCond("maggot_slime")

pickupSlimeMessage = Message("You've obtained a maggot slime, remember to "
                             "give one to the village idiot!"))

# Talking to instructor without maggot slime
Trans("start", "find_maggot_slime",
      And(Talk("tulimshar_instructor"), Not(HaveItemCond("maggot_slime"))),
      giveSlimeIntro)

# Talking to instructor with maggot slime
Trans("start", "give_to_idiot", Talk("tulimshar_instructor"), giveSlimeIntro)

# Talking to instructor again, without maggot slime
Trans("find_maggot_slime", "find_maggot_slime", Talk("tulimshar_instructor"),
      Message("tulimshar_instructor",
              "Did you give the village idiot a maggot slime yet?"))

# Picking up a maggot slime while searching for it
Trans("find_maggot_slime", "give_to_idiot", pickupSlime, pickupSlimeMessage]])

# Losing your last maggot slime without giving it to the idiot
Trans("give_to_idiot", "find_maggot_slime", ItemCount("maggot_slime", 0))

# Talking to instructor again, with maggot slime
Trans("give_to_idiot", "give_to_idiot", Talk("tulimshar_instructor"),
      Message("tulimshar_instructor",
              "Did you give the village idiot a maggot slime yet?"))

# Talking to idiot with maggot slime
Trans("give_to_idiot", "give_to_idiot", Talk("tulimshar_idiot"),
      Option("tasty", "Hmm, I see you have a tasty maggot slime.",
             ["You can have it", "Indeed, goodbye"]))

# Choosing to give a maggot slime to the idiot
Trans("give_to_idiot", "finish", Choice("tasty", 0),
      Sequence(DeleteInventory("maggot_slime"),
               Message("tulimshar_idiot", "Thanks, cheers mate.")))

# Script is not finished yet as the player should still report back to the
# instructor about the response of the idiot...
Personal tools