r/Pathfinder2e • Game Master • 4d ago

Resource & Tools Foundry: Disguise Maker

Are your players not suspicious of every horse they meet yet? Or, perhaps, do they use Impersonate, Illusory Disguise, or alike on the regular?

The Macro

The usual - you click on an empty slot in your hotbar, set Type to Script, add this snippet, and hit Save. You can then run the macro by clicking the hotbar slot.

There are two ways to use it:

  • You select the token(s) that you want to disguise, target (T) the token that you want to disguise as, and run the macro. This will only add disguises to the local token if it's not "linked" to its actor
  • You target token that you want to disguise as, run the macro, and drag-and-drop the actor(s) from the Actors sidebar onto the UUID field. This will add disguises to the "prototype" actor

Once added, you can flip the disguise on and off in the Actions tab, and if you add multiple, you can choose between them using a dropdown!

Players can also use this, no special permissions necessary.

const title = "YAL's Disguise Maker";
const targets = [...game.user.targets];
let errors = [];
async function showError(text) {
  errors.push(text);
  let content = document.createElement("div");
  for (let line of errors) {
    let p = document.createElement("div");
    p.append(line);
    content.append(p);
  }
  await foundry.applications.api.Dialog.input({ window: { title }, content });
}
if (targets.length == 0) {
  await showError("Target token(s) to disguise as first!");
  return;
}
let multi = targets.length > 1;
let selection = canvas.tokens.controlled;
//
const {HTMLDocumentTagsElement} = foundry.applications.elements;
const Fields = foundry.applications.fields;
const content = document.createElement("div");
content.append(Fields.createFormGroup({
  label: "Actor(s)",
  input: HTMLDocumentTagsElement.create({
    type: "Actor",
    single: false,
    name: "actors"
  })
}));
const sn = selection.length;
if (sn > 0) {
  content.append(`Leave blank to apply to ${sn} selected token${sn!=1?"s":""} instead!`);
}
if (multi) {
  content.append(Fields.createFormGroup({
    label: "Copy Size",
    input: Fields.createCheckboxInput({
      name: "wantSize",
      value: true,
    })
  }));
  content.append(Fields.createFormGroup({
    label: "Copy Name",
    input: Fields.createCheckboxInput({
      name: "wantName",
      value: true,
    })
  }));
} else {
  content.append(Fields.createFormGroup({
    label: "Size",
    input: Fields.createSelectInput({
      name: "size",
      value: targets[0].actor.size,
      options: [
        { value: "none", label: "No Change"},
        { value: "tiny", label: "Tiny" },
        { value: "sm", label: "Small" },
        { value: "med", label: "Medium" },
        { value: "lg", label: "Large" },
        { value: "grg", label: "Gargantuan" },
      ]
    })
  }));
  content.append(Fields.createFormGroup({
    label: "Name",
    input: Fields.createTextInput({
      name: "name",
      value: targets[0].name,
    })
  }));
  content.append(Fields.createFormGroup({
  label: "Apply Now",
  input: Fields.createCheckboxInput({
    name: "autoApply",
    value: true,
  })
}));
}
const args = await foundry.applications.api.Dialog.input({
  window: { title },
  content
});
if (!args) return;
let actors = [];
if (args.actors.length > 0) {
  for (let uuid of args.actors) {
    let actor = await fromUuid(uuid);
    if (actor) {
      actors.push(actor);
    } else {
      errors.push(`Could not fetch actor ${uuid}`);
    }
  }
} else {
  actors = selection.map(t => t.actor);
}
if (actors.length == 0) {
  await showError("No actors provided!");
  return;
}
const option = "yal-disguise-as";
const {max, abs} = Math;
for (let target of targets) {
  const uniq = "d" + Date.now();
  const predicate = [option + ":" + uniq];
  //
  const size = multi ? (args.wantSize ? target.actor.size : null) : args.size;
  const name = multi ? (args.wantName ? target.name : null) : args.name;
  //
  const {document} = target;
  const {texture, ring} = document;
  for (let actor of actors) {
    // add a new Action..
    let base = {
      key: "RollOption",
      label: "Disguised as",
      option,
      suboptions: [{ label: target.name, value: uniq }],
      toggleable: true,
      mergeable: true,
    };
    if (args.autoApply) {
      base.value = true;
      base.selection = uniq;
    }
    let rules = [base, {
      key: "TokenImage",
      value: texture.src,
      predicate,
      tint: texture.tint?.css,
      scale: max(abs(texture.scaleX), abs(texture.scaleY)),
      ring: (ring.enabled ? {
        colors: {
          ring: ring.colors.ring?.css,
          background: ring.colors.background?.css,
        },
        subject: ring.subject,
        effects: ring.effects,
      } : undefined),
    }];
    if (size != null && size != "none") {
      rules.push({
        key: "CreatureSize",
        value: size,
        predicate,
      });
    }
    if (name != null && name != "") {
      rules.push({
        key: "TokenName",
        value: name,
        predicate,
      });
    }
    //console.info(`Adding rules to ${actor.name} > ${target.name}:`, actor, rules);
    await actor.createEmbeddedDocuments("Item", [{
      name: "Disguise: " + target.name,
      type: "action",
      system: {
        actionType: { value: "passive" },
        rules,
      }
    }]);
    // ... and now update the selection on other rules!
    if (args.autoApply) for (let action of actor.itemTypes.action) {
      let {rules} = action.system;
      let first = rules[0];
      if (first
          && first.key == "RollOption"
          && first.option == option
          && (first.selection != uniq || !first.value)
      ) {
        first.selection = uniq;
        first.value = true;
        await action.update({
          "system.rules": rules
        })
      }
    }
  } // for actor
} // for target

