cropped-legend-quest-logo.png

Anatomy of a Skill

If you want to write your own skills for LegendQuest here’s all the information you’ll need.

If you’re not a developer and you’re just managing your own server you won’t need this info – just for us perky coders.

Set up

Firstly you’ll need to add the LegendQuest jar as a dependency. This is done in exactly the same way as bukkit (and you’ll need bukkit as a dependency too).

Once you have your project ready and dependencies in place you’ll need the basic structure of a plugin.

Here’s an example skill.

package me.sablednah.legendquest.skills;

import java.util.List;

import org.bukkit.entity.Arrow;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.metadata.Metadatable;

@SkillManifest(
    name = "Archer", type = SkillType.ACTIVE, author = "SableDnah", version = 1.0D, 
    description = "Fire a Powerful arrow", 
    levelRequired = 0, skillPoints = 0, 
    consumes = "", manaCost = 10, 
    buildup = 0, delay = 0, duration = 0, cooldown = 10000, 
    dblvarnames = { "damage", "velocity" }, dblvarvalues = { 5.0, 1.2 }, 
    intvarnames = {    "knockback", "fire" }, intvarvalues = { 1, 1 }, 
    strvarnames = { }, strvarvalues = { }
)
public class Archer extends Skill implements Listener {
    public boolean onEnable() { return true; }
    
    public void onDisable() { /* nothing to do */ }

    public CommandResult onCommand(Player p) {
        //Check if Player has the Archer Skill and that its unlocked
        if (!validSkillUser(p)) {
            return CommandResult.FAIL;
        }

        // load skill options
        SkillDataStore data = this.getPlayerSkillData(p);

        Integer knockback = ((Integer) data.vars.get("knockback"));
        Integer fire = ((Integer) data.vars.get("fire"));
        Double damage = ((Double) data.vars.get("damage"));
        Double velocity = ((Double) data.vars.get("velocity"));

        Arrow ammo = p.launchProjectile(Arrow.class);
        ammo.setKnockbackStrength(knockback);
        if (fire > 0) {
            ammo.setFireTicks(1200);
        }
        ammo.setVelocity(ammo.getVelocity().multiply(velocity));
        ammo.setMetadata("damage", new FixedMetadataValue(lq, damage));
        ammo.setMetadata("skillname", new FixedMetadataValue(lq, getName()));

        return CommandResult.SUCCESS;
    }

    @EventHandler
    public void impact(EntityDamageByEntityEvent event) {
        if (event.getDamager() instanceof Arrow) {
            Double dmg = getMetaDamage(event.getDamager());
            String name = getMetaSkillname(event.getDamager());
            if (name.equalsIgnoreCase(getName())){
                double damage = event.getDamage();
                event.setDamage(damage + dmg);
            }
        }
    }

    public Double getMetaDamage(Metadatable object) {
        List<MetadataValue> values = object.getMetadata("damage");
        for (MetadataValue value : values) {
            if (value.getOwningPlugin() == lq) {
                return value.asDouble();
            }
        }
        return 0.0D;
    }

    public String getMetaSkillname(Metadatable object) {
        List<MetadataValue> values = object.getMetadata("skillname");
        for (MetadataValue value : values) {
            if (value.getOwningPlugin() == lq) {
                return value.asString();
            }
        }
        return "";
    }
}

Ok lets explain some elements here.

@SkillManifest

Each skill requires this Manifest.

@SkillManifest(
    name = "Archer", type = SkillType.ACTIVE, author = "SableDnah", version = 1.0D, 
    description = "Fire a Powerful arrow", 
    levelRequired = 0, skillPoints = 0, 
    consumes = "", manaCost = 10, 
    buildup = 0, delay = 0, duration = 0, cooldown = 10000, 
    dblvarnames = { "damage", "velocity" }, dblvarvalues = { 5.0, 1.2 }, 
    intvarnames = {    "knockback", "fire" }, intvarvalues = { 1, 1 }, 
    strvarnames = { }, strvarvalues = { }
)

Here’s what all the parts do.

