Skip to content

Instantly share code, notes, and snippets.

@player-03
Last active June 24, 2024 23:00
Show Gist options
  • Save player-03/a3f7f8d73b6c7e95c75ebab625c88ce7 to your computer and use it in GitHub Desktop.
Save player-03/a3f7f8d73b6c7e95c75ebab625c88ce7 to your computer and use it in GitHub Desktop.
A macro to create improvised weapons for PF2e in FoundryVTT.
//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