Extending the MARS Combat System

From Multiverse

Jump to: navigation, search

Contents

Introduction

This article provides a basic overview of how to extend the MARS combat system. This article is intended for use with version 1.5 of the Multiverse platform.

Before going through this tutorial, you will want to make sure you have familiarity with server side scripting. Particularly you will want to familiarize yourself with the MarsAbility and MarsEffect classes that are used in the multiverse/config/sampleworld/ability_db.py script. This example also sets up additional attributes for players and mobs to use in combat calculations. For information on adding attributes, see MARS Combat Statistics.

Getting started

This example demonstrates how to implement some basic combat calculations to determine if an attack misses or hits when a melee combat ability is used. To do this, you will need some additional combat statistics. You will add three new attributes that can be assigned to players and mobs:

  • Strength
  • Dexterity
  • Level

To do this, edit the script multiverse/config/sampleworld/combat.py and add the following three lines to register the attributes:

CombatPlugin.registerStat( MarsStatDef("strength") )
CombatPlugin.registerStat( MarsStatDef("dexterity") )
CombatPlugin.registerStat( MarsStatDef("level") )

Extending MarsAbility

MARS provides a foundation on which you can build your world's particular rule set. Here we are going to focus on implementing a melee combat system that will handle hit or miss determination. To accomplish this we will extend the CombatAbility class. First look at our code in multiverse/config/sampleworld/ability_db.py that extends CombatAbility and then we will break it down into sections to explain whats going on.

class MeleeCombatAbility (CombatAbility):
    def resolveHit(self, state):
        params = HashMap()
        attacker = state.getObject()
        target = state.getTarget()

        #compute chance to hit by calculating an attack and defense stength
        attackerDexterity = int(attacker.statGetCurrentValue("dexterity"))
        attacherStrength = int(attacker.statGetCurrentValue("strength"))
        attackerLevel = int(attacker.statGetCurrentValue("level"))
        attackerAttackStrength = attackerDexterity + attacherStrength

        targetDexterity = int(target.statGetCurrentValue("dexterity"))
        targetStrength = int(target.statGetCurrentValue("strength"))
        targetLevel = int(target.statGetCurrentValue("level"))
        targetDefenseStrength = targetDexterity + targetStrength

        #Calc a hit bonus by comparing level diff: compare attack strength with target's defense strength
        hitBonus = (attackerAttackStrength + (attackerLevel*5)) - (targetDefenseStrength + (targetLevel*5))

        dice = Dice(100)
        roll = dice.roll(1)

        if roll + hitBonus > 80:
            params.put("result","hit")
        else:
            params.put("result","miss")

        return params


Look at the first couple of lines of the above code:

class MeleeCombatAbility (CombatAbility):
    def resolveHit(self, state):

This code defines a new class, MeleeCombatAbility, that extends the CombatAbility. You only want to override the resolveHit method that contains the logic to determine hit success.

Examine the next few lines:

params = HashMap()
attacker = state.getObject()
target = state.getTarget()

The above code sets up some local variables needed to perform calculations and pass information to the next step in the attack process.

The params variable will be used to store hit success or failure messages and the resolveHit method's return value.

The state parameter to the resolveHit method gives us access to a lot of useful information we can leverage. In the code above we use it to grab the information about the attack and about the target of the attack (multiverse.mars.core.MarsAbility.State). Both the state.getObject() and the state.getTarget() methods return an object of type multiverse.mars.objects.CombatInfo. We will use the attacker and target properties to gather information about each and use in determining our hit success.

Now look at the next section of code:

        #compute chance to hit by calculating an attack and defense stength
        attackerDexterity = int(attacker.statGetCurrentValue("dexterity"))
        attacherStrength = int(attacker.statGetCurrentValue("strength"))
        attackerLevel = int(attacker.statGetCurrentValue("level"))
        attackerAttackStrength = attackerDexterity + attacherStrength

        targetDexterity = int(target.statGetCurrentValue("dexterity"))
        targetStrength = int(target.statGetCurrentValue("strength"))
        targetLevel = int(target.statGetCurrentValue("level"))
        targetDefenseStrength = targetDexterity + targetStrength