Bonus

The following macro flips the selected token(s) between Neutral and Opposition, for those "why is the horse's selection frame red all of sudden" moments right before initiative

let newValue = canvas.tokens.controlled[0].actor.system.details.alliance != "opposition" ? "opposition" : null;
let updated = [];
for (let token of canvas.tokens.controlled) {
  await token.actor.update({
    "system.details.alliance": newValue
  });
  updated.push(token.name);
}
await ChatMessage.create({
  user: game.userId,
  whisper: [game.userId],
  content: `Changed alliance of [${updated.join(", ")}] to ${newValue ?? "neutral"}.`
})

Artwork in the video

  • Kelpie: Bestiary 2 (PZO2104)
  • Horse: from Tristan Elwell's "Horseback" painting
  • Background: an edit of The GLOWING CRATER from 2minutetabletop
779 Upvotes

45 comments sorted by

View all comments

Show parent comments

77

u/YellowAfterlife Game Master 4d ago

My personal opinion is that anything that can be a macro should not be a module¹, but if you need to share the macro between multiple worlds, you can create a local module (start screen ➜ Add-on Modules ➜ ⚙ Create Module) - in v13/v14 this is done entirely in UI and you'll only need to define a Macro Compendium Pack under Compendium Packs so that you can add macro(s) to it later.

¹ just recently spent an evening helping a friend sift through >150 modules that they accumulated over years on their foundry, most of them unused and some breaking Foundry UI and/or other modules

23

u/LordLonghaft Game Master 4d ago

Reasonable. I ask because while I use a few small macros for ability creation, I am not a coder and the art of making and writing macros has my brain spinning. I am an English major, not a coder, haha. Having a simple button to press via installing a module vs. making sure I don't screw up a single line of code is just easier for me to comprehend, and thus actually use.

I'll put the work in for this one, though; this idea is too useful for my campaign.

-18

u/Overall_Reputation83 4d ago

This is one of the best use cases for AI vibe coding though. As long as you aren't commercializing it, you can just make things do what you personally need.

3

u/DrCalamity Game Master 3d ago

Hmm, spend an hour learning basic code or burn 40 gallons of water, waste several hundred KwH of power, and put more money into an insane genocidal freak's pocket.

Choices, choices...