-
Notifications
You must be signed in to change notification settings - Fork 5
How it works
When an item is rolled, WIRE creates a chat message and an activation. The activation is a data structure that tracks all the properties of the ongoing process of resolving the item roll. The activation is completed when any immediately applicable attack rolls, saving throws and damage rolls have been completed, damage has been calculated and the necessary damage cards created and all active effects that need to be created have been.
The activation's data is stored in the chat message, and is continuously updated there so ending a session in the middle of an activation should work just fine. Deleting the chat message resulting from an item roll also resets the activation, although it does not bring back the state of the game at the time of the roll. Any concentration lost as a consequence of starting to cast another concentration spell, for example, is not restored.
Activations are also created every time a secondary phase of an item is triggered by a condition. For example, prompting for a saving throw to end an effect at the end of a turn or when a lingering effect like Acid Arrow or Moonbeam do follow up damage.
The resolver is a technical component that takes all the built-in rules and applies them to the activation. For example, if a damage roll is needed or if a saving throw should be skipped because there is no immediate damage to save against.
Each activation proceeds in a series of steps. These are predetermined from the item being rolled as a first step before anything is done. This then forms a queue of steps that will follow one another until the whole process is done.
The flow of a regular melee attack would look like:
- Apply the selected targets: Set the target of the attack to be the targeted token
- Perform attack roll: Make a roll using the statistics of the item
- Perform damage roll: If the attack hit or if there is some damage set to "Half on miss" or "Always full", make a damage roll using the items stats.
- Apply damage: Calculate the damage caused and present a damage card
- Apply effects: If the attack hit or if some effects are flagged to activate even on a miss or a failed save, copy the effects on the spell over to the target and activate them, giving them a duration matching the item or an effect specific optional override.
- Mark the attack completed: This is an important step because it triggers any conditions that start from an attack being attempted or completed.
This flow is customisable on a per item basis if needed. Here we're going into more technical territory, so make sure you aren't too scared of javascript before trying this. The place to do the customisation is in the item macro. Although you can do other things, the actual return value of the macro should be a state tree that when evaluated returns the chain of steps that the activation needs to complete. In practice, the flow described above would be represented like this in a macro:
return this.applySelectedTargets(
this.performAttackRoll(
this.performAttackDamageRoll(
this.applyDamage(
this.applyEffects(
this.attackCompleted()
)
)
)
)
);
As an example of an alternative flow could work the Aid spell. Usually, damage needs to be applied before effects because the effects could otherwise influence the outcome of the damage roll. But for Aid, the healing done should only come after the hit point maximum has been raised.
return this.applyDefaultTargetsAsEffective( // Effective means no attack/save needed
this.applyEffects( // Raise the hp maximum
this.performSaveDamageRoll( // Do the damage roll (save damage doesn't need a hit)
this.applyDamage() // Apply healing, i.e. negative damage
)
)
);
Each flow in a macro comprises of four kinds of instructions:
- Flow step statements, that do something, like
this.performSaveDamageRoll() - Conditional checks like
this.hasDuration(), which checks if the item has a duration specified - A control statement for executing all branches in sequence
this.sequence() - A control statement for checking statements until a match is found and stopping there
this.pick()
An example of a sequence would be
this.sequence(
this.hasDuration(
this.applyConcentration()
),
this.isAttack(
this.performAttackRoll()
)
)
Here, the check for an attack roll is performed always, even if the item has a duration. Contrast this with
this.pick(
this.hasDuration(
this.applyConcentration()
),
this.isAttack(
this.performAttackRoll()
)
)
This would end the evaluation immediately if the item had a duration, and would not check for an attack.
The default for all statements is to perform a this.pick for the follow-up arguments. I.e. the two following examples do exactly the same thing:
this.performAttackRoll(
this.hasDamage(
this.performAttackDamageRoll(
this.applyDamage(
this.applyEffects(
this.attackCompleted()
)
)
)
),
this.applyEffects(
this.attackCompleted()
)
)
this.performAttackRoll(
this.pick(
this.hasDamage(
this.performAttackDamageRoll(
this.applyDamage(
this.applyEffects(
this.attackCompleted()
)
)
)
),
this.applyEffects(
this.attackCompleted()
)
)
)
Most items require just a handful of steps but there are lots of combinations out of the box. Here is the default flow that is applied to any item roll that does not have a customised flow in the item macro, or one that does not return a value from the item macro.
const friendlyTarget = this.applyDefaultTargetsAsEffective(
this.sequence(
this.performSaveDamageRoll(
this.applyDamage()
),
this.applyEffects()
)
)
const nonAttack = this.pick(
this.isSave(
this.performSavingThrow(
this.hasDamage(
this.performSaveDamageRoll(
this.sequence(
this.applyDamage(),
this.applyEffects()
)
)
),
this.applyEffects()
)
),
this.otherwise(
friendlyTarget
)
)
const secondary = this.pick(
this.hasSaveableApplications(
this.performSavingThrow(
this.hasDamage(
this.performSaveDamageRoll(
this.sequence(
this.applyDamage(),
this.applyEffects()
)
)
),
this.applyEffects()
)
),
this.hasDamage(
this.applyDefaultTargetsAsEffective(
this.performSaveDamageRoll(
this.sequence(
this.applyDamage(),
this.applyEffects()
)
)
)
),
this.applyEffects()
)
const immediate = this.pick(
this.isConditionTriggered(
secondary
),
this.sequence(
this.hasDuration(
this.hasConcentration(
this.applyConcentration()
),
this.hasAreaTarget(
this.applyDurationEffect()
)
),
this.pick(
this.isTokenTargetable(
this.sequence(
this.applySelectedTargets(),
this.pick(
this.isAttack(
this.performAttackRoll(
this.hasDamage(
this.performAttackDamageRoll(
this.applyDamage(
this.applyEffects(
this.attackCompleted()
)
)
)
),
this.applyEffects(
this.attackCompleted()
)
)
),
this.otherwise(
nonAttack
)
)
)
),
this.hasAreaTarget(
this.hasDamageOrEffects(
this.confirmTargets(
nonAttack
)
)
),
this.isSelfTarget(
friendlyTarget
)
)
)
)
return this.pick(
this.isImmediateApplication(
immediate
),
this.isDelayedApplication(
secondary
),
this.isOverTimeApplication(
secondary
)
)
Finally, the answer to the question you might be asking: What is the this?. All this is executed inside a special environment, and all the calls refer to the environment in question. In practice, that's just how it is so don't worry about it, just remember the this and to call this.attackCompleted at the end of an attack.
So you can reorder the steps, but you can also define your own steps. The way this works is that you call this.registerFlowStep(name, runAsRoller, fn) inside the macro and use this.performCustomStep(name) inside the flow definition.
Here's an example from Lay on Hands:
this.skipConfigurationDialog();
this.requestCustomConfiguration(async (item, requirements) => {
const available = item.data.data.uses.value;
const { healing, conditions, uses } = await new Promise(async (resolve, reject) => {
await new Dialog({
title: "Lay on Hands",
content: `
<form id="lay-on-hands-use-form">
<div class="form-group">
<label>Points to use <span class="units">(${available} available)</span></label>
<div class="form-fields">
<input type="range" name="lohrange" id="lohpoints" value="1" min="1" max="${available}" step="1" oninput="this.form.lohinput.value=this.value" />
<input class="range-value" name="lohinput" value="1" oninput="this.form.lohrange.value=this.value" style="width: 50px; background: transparent; font-family: var(--font-primary);" />
</div>
</div>
<div class="form-group">
<label class="checkbox"><input type="checkbox" id="consumelohuses" checked="">Consume Uses</label>
</div>
</form>
`,
buttons: {
heal: {
label: "Heal",
callback: (html) => {
const healing = Math.clamped(Math.floor(Number(html.find('#lohpoints')[0].value)), 0, available);
const conditions = 0;
const uses = html.find('#consumelohuses')[0].checked ? healing : 0;
resolve({ healing, conditions, uses });
}
},
cureDiseasePoison: {
label: "Disease/Poison",
callback: (html) => {
const healing = 0;
const conditions = Math.clamped(Math.floor(Number(html.find('#lohpoints')[0].value) / 5), 0, Math.floor(available / 5));
const uses = html.find('#consumelohuses')[0].checked ? conditions * 5 : 0;
resolve({ healing, conditions, uses });
}
}
},
default: "heal",
}).render(true);
});
requirements.consumedUsageCount = uses;
requirements.skipDefaultDialog = true;
return {
healing,
conditions
};
});
this.registerFlowStep("applyLayOnHands", true, async (activation) => {
if (activation.config.healing) {
const damageParts = await game.wire.DamageParts.singleValue(activation.config.healing, "healing");
await activation.applyDamageRollParts(damageParts);
} else if (activation.config.conditions) {
const html = `Healing <b>${activation.config.conditions}</b> diseases or poisons`;
await activation.assignCustomHtml(html);
}
});
return this.applyDefaultTargetsAsEffective(
this.performCustomStep("applyLayOnHands",
this.applyDamage()
)
);
Note that most of the macro is simply the activativation dialog and all the boilerplate that goes with it. At the end, one custom flow step is registered and it is then used as the middle step in the flow chain.
The parameters for registerFlowStep are:
- The name of the step, that you use in
this.performCustomStep - A boolean that indicates if it should be ran as the GM (
false) or as the activating user. Running it as the activating user gets access to the users targets, 3D dice and anything else that is relevant on the activating end. - A callback that will be called in the proper place in the sequence. It gets the activation as its parameter and can access and change many properties of the running activation. Here we're manually chucking in a damage roll part without an actual roll.
This also illustrates skipping the default configuration dialog and requesting a custom one. Refer to a reference page detailing all the stuff you can call inside the macro for all the possibilities.
For custom configuration (the dialog that pops up when an item is rolled), use this.requestCustomConfiguration in the macro. It takes as a parameter a function that will be called when the item is rolled before the activation itself is started. It takes two parameters:
- The item being rolled
- An use configuration object, that contains information about the resources the activation will consume. See how in the Lay on Hands example above the number of charges consumed is set.
The default configuration dialog can be suppressed either by calling this.skipConfigurationDialog() in the macro or setting the skipDefaultDialog field to true in the use configuration.
The custom configuration callback is expected to return an object that will be stored as an activation configuration, and it will be available in every custom step through activation.config. Some things like spellLevel (the level of a cast spell) and upcastLevel (how many levels it was upcast above the minimum) will be added there by default. It can also be updated between steps by calling activation.assignConfig with the updated object as a parameter.