RPG Library ‐ The Attack Object - ThePix/QuestJS GitHub Wiki
The attack object does most of the work during combat. The attack object is created when a character initiates an attack, and destroyed when applied to the target. In theory, an RPG is all about role-playing, but in practice, RPGs are most heavily concerned with combat, and so in a sense, this is the heart of the library.
Go here for the introduction to RPG.
Attacks
There are two distinct stages to an attack. In the first stage, no target is assigned; changes here will affect them all. In the second stage the Attack object is cloned and assigned to one target; changes to the Attack object at this stage will apply to that target only.
Stage One - Outgoing From The Attacker
When an attack is made an Attack object is created. For a weapon attack, the attributes of the weapon are combined with the skill (a default is used if no skill is specified). For a non-weapon attack - usually a Spell - just the skill/spell attributes are used. Lists of primary and secondary targets are made, as determined by the skill/spell.
This is passed to various interested parties giving them the opportunity to respond to and modify the outgoing attack:
- the skill or spell
- the attacker
- active effects on the weapon
- active effects on the attacker
- active effects on anything the attacker is carrying
- the room the target is in
- any active effects on the room
Any of these with a "modifyOutgoingAttack" function will have that function called, and sent the attack object, giving them the potential to change both themselves and the attack. The function may choose to do nothing in some situations; an amulet might only have an effect when worn, for example.
To terminate the attack altogether call "abort" on the attack. You might want to do this after establishing the player is out of ammunition, for example.
attack.abort("Out of ammo!")
Transition
Prior to stage two, the parent Attack object is cloned, and it is the clone that is then passed to each target (if there are multiple attacks against one target, a clone is created for each one). Stage two therefore applies to the clone of the original attack, and any changes made apply to only the clone.
Stage Two - Incoming To The Target
In stage two, the attack is considered from the target point of view, so is incoming. Therefore spells, items, whatever need a "modifyIncomingAttack" function to modify the attack (this means an amulet could modify both an attack made by the wearer, and an attack targeting the wearing, or just one or the other).
- active effects on the target
- active effects on anything the target is carrying
- the target
Output
The attack then needs to state what happened.
Using
There are, then three stages to makes an attack. First it is created, then it is applied, then the player is told what happened. In this example, a goblin is attacking the player.
const attack = Attack.createAttack(w.goblin, player)
attack.apply()
attack.output()
Attributes
As these functions can modify an attack, you need to know what the attributes of an Attack object are, and when it makes sense to change them. Those flagged as "yes" on the "first" column can be modified during the first stage, in a "modifyOutgoingAttack" function. Those flagged as "yes" on the "second" column can be modified during the second stage, in a "modifyIncomingAttack" function.
name | first | second | comment |
---|---|---|---|
attacker | no | no | The attacker. |
skill | no | no | The skill (or spell). Do not use it to modify the skill, changes will not be saved. |
weapon | no | no | The weapon used (may be undefined ). |
attackNumber | yes | no | The number of attacks per target |
armourModifier | yes | yes | This number is added to the armour of all targets. |
armourMultiplier | yes | yes | This number is multiplied with the armour of all targets (before armourModifier is added). Set to zero have a skill ignore armour. |
offensiveBonus | yes | yes | Bonus to the attack roll. |
abort | yes | no | If true, the second stage will not be run (use if, for example, the attacker is out of magic points) |
primaryTargets | yes | no | An array of targets. I suggest not changing. |
secondaryTargets | yes | special | An array of targets. I suggest not changing except that you might want to empty the list if the primary attack fails. Only try to change in second stage in "onPrimaryFailure", which is designed for that purpose. |
element | yes | yes | The magical element the attack uses (fire, frost, etc.). |
damageBonus | yes | yes | See below. |
damageNumber | yes | yes | See below. |
damageSides | yes | yes | See below. |
damageMultiplier | yes | yes | See below. |
secondaryDamageBonus | yes | yes | See below. |
secondaryDamageNumber | yes | yes | See below. |
secondaryDamageSides | yes | yes | See below. |
secondaryDamageMultiplier | yes | yes | See below. |
Damage and secondary damage is calculated:
{damageBonus + damageNumber * [random.int(damageSides) - armour] } * damageMultiplier
Note that "damageMultiplier" and "secondaryDamageMultiplier" get modified when determining elemental effects, but you can apply your own affects too.
It is generally better to adjust rather than set values. That is, rather than set damageMultiplier to 2, set it to be twice its current value:
damageMultiplier *= 2
This allows other things to have their own effect too. An amulet might double damage, a spell double it again and a weakness to that element double it again. Multipiers should always be modified by multiplying or dividing. Other attributes should be modified by adding or subtracting.
The "msg" attribute
One of the big problems with combat is what gets reported to the player. How can we make the attacks read like prose? Or do we just give the hard facts?
Well, you can decide for yourself! Just add an "output" function to settings.
settings.attackOutputLevel = 2
settings.output = function(report) {
for (let el of report) {
if (el.level <= settings.attackOutputLevel) {
if (el.level === 1) {
msg(el.t)
}
else {
metamsg(el.t)
}
}
}
}
For this to work, all the output from your combat scripts need to go into the attack and collected, to be sent to this function on completion. The "reportTexts" attribute of the Attack object is an array of dictionaries, and each dictionary has a "t" attribute - the text - and a "level" attribute.
However, the easiest way to do output is to call the "msg" function of the attack itself. You can optionally include a number indicating the level of detail (1 is the most basic, while 4 reports die rolls, etc). The string will be passed through the text processor before adding, with the attack sent as a parameter, so you can refer to "weapon", "attacker" and "target".