Skip to content

Combat Event Scripts

"implements" : "combatEvent" in a mod's scripts section declares a combat event script. These scripts handle unit events during battle, including waits, attacks and round starts. Use them for effects triggered by battle events.

Attaching a script to a unit

A COMBAT_EVENT_TRIGGER bonus assigns a combat event script to a unit. Creature abilities, artifacts, secondary skills and other bonus sources can grant it:

1
2
3
4
5
"drainsLife" : {
    "type" : "COMBAT_EVENT_TRIGGER",
    "subtype" : "lifeDrain",
    "val" : 100
}

A spell grants the same bonus for its own duration through the built-in attachCombatScript spell effect:

1
2
3
4
5
6
7
8
"effects" : {
    "attachSpikes" : {
        "type" : "attachCombatScript",
        "eventScript" : "spikes",
        "eventValue" : 10,
        "eventParameters" : { "poison" : true }
    }
}

eventParameters is passed unchanged and validated against the script's schema. The engine does not add caster data. Include the spell, spell power or other required caster data in this object.

Description shown to the player

Every scripted ability uses the same COMBAT_EVENT_TRIGGER bonus type. The script's description field provides its player-facing text. ${val} is replaced with the total value of bonuses granting this script. ${parameterName} is replaced with the corresponding bonus parameter:

1
2
3
4
5
6
7
8
"lifeDrain" : {
    "implements" : "combatEvent",
    "script" : "combat/lifeDrain",
    "patches" : [ ],
    "priority" : 0,
    "schema" : { "properties" : {}, "additionalProperties" : false },
    "description" : "{Life Drain}\nRestores health equal to ${val}% of damage dealt."
}

Writing a script

Scripts extend combatScript and define only the required event methods. Events without a matching method are skipped:

local Base = require("combat/combatScript")
local Script = setmetatable({}, {__index = Base})
Script.__index = Script

function Script:onAfterAttacked(server, battle, unit, other)
    if other and other:isAlive() then
        server:damageUnit(battle, other, self.damage)
    end
end

return Script

Assumptions and guarantees

VCMI guarantees the following:

  • every implemented handler executes for its bearer. Events without a matching handler are skipped
  • bonus parameters are read-only. Persistent state requires a separate bonus
  • each action dispatches onActionFinished once, regardless of attack, counterattack or target count
  • script effects can generate onDeath, but script-applied spells do not generate onSpellHit. Death handlers generated by a script execute after the current handler returns
  • handlers execute serially and do not nest
  • every matching handler executes. A script must skip any counterattack, repeated attack or dead-bearer case it does not support

Event handlers

All handlers share the same signature and return nothing:

Signature: function Script:on<Event>(server, battle, unit, other, payload)

Bonus parameters initialize fields of self. For example, { "poison" : true } in addInfo becomes self.poison. The bonus value becomes self.val.

Each bonus granting the same script creates a separate handler invocation with its own val. Values are not combined before execution. Use the same stacking group for all sources when only the strongest bonus should apply, as in the core fire shield configuration.

Parameters:

  • server - callback for battle state modifications. See BattleServer
  • battle - current battle state. See Battle
  • unit - bonus bearer. See Unit
  • other - other event participant, such as the attacker; may be nil
  • payload - data about the attack or spell that caused this event. Fields unused by an event keep their default values:
  • ranged - whether the attack was a shot
  • isCounter - whether the attack is a counterattack, either a first strike or a regular retaliation
  • attackIndex - position of this attack among attacks made by the same side during the action. The first attack and every counterattack use 0; the second attack of a double attack uses 1
  • targets - one entry per affected unit. Each entry contains unit, damage, killed, damageBeforeDefense and healthBeforeAttack. Before the attack, only unit and healthBeforeAttack are set because damage has not been calculated
  • spell - spell that caused onUnitSpellcast or onSpellHit; nil for other events

For onSpellHit, unitBefore contains the target state captured before spell effects. It is nil for other events.

