Last active
April 18, 2024 20:58
-
-
Save player-03/32ca7880ddd0cebf63d1fb81c8d3d58a to your computer and use it in GitHub Desktop.
Dragon Form (PF2e)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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