Last active
June 24, 2024 23:00
-
-
Save player-03/a3f7f8d73b6c7e95c75ebab625c88ce7 to your computer and use it in GitHub Desktop.
A macro to create improvised weapons for PF2e in FoundryVTT.
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 macro for PF2e in FoundryVTT that helps you create improvised weapons | |
//on the fly. It provides a menu of traits and damage types to pick from, | |
//and will create and equip the item once you're done. | |
//As usual, remember that your GM has final say on what an improvised | |
//weapon does. Work with them to figure out what statistics make sense. | |
//In particular, it won't always make sense to increase the weapon's | |
//damage die, even though the macro allows it. | |
//Options | |
//======= | |
//What do you want to call your weapons by default? | |
const DEFAULT_WEAPON_NAME = "Improvised Weapon"; | |
//How large of an item does your character usually wield? Enter "med" for | |
//medium or "lg" if you are a giant instinct barbarian. | |
const DEFAULT_WEAPON_SIZE = "med"; | |
//What damage type should be selected by default? Must be "bludgeoning", | |
//"piercing", or "slashing". | |
const DEFAULT_DAMAGE_TYPE = "bludgeoning"; | |
//An approximate measure of how strong your improvised weapons are. 2 means | |
//they'll be about as strong as a simple weapon. 3 would put them between | |
//simple and martial weapons, and 4 would put them on par with many martial | |
//weapons. Get your GM's permission before changing this! | |
const IMPROVEMENTS = 2; | |
//Preparation | |
//=========== | |
//Don't touch anything from here on, unless you know what you're doing. | |
const WINDOW_TITLE = "Improvise a Weapon"; | |
for(const existingDialog of document.getElementsByClassName("dialog")) { | |
if(existingDialog.getElementsByClassName("window-title")[0].textContent === WINDOW_TITLE) { | |
return; | |
} | |
} | |
let defaultWeaponName = DEFAULT_WEAPON_NAME; | |
const existingWeaponIDs = []; | |
for(let item of actor.inventory) { | |
if(item.system.traits.otherTags.includes("improvised") && item.system.quantity > 0 | |
&& item.system.equipped.carryType === "held") { | |
existingWeaponIDs.push(item.id); | |
} | |
} | |
//Set up the base weapon. This will be filled out later, in `collectResults()`. | |
let weapon = { | |
flags: { | |
core: { }, | |
pf2e: { } | |
}, | |
img: "systems/pf2e/icons/unidentified_item_icons/adventuring_gear.webp", | |
name: DEFAULT_WEAPON_NAME, | |
ownership: { default: 0 }, | |
parent: actor, | |
permission: { default: 0 }, | |
sort: 0, | |
system: { | |
bonus: { value: 0 }, | |
bonusDamage: { value: 0 }, | |
bulk: { | |
per: 1, | |
value: 0.1 | |
}, | |
category: "simple", | |
damage: { | |
damageType: DEFAULT_DAMAGE_TYPE, | |
dice: 1, | |
die: "d4", | |
modifier: 0, | |
persistent: null | |
}, | |
description: { | |
gm: "", | |
value: "" | |
}, | |
hardness: 5, | |
hp: { value: 20, max: 20, brokenThreshold: 10 }, | |
identification: { status: "identified" }, | |
level: { value: 0 }, | |
publication: { title: "Pathfinder Player Core", license: "ORC", remaster: true, authors: "" }, | |
quantity: 1, | |
rules: [], | |
runes: { potency: 0, striking: 0, property: [] }, | |
size: DEFAULT_WEAPON_SIZE, | |
slug: null, | |
splashDamage: { value: 0 }, | |
traits: { | |
value: [], | |
otherTags: ["improvised"], | |
rarity: "common" | |
}, | |
usage: { | |
hands: 1, | |
type: "held", | |
value: "held-in-one-hand" | |
} | |
}, | |
type: "weapon" | |
}; | |
//Dialog window | |
//============= | |
let [ableToContinue, existingWeapon, handsHeld, damageDie] = await new Promise((resolve) => { | |
//CSS | |
//=== | |
const VALUE_NEW_WEAPON = "new-weapon"; | |
let content = "<style>\n" | |
//Checkboxes are too large by default, for how many we use. | |
+ "input[type=checkbox] { width: unset; height: unset; }\n" | |
//Text boxes are too long by default. | |
+ "input[type=text] { width: unset; }\n" | |
//Try to prevent line breaks right after a checkbox. | |
+ ".nowrap { white-space: nowrap; }\n" | |
//Some options depend on others, and should only show up once the | |
//dependency is chosen. | |
+ "input:not(:checked) ~ .requiresCheck { display: none; }\n" | |
+ "input:checked ~ .requiresNoCheck { display: none; }\n" | |
+ "</style>\n"; | |
//Weapon fundamentals | |
//=================== | |
//Existing weapon selection | |
//------------------------- | |
const CLASS_WEAPON_SELECTION = "improvised-weapon-selection"; | |
const CLASS_WEAPON_NAME = "improvised-weapon-name"; | |
const CLASS_WEAPON_IMAGE = "improvised-weapon-image"; | |
const WEAPON_NAME_INPUT = `<image class="${ CLASS_WEAPON_IMAGE }" style="max-height: 2em; vertical-align: middle;" src="${ weapon.img }"> <input type="text" class="${ CLASS_WEAPON_NAME }" placeholder="${ DEFAULT_WEAPON_NAME }" value="${ DEFAULT_WEAPON_NAME }">`; | |
content += '<div style="max-height: 200px; overflow: scroll;">'; | |
if(existingWeaponIDs.length > 0) { | |
content += `Interacting to:<br>`; | |
for(const weaponID of existingWeaponIDs) { | |
const weapon = actor.inventory.get(weaponID); | |
content += `<label><input type="radio" class="${ CLASS_WEAPON_SELECTION }" name="${ CLASS_WEAPON_SELECTION }" value="${ weaponID }"${ weaponID === existingWeaponIDs[0] ? 'checked="true"' : "" }> ` | |
+ `change grip on <image style="max-height: 1.5em; vertical-align: middle;" src="${ weapon.img }"> <strong>${ weapon.name }</strong></label><br>`; | |
} | |
content += `<input type="radio" id="pick-up-new-weapon" class="${ CLASS_WEAPON_SELECTION }" name="${ CLASS_WEAPON_SELECTION }" value="${ VALUE_NEW_WEAPON }">` | |
+ '<label for="pick-up-new-weapon"> pick up a new ' | |
+ WEAPON_NAME_INPUT | |
+ "</label>\n"; | |
content += '<div class="requiresCheck">'; | |
} else { | |
content += "Picking up a(n) " + WEAPON_NAME_INPUT; | |
} | |
//Size selection | |
//-------------- | |
const CLASS_SIZE = "weapon-size"; | |
const VALUE_LARGE = "large"; | |
content += `<p><label>Oversize weapon? <input type="checkbox"${ DEFAULT_WEAPON_SIZE === "lg" ? ' checked="true"' : "" } class="${ CLASS_SIZE }" value="${ VALUE_LARGE }"></label></p>\n`; | |
//Damage type selection | |
//--------------------- | |
const CLASS_DAMAGE_TYPE = "damage-type"; | |
content += "<p>Base damage: " | |
for(const type of ["bludgeoning", "piercing", "slashing"]) { | |
content += `<label><input type="radio" class="${ CLASS_DAMAGE_TYPE }" name="${ CLASS_DAMAGE_TYPE }" value="${ type }"${ type === DEFAULT_DAMAGE_TYPE ? ' checked="true"' : "" }> ${ type.charAt(0).toUpperCase() + type.substring(1) }</label>`; | |
} | |
content += "</p></div>\n" | |
//Clean up after `<div class="requiresChecked">`. | |
if(existingWeaponIDs.length > 0) { | |
content += "</div>\n"; | |
} | |
//Improvement selection | |
//===================== | |
const CLASS_IMPROVEMENTS_HEADER = "dialog-select-improvements-header"; | |
content += `<div><h2 class="${ CLASS_IMPROVEMENTS_HEADER }">Improvements</h2>\n`; | |
const CLASS_IMPROVEMENT = "improvement"; | |
function improvementCheckbox(value, label, childNode) { | |
return '<label class="nowrap">' | |
+ `<input type="checkbox" class="${ CLASS_IMPROVEMENT }" value="${ value }">` | |
+ `<span class="tag">${ label }</span>${ childNode ?? "" }</label>`; | |
} | |
//Option 1 | |
//-------- | |
content += "<p><strong>Increase base damage:</strong> " | |
+ improvementCheckbox("d6", "d6 (2-hand d8)", | |
improvementCheckbox("d8", "d8 (2-hand d10)").replace('class="', 'class="requiresCheck ')) | |
+ "</p><hr>\n"; | |
//Option 2 | |
//-------- | |
content += '<div style="max-height: 40px; overflow: scroll;">\n<strong>Add bonus damage:</strong> ' | |
const damageTypeOptions = ["acid", "cold", "electricity", "fire", "mental", "poison", "sonic", "spirit", "vitality", "void"]; | |
for(const damageType of damageTypeOptions) { | |
const damageTypeCapitalized = damageType.charAt(0).toUpperCase() + damageType.substring(1); | |
content += improvementCheckbox(damageType, damageTypeCapitalized) | |
+ "\n"; | |
} | |
content += "</div><hr>\n"; | |
//Option 3 | |
//-------- | |
const availableTraits = [ | |
"Agile", | |
"Backstabber", | |
"Backswing", | |
"Brace", | |
"Deadly", | |
"Disarm", | |
"Finesse", | |
"Forceful", | |
"Grapple", | |
"Nonlethal", | |
"Parry", | |
"Razing", | |
"Reach", | |
"Shove", | |
"Sweep", | |
"Trip", | |
"Versatile B", | |
"Versatile P", | |
"Versatile S", | |
//Thrown gets special treatment due to how many traits depend on it. | |
//This also makes it a good choice to use as a divider. | |
"Thrown 10", | |
//Less-useful traits go at the end, after the divider. | |
"Climbing", | |
"Concealable", | |
"Free-Hand", | |
"Hampering" | |
]; | |
const thrownTraits = [ | |
"Ranged Trip", | |
"Recovery", | |
"Tethered" | |
]; | |
content += '<div class="tags" data-tooltip-class="pf2e" style="max-height: 100px; overflow: scroll;">\n<p><strong>Add traits:</strong>' | |
for(const trait of availableTraits) { | |
const slug = trait.toLowerCase().replace(/ /g, "-"); | |
function tagCheckbox(slug, trait) { | |
const traitTooltip = "PF2E.TraitDescription" + trait.replace(/[ \-]/g, "") | |
.replace(/Versatile[A-Z]/, "Versatile").replace(/Thrown\d+/, "Thrown"); | |
return improvementCheckbox(slug, trait) | |
.replace('class="nowrap"', `class="nowrap" data-trait="${ slug }" data-tooltip="${ traitTooltip }"`); | |
} | |
if(slug.startsWith("thrown")) { | |
content += '</p><p>\n'; | |
content += `<input type="checkbox" class="improvement" id="improvement-${ slug }" value="${ slug }">`; | |
content += `<label class="tag" data-trait="thrown-10" data-tooltip="PF2E.TraitDescriptionThrown" for="improvement-${ slug }">Thrown</label><label for="improvement-${ slug }" class="requiresNoCheck"> …</label> `; | |
content += '<span class="requiresCheck">' | |
for(let trait2 of thrownTraits) { | |
const slug2 = trait2.toLowerCase().replace(" ", "-"); | |
content += tagCheckbox(slug2, trait2) + " "; | |
} | |
content += "</span></p><p>\n"; | |
continue; | |
} | |
content += tagCheckbox(slug, trait) + "\n"; | |
} | |
content += "</p></div>\n"; //TODO: add <hr> if there's a fourth section. | |
content += "</div>"; | |
//Additional scripts | |
//================== | |
//A script to insert into the dialog window as a `<script>` tag. The whole | |
//function will be converted to a string. It could be written as a string in | |
//the first place, but that would make it harder to read. | |
function scriptFunction() { | |
const dialog = Array.from(document.getElementsByClassName("dialog")).find( | |
(d) => d.getElementsByClassName("window-title")[0]?.textContent === WINDOW_TITLE); | |
if(!dialog) { | |
return; | |
} | |
const weaponImage = dialog.getElementsByClassName(CLASS_WEAPON_IMAGE)[0]; | |
const weaponSize = dialog.getElementsByClassName(CLASS_SIZE)[0]; | |
const damageTypes = Array.from(dialog.getElementsByClassName(CLASS_DAMAGE_TYPE)); | |
const weaponSelection = dialog.getElementsByClassName(CLASS_WEAPON_SELECTION); | |
const improvements = dialog.getElementsByClassName(CLASS_IMPROVEMENT); | |
const improvementHeader = dialog.getElementsByClassName(CLASS_IMPROVEMENTS_HEADER)[0]; | |
const confirmButtons = dialog.getElementsByClassName("dialog-button"); | |
let existingWeaponSelected = false; | |
//We'll need to look up checkboxes by value pretty often. | |
const improvementMap = { }; | |
for(const improvement of improvements) { | |
improvementMap[improvement.value] = improvement; | |
} | |
//Weapon selection | |
//================ | |
if(weaponSelection.length > 0) { | |
const inventory = game.actors.get(ACTOR_ID).inventory; | |
function onWeaponSelectionChange(changedButton) { | |
//This function is called twice per click, since two buttons | |
//change at once. We only care about the newly-checked button. | |
if(!changedButton.checked) { | |
return; | |
} | |
//Reset all checkboxes. | |
weaponSize.checked = DEFAULT_WEAPON_SIZE === "lg"; | |
for(const damageType of damageTypes) { | |
damageType.checked = damageType.value === DEFAULT_DAMAGE_TYPE; | |
} | |
for(const improvementCheckbox of improvements) { | |
improvementCheckbox.checked = false; | |
} | |
//Fill in checkboxes to match the selected weapon, if any. | |
const existingWeapon = inventory.get(changedButton.value); | |
existingWeaponSelected = !!existingWeapon; | |
if(existingWeapon) { | |
weaponSize.checked = ["lg", "huge", "grg"].includes(existingWeapon.system.size); | |
const damageDie = parseInt(existingWeapon.system.damage.die.substring(1)) | |
- (existingWeapon.system.equipped.handsHeld >= 2 ? 2 : 0); | |
if(damageDie >= 6) { | |
improvementMap["d6"].checked = true; | |
if(damageDie >= 8) { | |
improvementMap["d8"].checked = true; | |
} | |
} | |
(damageTypes.find((d) => d.value === existingWeapon.system.damage.damageType) ?? damageTypes[0]) | |
.checked = true; | |
for(const rule of existingWeapon.system.rules) { | |
if(rule.key === "FlatModifier" && rule.value === 1 && rule.selector?.[0] === "{item|_id}-damage") { | |
improvementMap[rule.damageType].checked = true; | |
} | |
} | |
for(let trait of existingWeapon.system.traits.value) { | |
if(trait.startsWith("deadly")) { | |
trait = "deadly"; | |
} | |
if(improvementMap[trait]) { | |
improvementMap[trait].checked = true; | |
} | |
} | |
} | |
//Update the remaining improvements count. | |
onImprovementChange(); | |
} | |
for(const weaponButton of weaponSelection) { | |
weaponButton.onchange = onWeaponSelectionChange.bind(this, weaponButton); | |
} | |
onWeaponSelectionChange(Array.from(weaponSelection).find((w) => w.checked)); | |
} | |
//Improvements | |
//============ | |
function onImprovementChange() { | |
let remaining = IMPROVEMENTS; | |
for(const improvement of improvements) { | |
if(improvement.checked && improvement.offsetParent) { | |
remaining--; | |
} | |
} | |
const s = remaining === 1 || remaining === -1 ? "" : "s"; | |
if(remaining < 0) { | |
improvementHeader.textContent = `Remove ${ -remaining } improvement${ s }`; | |
} else { | |
improvementHeader.textContent = `Pick ${ remaining } improvement${ s }`; | |
} | |
if(remaining === 0) { | |
for(const button of confirmButtons) { | |
button.removeAttribute("style"); | |
} | |
} else { | |
for(const button of confirmButtons) { | |
button.setAttribute("style", "opacity: 0.3;"); | |
} | |
} | |
updateImage(); | |
} | |
for(const improvement of improvements) { | |
improvement.onchange = onImprovementChange; | |
} | |
//Preview image | |
//============= | |
function updateImage() { | |
//Ignore d8 unless d6 is checked. | |
const damageDie = improvementMap["d6"].checked ? (improvementMap["d8"].checked ? 8 : 6) : 4; | |
const damageType = damageTypes.find((d) => d.checked)?.value ?? DEFAULT_DAMAGE_TYPE; | |
if(existingWeaponSelected) { | |
//The current selections wouldn't apply. | |
} else if(weaponSize.checked) { | |
switch(damageType) { | |
case "bludgeoning": | |
weaponImage.setAttribute("src", damageDie > 6 | |
? "systems/pf2e/icons/equipment/held-items/wheelbarrow.webp" | |
: damageDie > 4 | |
? "systems/pf2e/icons/equipment/adventuring-gear/ladder.webp" | |
: "systems/pf2e/icons/equipment/adventuring-gear/chain.webp"); | |
break; | |
case "piercing": | |
weaponImage.setAttribute("src", damageDie > 6 | |
? "systems/pf2e/icons/equipment/held-items/wand-of-refracting-rays.webp" | |
: damageDie > 4 | |
? "systems/pf2e/icons/equipment/other/attached-items/tripod.webp" | |
: "systems/pf2e/icons/equipment/adventuring-gear/long-tool.webp"); | |
break; | |
case "slashing": | |
weaponImage.setAttribute("src", damageDie > 6 | |
? "systems/pf2e/icons/equipment/snares/bleeding-spines-snare.webp" | |
: damageDie > 4 | |
? "systems/pf2e/icons/equipment/held-items/magnetic-construction-set.webp" | |
: "systems/pf2e/icons/equipment/held-items/basic-crutch.webp"); | |
} | |
} else { | |
switch(damageType) { | |
case "bludgeoning": | |
weaponImage.setAttribute("src", damageDie > 6 | |
? "systems/pf2e/icons/equipment/adventuring-gear/hammer.webp" | |
: damageDie > 4 | |
? "systems/pf2e/icons/equipment/adventuring-gear/crowbar.webp" | |
: "systems/pf2e/icons/equipment/adventuring-gear/mug.webp"); | |
break; | |
case "piercing": | |
weaponImage.setAttribute("src", damageDie > 6 | |
? "systems/pf2e/icons/equipment/adventuring-gear/grapling-hook.webp" | |
: damageDie > 4 | |
? "systems/pf2e/icons/equipment/snares/spike-snare.webp" | |
: "systems/pf2e/icons/equipment/held-items/tritons-conch.webp"); | |
break; | |
case "slashing": | |
weaponImage.setAttribute("src", damageDie > 6 | |
? "systems/pf2e/icons/equipment/adventuring-gear/artisan-tools.webp" | |
: damageDie > 4 | |
? "systems/pf2e/icons/equipment/adventuring-gear/piton.webp" | |
: "systems/pf2e/icons/equipment/worn-items/other-worn-items/talisman-cord.webp"); | |
} | |
} | |
} | |
weaponSize.onchange = updateImage; | |
for(const damageType of damageTypes) { | |
damageType.onchange = () => { | |
if(damageType.checked) { | |
updateImage(); | |
} | |
}; | |
} | |
//Initialization | |
//============== | |
if(weaponSelection.length === 0) { | |
//If there's an existing weapon, `onWeaponSelectionChange()` will | |
//have already run `onImprovementChange()`. | |
onImprovementChange(); | |
} | |
} | |
const script = scriptFunction.toString(); | |
content += "<script>(function scriptFunction() {\n" | |
//Insert constants we'll need. | |
+ `\tconst IMPROVEMENTS = ${ IMPROVEMENTS };\n` //Not a string. | |
+ `\tconst DEFAULT_DAMAGE_TYPE = "${ DEFAULT_DAMAGE_TYPE }";\n` | |
+ `\tconst DEFAULT_WEAPON_SIZE = "${ DEFAULT_WEAPON_SIZE }";\n` | |
+ `\tconst WINDOW_TITLE = "${ WINDOW_TITLE }";\n` | |
+ `\tconst CLASS_WEAPON_IMAGE = "${ CLASS_WEAPON_IMAGE }";\n` | |
+ `\tconst CLASS_SIZE = "${ CLASS_SIZE }";\n` | |
+ `\tconst CLASS_DAMAGE_TYPE = "${ CLASS_DAMAGE_TYPE }";\n` | |
+ `\tconst CLASS_WEAPON_SELECTION = "${ CLASS_WEAPON_SELECTION }";\n` | |
+ `\tconst CLASS_IMPROVEMENT = "${ CLASS_IMPROVEMENT }";\n` | |
+ `\tconst CLASS_IMPROVEMENTS_HEADER = "${ CLASS_IMPROVEMENTS_HEADER }";\n` | |
+ `\tconst ACTOR_ID = "${ actor.id }";\n` | |
//Insert the body of `scriptFunction`. | |
+ script.substring( | |
script.indexOf("{") + 1, | |
script.lastIndexOf("}")) + "\n" | |
+ "})()</script>\n"; | |
//Result collection | |
//================= | |
function collectResults(hands, html) { | |
//Check which weapon was selected, if any. | |
let existingWeapon = null; | |
for(const weaponButton of html[0].getElementsByClassName(CLASS_WEAPON_SELECTION)) { | |
if(weaponButton.checked) { | |
existingWeapon = actor.inventory.get(weaponButton.value); | |
break; | |
} | |
} | |
//Get the weapon's base attributes. | |
weapon.name = html[0].getElementsByClassName(CLASS_WEAPON_NAME)[0]?.value ?? DEFAULT_WEAPON_NAME; | |
weapon.system.size = html[0].getElementsByClassName(CLASS_SIZE)[0].checked ? "lg" : "med"; | |
for(const damageTypeButton of html[0].getElementsByClassName(CLASS_DAMAGE_TYPE)) { | |
if(damageTypeButton.checked) { | |
weapon.system.damage.damageType = damageTypeButton.value; | |
break; | |
} | |
} | |
if(existingWeapon) { | |
weapon.img = existingWeapon.img; | |
weapon.system.hp.value = existingWeapon.system.hp.value; | |
weapon.system.hp.max = existingWeapon.system.hp.max; | |
weapon.system.hp.brokenThreshold = existingWeapon.system.hp.brokenThreshold; | |
} else { | |
weapon.img = html[0].getElementsByClassName(CLASS_WEAPON_IMAGE)[0]?.getAttribute("src") ?? weapon.img; | |
if(weapon.system.size !== "med") { | |
weapon.system.hp.value = 40; | |
weapon.system.hp.max = 40; | |
weapon.system.hp.brokenThreshold = 20; | |
} | |
} | |
//Find the improvements. | |
let damageDie = 4; | |
for(const improvement of html[0].getElementsByClassName(CLASS_IMPROVEMENT)) { | |
if(!improvement.checked || !improvement.offsetParent) { | |
continue; | |
} | |
if(improvement.value === "d6" || improvement.value === "d8") { | |
if(damageDie < 8) { | |
damageDie += 2; | |
} | |
} else if(damageTypeOptions.includes(improvement.value)) { | |
weapon.system.rules.push({ | |
"damageType": improvement.value, | |
"fromEqiupment": true, | |
"key": "FlatModifier", | |
"label": "Bonus Damage", | |
"selector": "{item|_id}-damage", | |
"value": 1 | |
}); | |
} else { | |
weapon.system.traits.value.push(improvement.value); | |
} | |
} | |
//Any changes relying on `damageDie` should wait until after the loop. | |
weapon.system.damage.die = "d" + damageDie; | |
weapon.system.traits.value.push("two-hand-d" + (damageDie + 2)); | |
weapon.system.bulk.value = damageDie > 4 ? 1 : 0.1; | |
if(weapon.system.traits.value.includes("deadly")) { | |
weapon.system.traits.value[weapon.system.traits.value.indexOf("deadly")] | |
= "deadly-d" + (damageDie + 2); | |
} | |
resolve([true, existingWeapon, hands, damageDie]); | |
} | |
//The window itself | |
//================= | |
new Dialog({ | |
title: WINDOW_TITLE, | |
content: content, | |
buttons: { | |
onehand: { | |
//Use `<i></i>` syntax to avoid nesting. `<i />` does not work. | |
label: '<i class="fa-solid fa-wrench fa-flip-horizontal"></i> ' | |
+ '<strong>Grab 1-handed</strong>' | |
+ ' <i class="fa-solid fa-wine-bottle fa-flip-horizontal fa-flip-vertical"></i>', | |
callback: collectResults.bind(this, 1) | |
}, | |
twohand: { | |
label: '<i class="fa-solid fa-guitar fa-flip-vertical"></i> ' | |
+ '<strong>Grab 2-handed</strong>' | |
+ ' <i class="fa-solid fa-baseball-bat-ball"></i>', | |
callback: collectResults.bind(this, 2) | |
} | |
}, | |
default: "confirm", | |
close: () => resolve([false]) | |
}).render(true); | |
}); | |
if(!ableToContinue) { | |
return; | |
} | |
//Granting the weapon | |
//=================== | |
//Delete the previous weapon to allow replacing it. | |
if(existingWeapon) { | |
await existingWeapon.delete(); | |
} | |
//Create the weapon. | |
weapon = (await actor.createEmbeddedDocuments("Item", [weapon]))[0]; | |
//Grip it with the correct number of hands. | |
weapon.update({ | |
"system.equipped.carryType": "held", | |
"system.equipped.handsHeld": handsHeld | |
}); | |
//Confirmation message | |
//==================== | |
let posessivePronoun; | |
switch(/[a-z]+/.exec(actor.system.details.gender.value?.toLowerCase() ?? "")[0]) { | |
case "he": | |
case "him": | |
case "male": | |
case "masc": | |
case "masculine": | |
case "boy": | |
case "man": | |
posessivePronoun = "his"; | |
break; | |
case "she": | |
case "her": | |
case "female": | |
case "fem": | |
case "feminine": | |
case "girl": | |
case "woman": | |
posessivePronoun = "her"; | |
break; | |
default: | |
posessivePronoun = "their"; | |
} | |
function joinList(list) { | |
if(list.length >= 2) { | |
list.push("and " + list.pop()); | |
} | |
if(list.length >= 3) { | |
return list.join(", "); | |
} else { | |
return list.join(" "); | |
} | |
} | |
let weaponDescription = existingWeapon | |
? `This weapon now deals ` | |
: `This ${ weapon.system.size === "lg" ? "oversize " : "" }weapon deals `; | |
const weaponDamageList = [ | |
`${ weapon.system.damage.dice }d${ damageDie + (handsHeld > 1 ? 2 : 0) } ${ weapon.system.damage.damageType }` | |
]; | |
for(const rule of weapon.system.rules) { | |
if(rule.key === "FlatModifier" && rule.selector?.[0] === "{item|_id}-damage") { | |
weaponDamageList.push(rule.value + " " + rule.damageType); | |
} | |
} | |
weaponDescription += joinList(weaponDamageList) + " damage"; | |
const weaponTraitList = weapon.system.traits.value.filter((t) => !t.startsWith("two-hand")) | |
.map((trait) => { | |
const traitWords = trait.split("-"); | |
for(let i = 0; i < traitWords.length; i++) { | |
traitWords[i] = traitWords[i].charAt(0).toUpperCase() + traitWords[i].substring(1); | |
} | |
const traitForDisplay = traitWords.join(" "); | |
if(/-(?:\d+|d\d+|[bps])$/.test(trait)) { | |
traitWords.pop(); | |
} | |
const traitTooltip = "PF2E.TraitDescription" + traitWords.join(""); | |
return `<span data-trait="${ trait }" data-tooltip="${ traitTooltip }">${ traitForDisplay }</span>`; | |
}); | |
if(weaponTraitList.length > 0) { | |
weaponDescription += ', and has the <span data-tooltip-class="pf2e">' | |
+ joinList(weaponTraitList) | |
+ "</span> trait" + (weaponTraitList.length > 1 ? "s" : ""); | |
} | |
weaponDescription += "."; | |
const weaponName = weapon.name === DEFAULT_WEAPON_NAME | |
? DEFAULT_WEAPON_NAME.toLowerCase() | |
: weapon.name.toLowerCase().replace("improvised ", ""); | |
const traitsExceptTwoHand = weapon.system.traits.value.filter((t) => !t.startsWith("two-hand")); | |
await ChatMessage.create({ | |
type: 3, | |
user: game.user._id, | |
speaker: ChatMessage.getSpeaker({ token: actor }), | |
flavor: '<h4 class="action"><strong>Improvise a weapon</strong> <span class="action-glyph">1</span></h4>' | |
+ '<div class="tags paizo-style" data-tooltip-class="pf2e"><span class="tag" data-slug="manipulate" data-tooltip="PF2E.TraitDescriptionManipulate">Manipulate</span></div>' | |
+ '<hr class="action-divider">', | |
content: '<p class="action-content">' | |
+ `<img src="${ weapon.img }">` | |
+ "<span>" | |
+ (existingWeapon | |
? `${ actor.name } shifts ${ posessivePronoun } grip on ${ posessivePronoun } ${ weaponName }, holding it in ${ handsHeld > 1 ? "both hands" : "one hand" }. ` | |
: `${ actor.name } picks up ${ /^[aeiou]/i.test(weaponName) ? "an" : "a" } ${ weaponName } in ${ handsHeld > 1 ? "both hands" : "one hand" }. `) | |
+ "</span></p>" | |
+ `<span>${ weaponDescription }</span>` | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment