RPG Library: Skills and Spells - ThePix/QuestJS GitHub Wiki

A skill here is a special action a character could do, probably in combat. Attacking with a sword would be a simple example, but it could be: evading an attack; attacking two foes at once; making a limited attack to give a bonus next round; making a precise attack, with a higher chance to hit, but lower damage; etc.

Go here for the introduction to RPG.

In the game world, a weapon attack is very different to a spell, but mechanically they are very similar, and Quest handles them almost the same, under the general heading of skills.

Note that skills are stored in the rpg object, and not as part of the normal world system. An important side-effect of that is that the effect itself is not saved when the user saves the game. Quest does save details about what skills/spells a character can use, so generally this works fine, but you should consider the skill/spell itself to be immutable.

There are various types of skills/spells to cover some different basic situation:

  • Skill: A generic attack, generally best not used directly
  • Weapon attack: Used with a weapon to create an Attack object
  • Natural attack: Used without a weapon to create an Attack, but not a spell (for example, a bite or claw attack or a breath attack)
  • Spell: Used without a weapon to create an Attack; a spell must be learnt before it can be used
  • SpellSelf: A special type of spell that targets the caster, and always works
  • SpellSummon: A special type of spell that summons a creature
  • SpellInanimate: A special type of spell that targets items, rather than NPCs

All attacks must use a skill. If none is specified, the default is used (this is built-in).

const defaultSkill = new WeaponAttack("Basic attack", {
  primarySuccess:lang.primarySuccess,
  primaryFailure:lang.primaryFailure,
  modifyOutgoingAttack:function(attack) {},
})

Just to muddy tge waters a little, if the player has no weapon equipped, she will by default attack using the default WeaponAttack, with her fists as the weapon, even though this should really be a NaturalAttack.

Skills

Skills tend to be more straightforward, just because they is less a warrior can do with a sword. Here is a simple example of a skill that causes the player to make two attacks on a foe in one turn. This is done via the "modifyOutgoingAttack" function, which takes the current attack, and modifies it accordingly. The various attributes on the attack object can be found here.

new Skill("Double attack", {
  description:"Two attacks is better than one - though admittedky less accurate.",
  tactical:"Attack one foe twice, but at -2 to the attack roll",
  modifyOutgoingAttack:function(attack) {
    attack.offensiveBonus -= 2
    attack.attackNumber = 2
  },
})

This example has secondary targets. The built-in function rpg.getFoesBut will get all foes (from the player's perspective) in the location except the primary target. The "modifyOutgoingAttack" function does nothing for the primary attack, which proceeds as normal, only the secondary.

new Skill("Sweeping attack", {
  description:"You attack you foe with a flourish that may do minor damage to the others who assail you.",
  tactical:"Attack one foe as normal. In addition, attack any other foe -2; on a success do 4 damage.", 
  getSecondaryTargets:rpg.getFoesBut,
  modifyOutgoingAttack:function(attack) {
    if (options.secondary) {
      attack.damageNumber = 0
      attack.damageBonus = 4
    }
    attack.offensiveBonus -= 2
  },
})

The third example has a "testUseable" function - if the player tries to do this with a bow, it will fail.

It also has an "afterUse" function, which is here used to add an active effect to the attacker, set to last 1 turn (it also does any default stuff too).

new Skill("Defensive attack", {
  description:"Make a cautious attack, careful to maintain your defense, at the expense of your attack.",
  tactical:"Attack one foe with a -2 penalty, but any attacks on you will suffer a -3 penalty until your next turn.",
  testUseable:function(char) {
    if (char.getEquippedWeapon().weaponType === 'bow') return falsemsg("This skill is not useable with a bow.")
    return skills.defaultSkillTestUseable(char)
  },
  modifyOutgoingAttack:function(attack) {
    attack.offensiveBonus -= 2
  },
  afterUse:function(attack, count) {
    const effect = skills.findEffect('Defensive')
    effect.apply(attack.attacker, attack, 1)
    skills.defaultSkillAfterUse(attack, count)
  }
})

This assumes the skill is only to be used by the player. If it is only to be used by NPCs it should not print a message in "testUseable" if the NPC tries to use it. If a skill can be used by either the player or an NPC, it needs to test for that to decide if a message is needed. This example shows how to do that.

new WeaponAttack("Sweeping attack", {
  level:1,
  description:"You attack you foe with a flourish that may do minor damage to the others who assail you.",
  tactical:"Attack one foe as normal. In addition, attack any other foe -2; on a success do 4 damage.", 
  getSecondaryTargets:rpg.getFoesBut,
  testUseable:function(char) {
    if (!char.equipped.weaponType === 'blade') {
      if (char === player) msg("This skill is only useable with a bladed weapon.")
      return false
    }
    return rpg.defaultSkillTestUseable(char)
  },
  modifyOutgoingAttack:function(attack) {
    if (options.secondary) {
      attack.damageNumber = 0
      attack.damageBonus = 4
    }
    attack.offensiveBonus -= 2
  },
})

Attack Spells

Attack spells - spells that just do damage - are essentially like skills, except that there is no weapon.

new Spell("Ice shard", {
  description:"A blast of frost power blasts your target.",
  tactical:"On a successful hit, target takes 3d6.",
  damage:'3d6',
  primarySuccess:"A shard of ice jumps from {nms:attacker:the} finger to {nm:target:the}!",
  modifyOutgoingAttack:function(attack) {
    attack.element = "frost";
  },
})

Or just do:

new SpellElementalAttack("Rainbow blast", 'rainbow', '4d12', 'A great dazzling ball of rainbow colour')

This one targets everyone except the caster.

new Spell("Fireball", {
  noTarget:true,
  description:"A ball of fire engulfs the room.",
  tactical:"Targets all in the location except you; on a successful hit, target takes 2d6.",
  damage:'2d6',
  primarySuccess:"{nv:target:reel:true} from the explosion.",
  primaryFailure:"{nv:target:ignore:true} it.",
  getPrimaryTargets:rpg.getAll,
  modifyOutgoingAttack:function(attack) {
    attack.element = "fire";
    attack.msg("The room is momentarily filled with fire.", 1)
  },
})

This one ignores armour through the "modifyOutgoingAttack" function.

new Spell("Psi-blast", {
  description:"A blast of pure mental energy blasts your target.",
  tactical:"On a successful hit, target takes 3d6; ignores armour.",
  damage:'3d6',
  primarySuccess:"A blast of raw psi-energy sends {nm:target:the} reeling.",
  primaryFailure:"A blast of raw psi-energy... is barely noticed by {nm:target:the}.",
  modifyOutgoingAttack:function(attack) {
    attack.armourMultiplier = 0
  },
})

This one also targets other foes as secondary targets.

new Spell("Lightning bolt", {
  description:"A blast of lightning leaps to your target - and perhaps his comrades too.",
  tactical:"On a successful hit, target takes 3d6 and his allies take 2d6.",
  damage:'3d6',
  element:'storm',
  secondaryDamage:'2d6',
  primarySuccess:"A lightning bolt jumps from {nms:attacker:the} out-reached hand to {nm:target:the}!",
  secondarySuccess:"A smaller bolt jumps {nms:attacker:the} target to {nm:target:the}!",
  primaryFailure:"A lightning bolt jumps from {nms:attacker:the} out-reached hand to {nm:target:the}, fizzling out before it can actually do anything.",
  secondaryFailure:"A smaller bolt jumps {nms:attacker:the} target, but entirely misses {nm:target:the}!",
  getSecondaryTargets:rpg.getFoesBut,
  modifyOutgoingAttack:function(attack) {
    attack.element = "storm";
  },
  afterPrimaryFailure:function(attack) {
    attack.secondaryTargets = []
  },
})

Summoning spells

Summoning spells are very easy. Just use the SpellSummon class, and set the "prototype" attribute to the name of a monster to clone. It also needs a duration - the summoned entity will disappear after that many turns.

new SpellSummon("Summon Frost Elemental", {
  duration:6,
  description:"Summons a lesser frost elemental; it will last about a minute, unless it is destroyed before then.",
  prototype:'frost_elemental_prototype',
})

Spells that target inanimate objects

We can use the SpellInanimate class to create a spell that affects everything in the location that fits a certain profile. Here is an "Unlock" spell to illustrate.

The "getTargets" function gets a list of things that the spell will affect. If there are none, the "msgNoTarget" string is shown. If there is at least one, the "targetEffect" function is used on each.

new SpellInanimate("Unlock", {
  description:"All locks in this location will unlock.",
  getTargets:function(attack) { 
    const list = w[attack.attacker.loc].getExits().filter(el => el.isLocked()) 
    for (const key in w) {
      if (w[key].isHere() && w[key].locked) list.push(w[key])
    }
    return list
  },
  targetEffect:function(attack, ex) {
    if (ex instanceof Exit) {
      attack.msg("The door to " + ex.nice() + " unlocks.", 1)
      ex.setLock(false)
    }
    else {
      attack.msg(processText("{nv:item:unlock:true}.", {item:ex}), 1)
      ex.locked = false
    }
  },
  msgNoTarget:"{nv:attacker:cast:true} the {i:{nm:skill}} spell, but there are no locked doors.",
})

Despite the name, we can use SpellInanimate for monsters - it could, for example, target all undead (note, however, that it skips the active effects on the target, as by default these spells have "inanimateTarget" set to true). This example targets anything with an "unillusion" function. This could be an illusionary dragon, coin or wall.

new SpellInanimate("Unillusion", {
  description:"All illusions in this location will disappear.",
  automaticSuccess:true,
  getTargets:function(attack) { 
    const list = scopeHereParser().filter(el => el.unillusion)
    list.push(currentLocation)
    return list
  },
  targetEffect:function(attack, ex) {
    ex.unillusion(attack)
  },
  msgNoTarget:"{nv:attacker:cast:true} the {i:{nm:skill}} spell, but there are no illusions here.",
})

Note that having SpellInanimate target all the qualifying objects in the location rather than targeting a specific one was a design decision made mainly because while it is easy to add a "target" verb to NPCs, it is not so easy to do it for everything.

Transforming spells

These are a special case of SpellInanimate spells. It needs to be given a prototype (it assumes the prototype will have a name that ends "prototype", and that this ending is omitted in thisattribute). It will target all objects in the location with a "transform[prototype]" attribute. Targeted objects will be removed and replaced with a clone of the given prototype. the clone will be modified by the target's transform_[prototype]" function.

This example will transform any statues in the location into stone golems. It is assumed there is a "stone_golem_prototype" object.

new SpellTransformInanimate("Animate statues", {
  level:2,
  description:"Targeted statue will become a stone golem.",
  prototype:'stone_golem',
  msgNoTarget:"{nv:attacker:cast:true} the {i:{nm:skill}} spell, but there are no statues here.",
})

Statues need to be set up for this. Specifically they need a "transform_stone_golem" function; this will be passed the cloned object. I this case it is used to make the result stone golem look like a lion. It could also be used to modify attacks, etc., but bear in mind you cannot add functions or dictionaries as they will not get saved.

createItem("statue_lion", {
  alias:'statue of a lion',
  examine:'A marble statue of a lion rampant.',
  transform_stone_golem:function(o) {
    o.ex = 'A stone golem in the form of a lion.'
  },
  loc:"yard",
})

On-going Spells

A spell can give an on-going condition to the target, by applying an effect (for more details on effects, see also here). You can specify the name of an effect that is set up elsewhere in the spell's "targetEffectName" attribute (or just set it to true if the effect has the same name as the spell). Alternatively, you can set up the effect in the spell itself, as done here.

new Spell("Cursed armour", {
  description:"Can be cast on a foe to reduce the protection armour gives.",
  tactical:"Target loses 2 from their armour, to a minimum of zero.",
  primarySuccess:"{nms:target:the:true} armour is reduced.",
  effect:{
    category:'armour',
    modifyOutgoingAttack:function(attack) {
      attack.armourModifier = (attack.armourModifier > 2 ? attack.armourModifier - 2 : 0)
    },
  },
})

new SpellSelf("Stoneskin", {
  description:"Can be cast on yourself to give protection to all physical and many elemental attacks.",
  tactical:"Adds 2 to your armour.",
  primarySuccess:"Your skin becomes as hard as stone - and yet as flexible as it was.",
  effect:{
    category:'armour',
    modifyIncomingAttack:function(attack) {
      attack.armourModifier += 2
    },
  },
})

You can give an effect a "category" attribute as shown here; a character can only ever have one effect of a specific category active at one time. Applying a second one will cancel the first.

The first is an attack spell, and the effect reduces the armour of the target. The second uses the SpellSelf class to make the spell target the caster, giving a bonus to armour.

Effects need not do something on their own - they could just flag that the effect is there and something else reacts to that.

new SpellSelf("Lore", {
  level:2,
  description:"While this spell is active, you will gain new insights into items and creatures you look at.",
  primarySuccess:"You feel enlightened.",
  effect:{},
})

You could then set up a text processor directive:

tp.addDirective("lore", function(arr, params) {
  return player.activeEffects.includes('Lore') ?  arr[1] : arr[0]
})

Then add lore to the description of items.

  examine:"{lore:It is just a stone with some squiggles scratched on it.:This is one of the Stones of Ugar, one of five made over seven centuries ago, by the arches Mages of Stifka.}",

This next example allows the caster to access new areas.

new SpellSelf("Walk On Water", {
  level:2,
  description:"While this spell is active, you can walk on water!",
  primarySuccess:"You feel lighter.",
  incompatible:'enhancements',
  effect:{},
})

You could then set up an exit by a lake or river that checks for the effect.

  north:new Exit('lake_swimming', {
    simpleUse:function(char) {
      if (char.hasEffect('Walk On Water')) {
        return util.defaultSimpleExitUse(char, new Exit('lake', {origin:this.origin, dir:this.dir, msg:"You walk out on to the surface of the lake."}))
      }
      return util.defaultSimpleExitUse(char, this)
    },
    msg:'You dive into the lake...',
  }),

You can put an effect on an item too, using the SpellInanimate class. The "getTargets" attribute gets an array of target items, while "targetEffect" will get applied to each item in that list.

new SpellInanimate("Storm Bow", {
  level:2,
  description:"The Storm Bow spell will temporarily enchant any bow to do extra Storm-based damage.",
  tactical:"Can be cast on any bow the player is holding. The weapon will then do Storm damage, and an additional 6 damage.",
  getTargets:function(attack) { return scopeReachable().filter(el => el.weaponType === 'bow' && el.loc === attack.attacker.name) },
  targetEffect:function(attack, item) {
    attack.msg(processText("{nm:item:the:true} now fizzles with electrical energy.", {item:item}), 1)
    item.activeEffects.push('Storm Bow')
  },
  effect:{
    modifyOutgoingAttack:function(attack) {
      attack.element = "storm"
      attack.damageBonus += 6
    },
  },
  msgNoTarget:"You have no bow for this spell.",
})

Attributes for Skills And Spells

Basics

name: The skill is identified in code using this. Must be present and must be unique, but unlike objects can contain spaces.

alias: The user sees this name. If al alias is not givem this will be set to the name. It is mostly for people translating to another language.

regex: User can type anything that matches this to reference the skill. If not given, it will be set from the alias.

type: What sort of skill is this? Use "weapon" if it uses a weapon, "natural" for natural attacks like punch or bite, or "spell" for spells. This will get set automatically. However, you can have your own types too, and would have to set it for those.

activityType: Like type, but used specifically for matching with activities. Usually the same as "type", but for spells use "castSpell", "castSpellSelf" or "castSpellMind". If set to "castSpellSelf", the spell can be cast when blinded. If set to "castSpellMind", the spell can be cast when dumb and unhanded. This will get set automatically, however, you can have your own types too, and would have to set it for those.

element: A spell can be given an element.

level: A spell can be given a level.

offensiveBonus: A bonus to the attack roll. This is only used if the skill/spell does not use a weapon. If it does use a weapon the bonus from the weapon will be used, and you should use "modifyOutgoingAttack" to give any bonus or penalty.

statForOffensiveBonus: What attribute of the attacker to use for the offensive bonus. It will default to "offensiveBonus_[type]" if that exists, or "offensiveBonus" if the character does not have that, but may be useful if you do something weird.

damage: A string in the usual format (eg "3d6+2"). On a successful hit, the skill/spell will do this damage. This is only used if the skill/spell does not use a weapon. If it does use a weapon the damage from the weapon will be used, and you should use "modifyOutgoingAttack" to give any bonus or penalty.

secondaryDamage: The damage the skill does on a successful hit against secondary targets, a string in the usual RPG format. If present, this is used regardless of "noWeapon". If absent, no damage is done (though you could have it done in a function).

Effects

targetEffectName: Set to true if the effect has the same name as the spell, or set to the name as a string for a different name, or omit if not applicable.

effect: A dictionary of attributes used to create an effect. The effect will have the same name as the spell/skill, and _targetEffectName" will be set to true. The attributes named in rpg.copyToEffect will be copied from the spell/skill to the effect.

duration: The on-going effect will expire after this many turns (if absent, will last indefinitely).

Flags

noDamage: If true, then no damage is calculated when the attack is successful. Spells have this set automatically unless they have a damage attribute. Other attacks will use the character's default damage if none is given, unless this is set.

noTarget: If true, the parser will not object if the player does not specify a target. This is true by default for SpellSelf.

noWeapon: If true, then the skill does not use a weapon. This is automatically set to true for spells. The skill/spell must provide the stats for the attack, specifically "damage" and optionally "offensiveBonus" and "element".

automaticSuccess: If true the effect is automatically applied, otherwise it only applies if the attack roll is successful. This is true by default for SpellSelf.

inanimateTarget: This spell targets inanimate objects.

suppressAntagonise: This spell will not annoy the target; perhaps because it is a buff or healing, or maybe just part of its magic.

Functions

modifyOutgoingAttack(attack, source): This function is run when a character performs an attack with this skill or spell - for example, a skill might give a bonus to damage dealt. You can use it to add output text by appending comments to attack.report.

getPrimaryTargets(target): This function should return an array of all the primary targets. The default just returns the given target. You can use a built-in function, rpg.getAll or rpg.getFoes (any NPC whose alligiance is not 'friend') or rog.getHosiles (any NPC with aggressive set true, i.e., actively attacking), or your own.

getSecondTargets(target): This function should return an array of all the secondary targets. The default returns an empty array. You can use a built-in function, as before. Versions that exclude the target are also available, so rpg.getFoesBut is like rpg.getFoes, but does not include the target, which is to be prefered if the target is affected by the primary effect.

getTargets(): This function is is used when targeting inanimate object.

afterPrimaryFailure(attack): This function will be called for every primary target that is missed. For example, it is used in Lightning bolt above to stop secondary targets if the attack misses.

testUseable(attacker): A function that will be passed the character. Should return true if the skill/spell can be used by that character, or give a message and return false if not.

_afterUse(attack, count):_A function that will be run after the attack is resolved. It will have the attack object passed to it, together with the number of primary targets that were hit. If the attack aborted before being resolved, this function will still run, but the count will be -1.

targetEffect(attack, target): A function that will be called if the attack hits. The default handles on-going effects, but this can be used for other things, such as to unlock all the doors for an Unlock spell.

Strings

primarySuccess, secondarySuccess, primaryFailure, secondaryFailure: Responses to give in the respective situation. You can use "target" as a text processor parameter.

msgNoTarget: If there are no primary targets, this message is used (or the default if not given).

description: The "in character" description of the spell - how someone in the game world might describe it.

tactical: The "out of character" description - the numbers.

tooltip: This text will be used as a tooltip to let the user know what the skill does.

msgAttack: Used when an attack is made.