The section of code above simply gathers information about the three attributes we defined in our Getting Started section (this tutorial assumes you know how and have added the attributes in the proper places like character creation and mob templates). The statGetCurrentValue method simply grabs the current value of the attribute. As you see we can utilize our attributes to formulate attack and defense strengths. Our example here is very basic and could and probably will be much more complex as you build your system.

Now look at the next section of code:

# Calc a hit bonus by comparing level difference and attacker's attack strength 
# with target's defense strength
hitBonus = (attackerAttackStrength + (attackerLevel*5)) - (targetDefenseStrength + (targetLevel*5))

This code shows a very basic example of how we can use the attributes we define to calculate a to hit bonus that we can apply to our random hit roll. The calculation basically takes the calculated attack strength and defense strengths of the two combatants and then modifies those based on the levels of each. If the target’s defense strength or level is higher than the attack strength and level of the attacker, then it will be more difficult for the attacker to hit the target.

The next and final section of code uses the above information to calculate hit success and return a message about the outcome:

dice = Dice(100)
roll = dice.roll(1)

if roll + hitBonus > 80:
      params.put("result","hit")
else:
      params.put("result","miss")

return params

The above code uses the multiverse.mars.util.Dice class to generate a random number between 1 and 100. We then add our hitBonus value to the roll and compare that against our base chance to hit or 80. If the resulting value is above 80, then we add a hit result to our params property, otherwise we assign a miss result.

And that pretty much wraps up our example for extending the CombatAbility class to implement our own was of determining hit calculations. In the next section we will extend the DamageEffect class to apply our hit determination result.

Extending DamageEffect

Once a hit determination is made, the system attempts to apply the resulting effect on the target. Here we will show an example of extending the multiverse.mars.effects.DamageEffect class so that we can capture whether or not the attack was successful or not and handle it accordingly. Below we will show the entire implementation of our new effect that override DamageEffect, and then go through sections of it to give more detail on what’s going on:

class CombatEffect (DamageEffect):
    def apply(self, state):
        params = state.getParams()
        result = params.get("result")
        target = state.getObject()

        if (result == "miss"):
            ##Throw a missed msg to the client
            ##We could also just invoke a DamageMessage with a zero dmg value
            msg = TargetedExtensionMessage(state.getObject().getOid());
            msg.setProperty("ext_msg_subtype", "mv.COMBAT_ABILITY_MISSED");
            msg.setProperty("target", state.getTarget().getOid());
            msg.setProperty("attacker", state.getObject().getOid());   
            Engine.getAgent().sendBroadcast(msg);
        else:
            #Since we extend DamageEffect we could modify the damage here
            # and then send our own DamageMessage
            MarsEffect.apply(self, state)

So now lets take a look at our CombatEffect in more detail.

class CombatEffect (DamageEffect):
    def apply(self, state):

The above code show were we are creating new class called CombatEffect that extends the DamageEffect class. In this case we are only overriding the apply method. This method is where want to catch the result of our MeleeCombatAbility and determine if we will actually apply the damage to the target.

        params = state.getParams()
        result = params.get("result")
        target = state.getObject()

In the above code we are again grabbing information that we will need to process the results of our attack. Here we are grabbing the result value that we set in our MeleeCombatAbility and grabbing information about the target that we may or may not apply damage to.

        if (result == "miss"):
            ##Throw a missed msg to the client
            ##We could also just invoke a DamageMessage with a zero dmg value
            msg = TargetedExtensionMessage(state.getObject().getOid());
            msg.setProperty("ext_msg_subtype", "mv.COMBAT_ABILITY_MISSED");
            msg.setProperty("target", state.getTarget().getOid());
            msg.setProperty("attacker", state.getObject().getOid());   
            Engine.getAgent().sendBroadcast(msg);
        else:
            #Since we extend DamageEffect we could modify the damage here
            # and then send our own DamageMessage
            MarsEffect.apply(self, state)

In the above code we are simply testing the result of our combat result and acting accordingly. In this case if our result was a miss, then we send a TargetedExtensionMessage to the attack to advise them of the miss. If the result was a hit, then we simply call MarsEffect.apply to apply the damage to the user and continue on.

Applying the custom combat system

Now that we have all of our basic stuff implemented, now it is time to put it to use. One this of note that is very important; When using the custom class we created in the previous section, they must be located in the script file prior to using them or you will get Python compilation errors.

For detailed information for creating and adding new abilities to your world please review the MARS Abilities and Combat Plugin document if you have not already.