name
The unique name for this skill. This is used as the “skillname” when making skill configurations.
type
One of SkillType.ACTIVE, SkillType.PASSIVE, SkillType.TRIGGERED.
Active skills reuire the /skill command or using a linked item to “trigger” the skill. Triggering causes the onCommand() meathod to be excuted.
PASSIVE skills are always active (yeah its an oxymoron I know!).
Triggered skills are triggered by events and cause a “reaction”.
If in doubt use ACTIVE for something that requires the player to “use” the skill, TRIGGERED for anything that reacts to an event – (hitting something, firing somehting, taking damage).. and PASSIVE for something always on – like an increase in armour.
author
Use your bukkitdev name here if you have one just so we can find you to thank you (or bug you with bug reports! :p )
version
So you know if they are using the latest version of your skill
description
Brief description of what the skill does.
levelRequired
This is the default level that the skill becomes unlocked. This is normally overriden in the skill configs so 0 is common.
skillPoints
The default number of skill points. A nice indicator of how powerfull you think this skill is. Again this is almost always overridden by end users anyways.
consumes
Item to removed from inventory on skill use (active skills mainly, but could be used for a triggered).
manaCost
Amount of mana to remove on usage. Again this is for active skills.
buildup, delay, duration, cooldown
The default values for skill timings. See the skills page for a better description of what these do. Passive skills ignore these, and triggered tend to only use duration if any.
dblvarnames and dblvarvalues
Used in the “vars:” section of a config. These are double variables that LegenqQuest will track per player for you to use in your skill. for each tring in dblvarnames you need a partner in dblvarvalues.
For example dblvarnames = { "damage", "velocity" }, dblvarvalues = { 5.0, 1.2 } Creates a double with the name of damage with a default value of 5.0 and a double called volicity with a default value of 1.2. See below for a detailed explanation of the SkillDataStore system
intvarnames and intvarvalues
As for dblvarnames and dblvarvalues, only for integer type variables.
strvarnames and strvarvalues
Again same as dblvarnames and intvarnames above but for strings.

Basic Structure

public class NewSkill extends Skill implements Listener {
    public boolean onEnable() { return true; }
    public void onDisable() { /* nothing to do */ }
    public CommandResult onCommand(Player p) {
        //Check if Player has the NewSkill Skill and that its unlocked
        if (!validSkillUser(p)) {
            return CommandResult.FAIL;
        }
        // load skill options
        SkillDataStore data = this.getPlayerSkillData(p);
        //Do stuff here
        Integer knockback = ((Integer) data.vars.get("knockback"));
    Double damage = ((Double) data.vars.get("damage"));
        String message = ((String) data.vars.get("message"));
    }
}

This is the basic structure of a skill. The skills class needs to extent the LegendQuest Skill class (me.sablednah.legendquest.skills.Skill), and if it uses events – impliment Listener.
Any @EventHandler listeners are registeres automatically by LegendQuest on loading your skill.

onEnable() is called as the skill is loaded, and onDisable() if unloaded.

public CommandResult onCommand(Player p) { Is triggered when an active skill is used. For passive/triggered skills you can just return CommandResult.NOTAVAILABLE; Player p is the player that is trying to se the skill. And if the skills has any “consumes” or mana cost these have been paid before triggering this method. After sucessfull usage the oncommand should return return CommandResult.SUCCESS;

Important parts to notice include:

if (!validSkillUser(p)) {
    return CommandResult.FAIL;
}

This checks that the current player is actually allowed to use the skill. It checks their race/class is allowed the skill. That they have the correct level, have bought it if it requires skill points, and unlocked any skills it is dependant on.

Once you’ve checked the player is valid – you’ll want to load their settings with
SkillDataStore data = this.getPlayerSkillData(p);

SkillDataStore

Once you have obtained a SkillDataStore you can extract the variables from it like so:

Integer knockback = ((Integer) data.vars.get("knockback"));
Double damage = ((Double) data.vars.get("damage"));
String message = ((String) data.vars.get("message"));

Remember to use the same types as you declared in the manifest. dblvarnames – Double, intvarnames – Integer and strvarnames – String.
SkillDataStore also contains all the skill settings for this players configuration for the skill. So you can get the name in config with data.name the duration (usefull for triggered skills).

The SkillDataStore will be specific to each player – so you could have one class use a skill in a completely diferent way to another – the variations will be in the data store.

Utility methods

By extending the Skill class you get acces to several utility methods in your skill.

You can get the current PC class for a player either by their Player object or a UUID with getPC(Player) & getPC(UUID)
The PC class contains their stats (e.g. getStatSTR()), mana, skills, class, race and has methods to check if they can craft/brew/smelt/etc, add XP, and do a SkillTest agains a stat.

Also check out me.sablednah.legendquest.mechanics.Mechanics for a range of utility finctions for getting attribute modifiers.

Heres a few simple examples.

Get players Dexterity
getPC(p).getStatDex();
Check if players has unlocked a skill (by name).
getPC(p).isValidSkill(skillName);
Get players level (regular bukkit call)
p.getLevel()
Get Strength modifier to add to damage
int damageMod = Mechanics.getPlayersAttributeModifier(getPC(p),Attribute.STR);
Give some bonus XP
getPC(p).giveXP(100);
Test the players intelligence
boolean passTest = Mechanics.skillTestB(Difficulty.TOUGH,Attribute.INT,getPC(p));
Test the players charisma and get the degree of fail/pass
int result = Mechanics.skillTest(Difficulty.TOUGH,Attribute.CHR,getPC(p));
Test the players charisma against an oponents inteligence (a charm test) where p2 is the oposing Player object.
boolean pass = Mechanics.opposedTest(getPC(p), Difficulty.AVERAGE, Attribute.CHR, getPC(p2), Difficulty.AVERAGE, Attribute.INT) ;
Get players height (as set in race).
getPC(p).getMaxHeadroom();