Every handler receives the complete target list. self:ownEntry(unit, payload) returns the entry for the handling unit. target.unit is nil if the event removed the unit from the battlefield.

Handlers:

  • onBeforeAttack - executes on the attacker before each attack, including counterattacks
  • onBeforeAttacked - executes before each attack on every target, including secondary targets
  • onAfterAttack - executes on the attacker after the attack. It also executes if an attack reaction killed the attacker; check unit:isAlive() when required
  • onAfterAttacked - executes after the attack on every target. It also executes after a lethal hit; check unit:isAlive() when required
  • onWait - executes when unit waits
  • onDefend - executes when unit defends
  • onBeforeMove - executes before movement. See Moves
  • onAfterMove - executes after movement, once for every onBeforeMove
  • onUnitSpellcast - executes after unit casts a spell
  • onSpellHit - executes after casting on every affected unit. other is the casting unit, or nil for a hero cast. See Spell hits
  • onDeath - executes after the action that killed unit. other is the killing unit, or nil for deaths caused by spells, moats or scripts. See Deaths
  • onActionFinished - executes once after action resolution on the actor and every affected unit. other is the actor, or nil for a hero cast. See The end of an action
  • onBattleSetup - executes once for every unit before tactics. other is nil
  • onBattleStart - executes once for every unit after tactics and before opening spells. other is nil
  • onRoundStart - executes for every alive unit at the start of each round after the first. onBattleStart covers the first round. other is nil

Order in which combat events fire

Attack events execute in this order for each attack:

1
2
3
4
5
onBeforeAttack   (attacker)  \  one group, ordered by priority
onBeforeAttacked (each unit about to be hit)  /
      ... damage is calculated and applied, combat log entries are added ...
onAfterAttack    (attacker)  \  one group, ordered by priority
onAfterAttacked  (each unit that was hit)  /

The attacker and targets form one group ordered by priority. The side of the attack does not affect order. Life drain at priority 0 therefore executes before fire shield at priority 50.

The complete sequence also executes for counterattacks, additional attacks and attacks whose bearer dies during resolution. Each script decides whether its effect still applies.

Moves

A move is a move action, movement before a melee attack or adjacent spell cast, or return movement from RETURN_AFTER_STRIKE. Return movement is a separate move. No movement events execute when the unit remains on its current hex.

Every onBeforeMove has a matching onAfterMove, including movement cancelled after a handler changes the unit or destination. Movement events preceding an attack execute before attack count is calculated, so ADDITIONAL_ATTACK granted by them applies to the current attack.

Spell hits

onSpellHit is generated only by deliberate hero and unit casts, including Enchanter casts. Moats, spell-like attacks, obstacle effects and script-applied spells do not generate it.

Deaths

Death events execute after the action that caused them. Deaths from one action form one batch. Deaths caused by handlers form subsequent batches until none remain. Clones are included. payload.targets contains one entry per death in the batch; killed is the number of creatures killed by the lethal hit.

The end of an action

onActionFinished executes once for the actor and every unit affected by the action, after counterattacks and additional attacks. Use it to apply effects accumulated across the action. other identifies the actor; compare it with unit to detect the unit's own action.

Built-in scripts

Every combat event script declares priority in its scripts entry. Handlers for the same event execute from lowest to highest priority. The usual value is 0. The field is required because the fallback bonus order is alphabetical and therefore unsuitable for behavior ordering.

Priority also orders death handlers. Resurrection scripts use a priority below 100. Scripts that react to a final death use 100 or higher.

ballistaDamage

Applies a CREATURE_DAMAGE bonus to a war machine during battle setup. The value uses its hero's base and artifact attack, multiplied by bonus val. Army, spell and terrain attack bonuses are excluded.

Override getDamageRange(unit, minDamage, maxDamage) in a patch to change the formula. It returns the final minimum and maximum damage.

arrowTowerDamage

Applies arrow tower damage calculated from the defended town's buildings during battle setup. Outside a siege, the tower keeps its creature damage.

