Skip to content

Example Macros

Stephen Cooper edited this page Jul 11, 2021 · 15 revisions

There is a new tab in skills, spells, invocations, and psionics named "Macro". In this tab are two fields: "Type" (either "script" or "chat") and a textarea where you can type in your code.

Please see [Using Macros] for instructions on the data structures and how to access the arguments passed to the HM Macros.

To find out what an object looks like, use console.log(item)

Common code snippets

Basic logging

These commands will log the object parameters to the console (F12) and once in the console, you can expand attributes and explore the objects to find exactly what you need.

console.log(actor);
console.log(token);
console.log(speaker);
console.log(item);
console.log(rollResult);
console.log(rollData);

Get the targeted actor

const targets = game.user.targets;
if (!targets?.size) {
  ui.notifications.warn(`No targets selected, you must select exactly one target.`);
  return null;
} else if (targets.size > 1) {
  ui.notifications.warn(`${targets.size} targets selected, you must select exactly one target.`);
 return null;
}
const targetActor = Array.from(targets)[0].document._actor;

Call a pre-defined macro

const macroName = "Spell Fatigue";
const macro = game.macros.find(m => m.name === macroName && m.data.type === 'script' && !m.hasPlayerOwner); 
if (macro) 
  eval(macro.data.command); 
else 
  ui.notifications.warn(`No GM macro named ${macroName} found`);

Sample Macros

Macros for hooks

Create script macros and name them identically to the hook you want to implement, and the macro will be automatically called as part of that hook.

hm3.onSpellRoll

This script can be customized to however you are doing fatigue for your spell casting. E.g. if you want to treat level 1 spells as not costing fatigue, then you would implement it here (check item.level). This is a macro for the rules as written in HarnMaster.

let fatigue = "0:"
let result = ''
switch (rollResult.result) {
  case 'CS':
    fatigue = "0";
    result = "Mage creates a perfect Form that costs no fatigue";
    break;
  case 'MS':
    fatigue = "+1";
    result = "Mage fine-tunes the Form, causing fatigue";
    break;
  case 'MF':
    fatigue = "+1";
    result = "Mage creates a Form but it is not suited for the spell. Aborting the spell costs fatigue";
    break;
  case 'CF':
    fatigue = "+2";
    result = "The Principle is summoned into a faulty Form which could not be repaired. Optionally roll on critical spell failure table.";
    break;
  default:
    fatigue = "0";
    result = `Unknown roll result ${rollResult.result}`;
 }
  
 game.hm3.macros.changeFatigue(fatigue, actor);
 ChatMessage.create({
     user: game.user._id,
     speaker: speaker,
     content: result
 }, {});

hm3.onFumbleRoll

If the actor fails his/her fumble roll, add an Active Effect that expires after one round to indicate that he/she must spend an action to pick up the weapon dropped or to draw a new weapon.

if (!rollResult.isSuccess) {
  const ae = actor.effects.find(m => m.data.label === "Fumble");
  if (ae) {
    const result = await ae.update({
      disabled: false,
      duration: {
        startTurn: game.combat.data.turn,
        startRound: game.combat.data.round,
        rounds: 1
      }
    })
  } else {
    const activeEffectData = {
      label: "Fumble",
      icon: "icons/commodities/metal/fragments-sword-steel.webp",
      duration: {
        startTurn: game.combat.data.turn,
        startRound: game.combat.data.round,
        rounds: 1
      },
      changes: []
    }
    const result = await ActiveEffect.create(activeEffectData, { parent: token.actor });
  };
}

hm3.onShockRoll

If the actor fails their shock roll, set the actor as defeated (will be skipped in the combat tracker) and put a skull over the token. This replicates what happens when you press the skull icon in the combat tracker.

if (!rollResult.isSuccess) {
  if (!token.actor.effects.find(m => m.data.label === "Dead")) {
    const activeEffectData = {
      label: "Dead",
      icon: "icons/svg/skull.svg",
    }
    const result = await ActiveEffect.create(activeEffectData, { parent: token.actor });
    console.log(result)
    result.setFlag("core", "statusId", "dead");
    result.setFlag("core", "overlay", true);
    token.combatant.update({ defeated: true });
  };
}

hm3.onStumbleRoll

If the actor fails their stumble roll, put an AE on the token which reminds the player that next round they have to spend an action to get back up. This AE expires after one round.

if (!rollResult.isSuccess) {
  const ae = token.actor.effects.find(m => m.data.label === "Restrained");
  if (ae) {
    const result = await ae.update({
      disabled: false,
      duration: {
        startTurn: game.combat.data.turn,
        startRound: game.combat.data.round,
        rounds: 1
      }
    });
  } else {
    const activeEffectData = {
      label: "Restrained",
      icon: "icons/svg/net.svg",
      duration: {
        startTurn: game.combat.data.turn,
        startRound: game.combat.data.round,
        rounds: 1
      }
    }
    const result = await ActiveEffect.create(activeEffectData, { parent: token.actor });
    result.setFlag("core", "statusId", "restrain");
  }
}

Place a circular template when casting a spell.

You can explore the options for making a template by placing a template, making it look how you want, then copying the settings to the MeasuredTemplateData

//*****************************************************************************
// Find the target of the spell - if no target selected, select the caster
const spellName = item.name;
const targets = game.user.targets;
console.log(Array.from(targets)[0])
const targetToken = (targets?.size == 1) ?
  Array.from(targets)[0] :
  token;
// If successful, set down a template. The width depends on SI
if (rollResult.isSuccess) {
  const width = rollResult.isCritical ?
    item.data.data.skillIndex * 2 :
    item.data.data.skillIndex;
  const foo = await MeasuredTemplate.create({
    t: "circle",
    x: targetToken.center.x,
    y: targetToken.center.y,
    distance: width,
    width: 1,
    angle: 0
  }, { temporary: false });
}

Place a template when casting a spell, angle points to the target. Add in Token Fx

You can explore the options for making a template by placing a template, making it look how you want, then copying the settings to the MeasuredTemplateData

const spellName = item.name;
// *****************************************************************************
// *****************************************************************************
// Find the target of the spell - if no target selected, "abort" the spell
const targets = game.user.targets;
if (!targets?.size) {
  ui.notifications.warn(`No targets selected, you must select exactly one target, spell ${spellName} aborted.`);
  return null;
} else if (targets.size > 1) {
  ui.notifications.warn(`${targets} targets selected, you must select exactly one target, spell ${spellName} aborted.`);
  return null;
}
const targetToken = Array.from(targets)[0];

// *****************************************************************************
// If successful, set down a template. The length is SI*5'
if (rollResult.isSuccess) {
  const source = token.center;
  const dest = targetToken.center;
  const ray = new Ray(source, dest);
  const length = item.data.data.skillIndex * 5;
  const data = {
    t: "cone",
    user: game.user.id,
    fillColor: game.user.color,
    direction: ray.angle * 180 / 3.14,
    angle: 33,
    x: source.x,
    y: source.y,
    distance: length,
    width: 5,
    flags:
    {
      tokenmagic:
      {
        options:
        {
          tmfxPreset: "Wild Magic",
          tmfxTint: 0x00FF90,
          tmfxTextureAlpha: 0.50
        }
      }
    }
  };
  const template = await MeasuredTemplate.create(data, { temporary: false });
}

Magic Sword spell macro

This is the most complex macro. It does several things

  1. checks that you have exactly one token targeted and gets the actor for that token
  2. Looks for an existing Active Effect on the target named the same as the spell.
  3. Either creates a new Active Effect or re-activates an existing Active Effect.
// *****************************************************************************
// Find the target of the spell - if no target selected, "abort" the spell
const spellName = item.name;
const targets = game.user.targets;
if (!targets?.size) {
  ui.notifications.warn(`No targets selected, you must select exactly one target, spell ${spellName} aborted.`);
  return null;
} else if (targets.size > 1) {
  ui.notifications.warn(`${targets.size} targets selected, you must select exactly one target, spell ${spellName} aborted.`);
  return null;
}
const targetActor = Array.from(targets)[0].document._actor;

// *****************************************************************************
// Only proceed if we have a success
if (rollResult.isSuccess) {
  // Either add 5 or 10 to the ML of the target
  let bonus = 5;
  if (rollResult.isCritical) {bonus = 10}
  const changeData = [
        {key: 'data.eph.meleeDMLMod', value: bonus, mode: 2 },
        {key: 'data.eph.meleeAMLMod', value: bonus, mode: 2 }
    ];

  // *****************************************************************************
  // Now find out if this actor already has an Active Effect for this spell.
  const ae = targetActor.effects.find(m => m.data.label === spellName);
  if (ae) {
    // update the current active effect
    const result = await ae.update({disabled: false, 
                                    duration: {startTurn: game.combat.data.turn,
                                    startRound: game.combat.data.round,
                                    rounds: 10},
                                   'changes': changeData});
    if (result) console.log(`Active Effect ${spellName} activated!`);
  } else {
    // create a new Active Effect
    const activeEffectData = {
      label: spellName,
      icon: item.data.img,
      origin: targetActor.uuid,
      duration: {startTurn: game.combat.data.turn,
                 startRound: game.combat.data.round,
                 rounds: 10},
      'changes': changeData
    }
    const result = await ActiveEffect.create(activeEffectData, {parent: targetActor});
    if (result) console.log(`Active Effect ${spellName} created!`);
  }
}

Token Magic FX spell macro

This macro relies on Turn Alert which currently isn't working in 0.8, as well as TokenMagic. It does three things:

  1. Calls a GM-owned macro named 'Magic Shield' to apply a Token Magic effect on the currently selected token
  2. Calls TurnAlert to clear the TokenMagic effect (not working)
const spellName = item.name;
if (rollResult.isSuccess) {

  console.log("Creating TokenMagic");
  const spellFx = game.macros.find(m => m.name === `${spellName} Fx` && m.data.type === 'script' 
&& !m.hasPlayerOwner); 
  if (spellFx) eval(spellFx.data.command);
  if (!spellFx) console.log(`No GM macro named "${spellName} Fx" found`)

  console.log("Creating turn alert");
// get the turn id of the current combatant
  const turnId = game.combat.combatant.id;
  console.log(`turnId: ${turnId}`);
  const roundNumber = 1;
  const roundAbsolute = false;
  const alertData = {
    round: roundNumber,
    roundAbsolute: roundAbsolute,
    turnId: turnId,
    macro: "Remove Spell Fx",
    label: `End of duration for "${spellName}`,
    message: `The spell ${spellName} cast by ${token.name} has ended`

  }
console.log(alertData);
  TurnAlert.create(alertData);
}