Implementing CombatEffect

Our first step in implementing our combat system is to create a new effect. This effect will use the CombatEffect that we created previously. This effect should be defined after our CombatEffect class definition and should be in the multiverse/config/sampleworld/ability_db.py file.

effect = CombatEffect("default weaponskill effect")
effect.setMinInstantDamage(5)
effect.setMaxInstantDamage(15)
effect.setDamageType("Physical")
Mars.EffectManager.register(effect.getName(), effect)

The above code is the example we use in the updated Sample World demo that was released with the 1.2 version. Here we define the name of the effect by passing it as a parameter to the CombatEffect constructor. Then we set our basic damage values and damage type and register the effect with the EffectManager.

Implementing MeleeCombatAbility

Now that we have our combat effect setup we can create an instance of our MeleeCombatAbility. You will want to make sure that when you implement the ability that it is after our class definition. Abilities should also be defined in the multiverse/config/sampleworld/ability_db.py file.

ability = MeleeCombatAbility("Wounding Thrust")
ability.setTargetType(MarsAbility.TargetType.ENEMY)
ability.setMaxRange(5000)
ability.setActivationEffect(Mars.EffectManager.get("default weaponskill effect"))
ability.addCoordEffect(MarsAbility.ActivationState.COMPLETED, attackEffect)
ability.setActivationCost(5)
ability.setCostProperty("stamina")
ability.setCompleteSound("swordhit.wav")
ability.setActivationTime(1000)
ability.setRequiredSkill(Mars.SkillManager.get("Sword"), 1)
ability.setIcon("Interface\FantasyWorldIcons\WEAPON_sword_A")
ability.setExperiencePerUse(1)
ability.setBaseExpThreshold(10)
ability.setMaxRank(3)
# define a leveling map so that one can control how it gains levels
woundinglm = LevelingMap()
woundinglm.setAllLevelModification(0.0, 10)
ability.setLevelingMap(woundinglm)
Mars.AbilityManager.register(ability.getName(), ability)

You should already be familiar enough with the base CombatAbility and how to set them up, so I will only cover some items of not about the above code.

ability = MeleeCombatAbility("Wounding Thrust")

The above line of code creates a new ability using our MeleeCombatAbility that we defined earlier and sets its name to "Wounding Thrust."

ability.setActivationEffect(Mars.EffectManager.get("default weaponskill effect"))

The above line of code sets the abilities associated effect to "default weaponskill effect," which is the effect that we created earlier using our CombatEffect class.

Assigning the Ability

All that is really left is to assign the ability to a player or mob. There are a couple of ways in which you can assign an ability to a player or mob. In both cases, you can set the ability as the auto attack ability by setting the CombatInfo.COMBAT_PROP_AUTOATTACK_ABILITY to the name of the ability you created. So for a mob you would open up the multiverse/config/sampleworld/templates.py file, find the mob you want to assign the ability to and change the current entry to something like:

…..
tmpl.put(CombatClient.NAMESPACE, CombatInfo.COMBAT_PROP_AUTOATTACK_ABILITY, "Wounding Thrust")
…..

Another way for players to obtain abilities has been introduced with the Multiverse 1.2 platform release. Players may now obtain skills through NPC trainers. For more information on setting up trainer, please review the MARS Trainer Plugin documentation.

If a skill was setup in the multiverse/config/sampleworld/skill_db.py file and an ability was set as the skills default ability, then the player would automatically receive the default ability in their abilities bar when they train the associated skill.

An example given in the updated Sample World demo shows how a Sword skill was setup and defines a default ability as shown below:

skill = MarsCombatSkill("Sword")
skill.setDefaultAbility("Wounding Thrust")
skill.setExperiencePerUse(1)
skill.setBaseExpThreshold(10)
skill.setMaxRank(3)
# create a leveling map for this skill
swordlm = LevelingMap()
swordlm.setAllLevelModification(0.5, 0)
skill.setLevelingMap(swordlm)
Mars.SkillManager.register(skill.getName(), skill)

You will notice that on this line…

skill.setDefaultAbility("Wounding Thrust")

…that the ability that we setup has been associated with the Sword skill that was created here. This tells the Skills and Abilities system to automatically assign the “Wounding Thrust” ability to the player when they train the “Sword” skill.

Personal tools