Parameters:

  • keepBase - damage of the keep in a town with nothing built
  • towerBase - damage of the two lesser towers in a town with nothing built
  • perBuilding - damage each building adds to the keep; the lesser towers get half of it

The scripts below preserve compatibility with retired bonuses. They reproduce the corresponding H3 and WoG behavior, including quirks. Mods requiring different behavior should provide separate scripts.

rebirth

Resurrects the bearer once per battle. The restored count is a percentage of the initial stack count and uses random rounding. Clones are not resurrected. The resurrected stack cannot retaliate until its next turn.

Priority 0 executes resurrection before final-death reactions.

Parameters:

  • val - share of the starting size of the stack that is resurrected, in percent
  • guaranteed - if true, restores at least one creature

lifeDrain

Restores part of the bearer's damage as health and may resurrect creatures in the stack. Only damage dealt to living targets counts.

Priority 0 applies healing before later attack reactions.

Parameters:

  • val - share of the dealt damage restored to the attacker, in percent

fireShield

Deals fire damage to a melee attacker based on damage before defence. Fire-immune attackers and secondary targets of area attacks take no reflected damage.

Priority 50.

Parameters:

  • val - share of the reflected damage, in percent

deathStare

Kills creatures in the attacked stack. Each creature in the bearer stack provides one chance, and eligible attackers limit the maximum kills.

Priority 100 executes after reactions that may kill the bearer and prevent the effect.

Parameters:

  • val - chance for each creature to kill one, in percent
  • situation - when the ability applies: "melee", "ranged", "rangedDistancePenalty", "rangedWallPenalty" or "rangedDistanceAndWallPenalty"
  • spell - spell used for the animation, immunities and combat log. Defaults to death stare

killsIn returns the kill count, or nil when the ability does not apply. Override it in a patch to add another situation. combat/deathStareCommander is an example:

1
2
3
4
5
6
7
function Script:killsIn(server, battle, unit, other, payload)
    if self.situation ~= "commander" then
        return Base.killsIn(self, server, battle, unit, other, payload)
    end

    return <however many this ability kills>
end

"commander" is DEPRECATED and is defined by that patch. It preserves the commander skill converted from DEATH_STARE. For this situation, val is the kill count before applying the stack-level ratio. Add new situations through patches.

enchanted

Reapplies a spell to the bearer or its side at the start of every round.

Parameters:

  • spell - spell whose effects are applied
  • level - mastery level the effects are applied at
  • massive - true to affect every allied unit instead of only the bearer
  • duration - how many turns the effects last. Defaults to 50, long enough for the effect to accumulate instead of expiring between rounds

summonGuardians

DEPRECATED, transition only - see the note at the start of this section.

Summons guardians around the bearer at battle start. Placement follows H3 rules, including positions near the bearer's battlefield edge.

Parameters:

  • creature - creature to summon as guardian
  • val - size of each guardian stack, in percent of the guarded stack

transmutation

DEPRECATED, transition only - see the note at the start of this section.

Replaces the attacked stack with another creature type, as used by the WoG werewolf ability. Non-living units and units with TRANSMUTATION_IMMUNITY are not affected.

Priority 300.

Parameters:

  • val - percentage chance to trigger on each attack
  • creature - creature the victim turns into. Defaults to the attacker's own creature
  • transmuteBy - "health" keeps the total health of the victim, "count" keeps its creature count

soulSteal

DEPRECATED, transition only - see the note at the start of this section.

Adds creatures to the bearer stack for each killed enemy creature, including growth beyond the original stack size. Only living targets count.

Parameters:

  • val - creatures gained for each killed enemy creature
  • permanent - true to keep the gained creatures after the battle

destruction

DEPRECATED, transition only - see the note at the start of this section.

Kills creatures of the attacked stack outright, on top of the damage the attack itself dealt.

Priority 400.

Parameters:

  • val - percentage chance to trigger on each attack
  • killBy - "percentage" kills a share of the victim's stack, "count" kills a fixed number
  • amount - the share, or the number of creatures, depending on killBy