Skip to content

Instantly share code, notes, and snippets.

@player-03
Last active April 18, 2024 20:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save player-03/32ca7880ddd0cebf63d1fb81c8d3d58a to your computer and use it in GitHub Desktop.
Save player-03/32ca7880ddd0cebf63d1fb81c8d3d58a to your computer and use it in GitHub Desktop.
Dragon Form (PF2e)
//A spellcasting macro for PF2e in FoundryVTT. Casts the remastered Dragon Form
//spell, giving you both the battle form and the dragon breath action.
//Note: Sea, Sky, and Underworld dragons have burst-shaped dragon breath, which
//the spell does not account for. The macro assumes a 20-foot burst, but this is
//not supported by the rules, so ask your GM before picking these dragons.
//Options
//-------
//Delete lines from here if you don't want to see as many choices.
//Be careful not to mess up the formatting!
const dragons = {
adamantine: { tradition: "primal", damage: "bludgeoning", breath: "cone", speed: "burrow", save: "reflex" },
black: { tradition: "arcane", damage: "acid", breath: "line", speed: "swim", save: "reflex" },
blue: { tradition: "arcane", damage: "electricity", breath: "line", speed: "burrow", save: "reflex" },
brass: { tradition: "arcane", damage: "fire", breath: "line", speed: "burrow", save: "reflex" },
brine: { tradition: "primal", damage: "acid", breath: "line", speed: "swim", save: "reflex", rarity: "uncommon" },
bronze: { tradition: "arcane", damage: "electricity", breath: "line", speed: "swim", save: "reflex" },
cloud: { tradition: "primal", damage: "electricity", breath: "cone", save: "reflex", rarity: "uncommon" },
conspirator: { tradition: "occult", damage: "poison", breath: "cone", speed: "climb", save: "fortitude" },
copper: { tradition: "arcane", damage: "acid", breath: "line", save: "fortitude" },
crystal: { tradition: "primal", damage: "piercing", breath: "cone", speed: "burrow", save: "reflex", rarity: "uncommon" },
diabolic: { tradition: "divine", damage: "fire", breath: "cone", save: "reflex" },
empyreal: { tradition: "divine", damage: "spirit", breath: "cone", save: "reflex" },
forest: { tradition: "primal", damage: "piercing", breath: "cone", save: "reflex", rarity: "uncommon" },
fortune: { tradition: "arcane", damage: "force", breath: "cone", save: "reflex" },
gold: { tradition: "divine", damage: "fire", breath: "cone", speed: "swim", save: "reflex" },
green: { tradition: "arcane", damage: "poison", breath: "cone", speed: "swim", save: "fortitude" },
magma_0: { tradition: "primal", damage: "fire", breath: "cone", save: "reflex", rarity: "uncommon" },
magma_1: { tradition: "primal", damage: "bludgeoning", breath: "cone", save: "reflex", rarity: "uncommon" },
red: { tradition: "arcane", damage: "fire", breath: "cone", save: "reflex" },
sea: { tradition: "arcane", damage: "bludgeoning", breath: "burst", speed: "swim", save: "reflex", rarity: "uncommon" },
silver: { tradition: "divine", damage: "cold", breath: "cone", save: "reflex" },
sky: { tradition: "divine", damage: "electricity", breath: "burst", save: "reflex", rarity: "uncommon" },
sovereign: { tradition: "occult", damage: "mental", breath: "cone", save: "will", rarity: "uncommon" },
umbral: { tradition: "primal", damage: "void", breath: "cone", save: "reflex", rarity: "uncommon" },
underworld: { tradition: "arcane", damage: "fire", breath: "burst", speed: "burrow", save: "reflex", rarity: "uncommon" },
white: { tradition: "arcane", damage: "cold", breath: "cone", save: "reflex" },
};
//Don't modify anything below this line unless you know what you're doing.
//------------------------------------------------------------------------
const SLUG = "custom-spell-effect-dragon-form";
const DRAGON_BREATH_SLUG = "custom-action-dragon-breath";
const existingDragonBreath = actor.itemTypes.action.find((e) => e.system.slug === DRAGON_BREATH_SLUG)
if(existingDragonBreath) {
await existingDragonBreath.delete();
}
const existing = actor.itemTypes.effect.find((e) => e.system.slug === SLUG);
if(existing) {
await existing.delete();
return;
}
let spellTradition;
let spellHeightened;
[spellTradition, spellHeightened] = await new Promise((resolve) => {
function resolveFromHtml(tradition, html) {
resolve([
tradition,
html[0].getElementsByTagName("input")
?.["custom-macro-heightened-dragon-form"]?.checked
]);
}
new Dialog({
title: "Spell tradition",
content: '<p>How are you casting Dragon Form?</p> <label><input type="checkbox" id="custom-macro-heightened-dragon-form" value="heightened"> Heightened to rank 8+</label>',
buttons: {
arcane: {
label: "Arcane",
callback: (html) => resolveFromHtml("arcane", html)
},
divine: {
label: "Divine",
callback: (html) => resolveFromHtml("divine", html)
},
occult: {
label: "Occult",
callback: (html) => resolveFromHtml("occult", html)
},
primal: {
label: "Primal",
callback: (html) => resolveFromHtml("primal", html)
},
none: {
label: "N/A",
callback: (html) => resolveFromHtml("none", html)
}
},
close: () => resolve(null)
}).render(true);
});
if(!spellTradition) {
return;
}
//Build the "Dragon Form" effect.
const choice = await new Promise((resolve) => {
const optionArrays = {
arcane: [],
divine: [],
occult: [],
primal: []
};
for(let key in dragons) {
let name = key.substring(0, 1).toUpperCase() + key.substring(1);
if(name.includes("_")) {
name = name.substring(0, name.indexOf("_"));
}
const details = dragons[key];
details.dragon = name;
optionArrays[details.tradition]?.push(
`<option value="${ key }">${ name } [${ details.damage }]`
+ (details.rarity ? `[${ details.rarity }]` : "")
+ `</option>`);
}
let arcane = "";
if(optionArrays.arcane.length > 0) {
arcane = '<optgroup label="Arcane"> '
+ optionArrays.arcane.join(" ")
+ "</optgroup> ";
}
let divine = "";
if(optionArrays.divine.length > 0) {
divine = '<optgroup label="Divine"> '
+ optionArrays.divine.join(" ")
+ "</optgroup> ";
}
let occult = "";
if(optionArrays.occult.length > 0) {
occult = '<optgroup label="Occult"> '
+ optionArrays.occult.join(" ")
+ "</optgroup> ";
}
let primal = "";
if(optionArrays.primal.length > 0) {
primal = '<optgroup label="Primal"> '
+ optionArrays.primal.join(" ")
+ "</optgroup> ";
}
let optionsInOrder;
switch(spellTradition) {
case "divine":
optionsInOrder = divine + arcane + occult + primal;
break;
case "occult":
optionsInOrder = occult + arcane + divine + primal;
break;
case "primal":
optionsInOrder = primal + arcane + occult + divine;
break;
default:
optionsInOrder = arcane + occult + divine + primal;
}
const SELECT_ID = "custom-macro-select-dragon-form";
new Dialog({
title: "Dragon type",
content: `<label for="${ SELECT_ID }">What kind of dragon are you?</label> `
+ `<select id="${ SELECT_ID }"> `
+ optionsInOrder
+ "</select>",
buttons: {
apply: {
label: 'Be a dragon <i class="fa-solid fa-dragon fa-flip-horizontal"></i>',
icon: '<i class="fa-solid fa-dragon"></i>',
callback: (html) => {
const selection = html[0].getElementsByTagName("select")?.[SELECT_ID]
?.selectedOptions[0]?.value;
resolve(dragons[selection]);
}
}
},
default: "apply",
close: () => resolve(null)
}).render(true);
});
if(!choice) {
return;
}
let breathTemplate;
if(choice.breath.includes("line")) {
choice.breath = "100-foot line";
breathTemplate = "@Template[type:line|distance:100]";
} else if(choice.breath.includes("burst")) {
choice.breath = "20-foot burst within 50 feet";
breathTemplate = "@Template[type:burst|distance:20]";
} else {
choice.breath = "30-foot cone";
breathTemplate = "@Template[type:cone|distance:30]";
}
/**
* @param traits Optional, and "unarmed" will be added if you leave it out.
*/
function unarmedStrikeMod(attack, damage, traits) {
const result = {
modifier: attack,
damage: { modifier: damage }
};
if(traits) {
if(!traits.includes("unarmed")) { traits.push("unarmed"); }
result.traits = traits;
}
return result;
}
const speeds = { land: 40, fly: 100 };
if(choice.speed == "burrow") {
speeds.burrow = 20;
} else if(choice.speed == "climb") {
speeds.climb == 40;
} else if(choice.speed == "swim") {
speeds.swim = 60;
}
const rollOption = "self:effect:" + choice.dragon.toLowerCase() + "-dragon-form";
//Note: this isn't how you're supposed to add rule elements. Rather than following my example, you might want to get the rule-element-generator mod.
const rules = [{
key: "BattleForm",
overrides: {
hasHands: true,
resistances: [{ type: choice.damage, value: 10 }],
senses: {
darkvision: { },
scent: { acuity: "imprecise", range: 60 }
},
speeds: speeds,
strikes: {
claw: {
baseType: "claw",
category: "unarmed",
damage: { dice: 3, die: "d10", damageType: "slashing" },
img: "icons/creatures/claws/claw-scaled-red.webp",
traits: ["unarmed", "agile"]
},
jaws: {
baseType: "jaws",
category: "unarmed",
damage: { dice: 2, die: "d12", damageType: "piercing" },
img: "icons/creatures/abilities/mouth-teeth-fire-orange.webp",
traits: ["unarmed"]
},
tail: {
category: "unarmed",
damage: { dice: 3, die: "d10", damageType: "bludgeoning" },
img: "icons/creatures/abilities/tail-swipe-green.webp",
traits: ["unarmed", "reach-10"]
}
},
traits: ["dragon"]
},
value: {
brackets: [{
start: 6,
end: 7,
value: {
armorClass: { modifier: "18 + @actor.level" },
size: "lg",
skills: { ath: { modifier: 23 } },
strikes: {
claw: unarmedStrikeMod(22, 6),
jaws: unarmedStrikeMod(22, 6),
tail: unarmedStrikeMod(22, 6)
},
tempHP: 10
}
},
{
start: 8,
value: {
armorClass: { modifier: "21 + @actor.level" },
size: "huge",
skills: { ath: { modifier: 28 } },
strikes: {
claw: unarmedStrikeMod(28, 12, ["agile", "reach-10"]),
jaws: unarmedStrikeMod(28, 12, ["reach-10"]),
tail: unarmedStrikeMod(28, 12, ["reach-15"])
},
tempHP: 15
}
}],
field: "item|system.level.value"
}
},
{
key: "DamageDice",
diceNumber: 2,
dieSize: "d6",
damageType: choice.damage,
selector: "jaws-damage"
},
{
key: "FlatModifier",
value: {
brackets: [{ start: 8, value: 20 }],
field: "item|system.level.value"
},
type: "status",
selector: "fly-speed"
},
{
key: "RollOption",
domain: "all",
option: rollOption
}];
if(choice.tradition != spellTradition) {
choice.tradition = null;
}
switch(choice.tradition) {
case "arcane":
rules.push({
key: "Resistance",
type: "magical",
value: 5
});
break;
case "divine":
rules.push({
key: "Resistance",
type: "spirit",
value: 10
});
rules.push({
key: "Resistance",
type: "vitality",
value: 10
});
rules.push({
key: "Resistance",
type: "void",
value: 10
});
break;
case "occult":
rules.push({
key: "Resistance",
type: "mental",
value: 10
});
break;
case "primal":
rules.push({
key: "Resistance",
type: "physical",
value: 5
});
break;
default:
}
const item = {
stats: { },
effects: [],
flags: { core: { } },
img: "https://assets.forge-vtt.com/bazaar/core/icons/creatures/reptiles/dragon-horned-blue.webp",
name: "Spell Effect: Dragon Form (" + choice.dragon + ")",
ownership: { default: 0 },
permission: { default: 0 },
sort: 0,
system: {
description: {
gm: "",
value: "<p>@UUID[Compendium.pf2e.spells-srd.Item.5c692cCcTDXjSEzk]{Dragon Form}</p>"
},
duration: { value: 1, unit: "minutes", expiry: "turn-start", sustained: false },
level: { value: spellHeightened ? 8 : 6 },
publication: { title: "Pathfinder Player Core", license: "ORC", remaster: true, authors: "" },
rules: rules,
slug: SLUG,
start: { value: 0 },
tokenIcon: { show: true },
traits: { value: [], otherTags: [], rarity: "common" }
},
type: "effect"
};
//Build the "Dragon Breath" action. Sadly this can't be added via rule elements.
const dragonBreathItem = {
flags: { core: { } },
img: "https://assets.forge-vtt.com/bazaar/systems/pf2e/assets/icons/actions/TwoActions.webp",
name: "Dragon Breath",
ownership: { default: 0 },
parentCollection: "items",
system: {
actionType: { value: "action" },
category: "offensive",
description: {
gm: "",
value: "You exhale deadly magical energy, dealing @Damage[10d6[" + choice.damage + "]]{10d6 " + choice.damage + "} to each creature in a " + breathTemplate + ", with a @Check[type:" + choice.save + "|dc:resolve(@actor.attributes.spellDC.value)|basic:true] save. Once activated, Dragon Breath can't be used again for [[1d4]] rounds.</p>"
},
publication: item.system.publication,
rules: [],
slug: DRAGON_BREATH_SLUG,
traits: {
rarity: "common",
toggles: { },
value: [choice.tradition, choice.damage]
}
},
type: "action"
};
await actor.createEmbeddedDocuments("Item", [item, dragonBreathItem]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment