foundryacks/6.2/module/item/entity.js

420 lines
12 KiB
JavaScript

import { AcksDice } from "../dice.js";
/**
* Override and extend the basic :class:`Item` implementation
*/
export class AcksItem extends Item {
/* -------------------------------------------- */
/* Data Preparation */
/* -------------------------------------------- */
/**
* Augment the basic Item data model with additional dynamic data.
*/
prepareData() {
// Set default image
let img = CONST.DEFAULT_TOKEN;
switch (this.data.type) {
case "spell":
img = "/systems/acks/assets/default/spell.png";
break;
case "ability":
img = "/systems/acks/assets/default/ability.png";
break;
case "armor":
img = "/systems/acks/assets/default/armor.png";
break;
case "weapon":
img = "/systems/acks/assets/default/weapon.png";
break;
case "item":
img = "/systems/acks/assets/default/item.png";
break;
}
if (!this.data.img) this.data.img = img;
super.prepareData();
}
static chatListeners(html) {
html.on("click", ".card-buttons button", this._onChatCardAction.bind(this));
html.on("click", ".item-name", this._onChatCardToggleContent.bind(this));
}
getChatData(htmlOptions) {
const data = duplicate(this.data.data);
// Rich text description
data.description = TextEditor.enrichHTML(data.description, htmlOptions);
// Item properties
const props = [];
const labels = this.labels;
if (this.data.type == "weapon") {
data.tags.forEach(t => props.push(t.value));
}
if (this.data.type == "spell") {
props.push(`${data.class} ${data.lvl}`, data.range, data.duration);
}
if (data.hasOwnProperty("equipped")) {
props.push(data.equipped ? "Equipped" : "Not Equipped");
}
// Filter properties and return
data.properties = props.filter((p) => !!p);
return data;
}
rollWeapon(options = {}) {
let isNPC = this.actor.data.type != "character";
const targets = 5;
const data = this.data.data;
let type = isNPC ? "attack" : "melee";
const rollData =
{
item: this.data,
actor: this.actor.data,
roll: {
save: this.data.data.save,
target: null
}
};
if (data.missile && data.melee && !isNPC) {
// Dialog
new Dialog({
title: "Choose Attack Range",
content: "",
buttons: {
melee: {
icon: '<i class="fas fa-fist-raised"></i>',
label: "Melee",
callback: () => {
this.actor.targetAttack(rollData, "melee", options);
},
},
missile: {
icon: '<i class="fas fa-bullseye"></i>',
label: "Missile",
callback: () => {
this.actor.targetAttack(rollData, "missile", options);
},
},
},
default: "melee",
}).render(true);
return true;
} else if (data.missile && !isNPC) {
type = "missile";
}
this.actor.targetAttack(rollData, type, options);
return true;
}
async rollFormula(options = {}) {
const data = this.data.data;
if (!data.roll) {
throw new Error("This Item does not have a formula to roll!");
}
const label = `${this.name}`;
const rollParts = [data.roll];
let type = data.rollType;
const newData = {
actor: this.actor.data,
item: this.data,
roll: {
type: type,
target: data.rollTarget,
blindroll: data.blindroll,
},
};
// Roll and return
return AcksDice.Roll({
event: options.event,
parts: rollParts,
data: newData,
skipDialog: true,
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.format("ACKS.roll.formula", { label: label }),
title: game.i18n.format("ACKS.roll.formula", { label: label }),
});
}
spendSpell() {
this.update({
data: {
cast: this.data.data.cast + 1,
},
}).then(() => {
this.show({ skipDialog: true });
});
}
getTags() {
let formatTag = (tag, icon) => {
if (!tag) return "";
let fa = "";
if (icon) {
fa = `<i class="fas ${icon}"></i> `;
}
return `<li class='tag'>${fa}${tag}</li>`;
};
const data = this.data.data;
switch (this.data.type) {
case "weapon":
let wTags = formatTag(data.damage, "fa-tint");
data.tags.forEach((t) => {
wTags += formatTag(t.value);
});
wTags += formatTag(CONFIG.ACKS.saves_long[data.save], "fa-skull");
if (data.missile) {
wTags += formatTag(
data.range.short + "/" + data.range.medium + "/" + data.range.long,
"fa-bullseye"
);
}
return wTags;
case "armor":
return `${formatTag(CONFIG.ACKS.armor[data.type], "fa-tshirt")}`;
case "item":
return "";
case "spell":
let sTags = `${formatTag(data.class)}${formatTag(
data.range
)}${formatTag(data.duration)}${formatTag(data.roll)}`;
if (data.save) {
sTags += formatTag(CONFIG.ACKS.saves_long[data.save], "fa-skull");
}
return sTags;
case "ability":
let roll = "";
roll += data.roll ? data.roll : "";
roll += data.rollTarget ? CONFIG.ACKS.roll_type[data.rollType] : "";
roll += data.rollTarget ? data.rollTarget : "";
return `${formatTag(data.requirements)}${formatTag(roll)}`;
}
return "";
}
pushTag(values) {
const data = this.data.data;
let update = [];
if (data.tags) {
update = duplicate(data.tags);
}
let newData = {};
var regExp = /\(([^)]+)\)/;
if (update) {
values.forEach((val) => {
// Catch infos in brackets
var matches = regExp.exec(val);
let title = "";
if (matches) {
title = matches[1];
val = val.substring(0, matches.index).trim();
} else {
val = val.trim();
title = val;
}
// Auto fill checkboxes
switch (val) {
case CONFIG.ACKS.tags.melee:
newData.melee = true;
break;
case CONFIG.ACKS.tags.slow:
newData.slow = true;
break;
case CONFIG.ACKS.tags.missile:
newData.missile = true;
break;
}
update.push({ title: title, value: val });
});
} else {
update = values;
}
newData.tags = update;
return this.update({ data: newData });
}
popTag(value) {
const data = this.data.data;
let update = data.tags.filter((el) => el.value != value);
let newData = {
tags: update,
};
return this.update({ data: newData });
}
roll() {
switch (this.type) {
case "weapon":
this.rollWeapon();
break;
case "spell":
this.spendSpell();
break;
case "ability":
if (this.data.data.roll) {
this.rollFormula();
} else {
this.show();
}
break;
case "item":
case "armor":
this.show();
}
}
/**
* Show the item to Chat, creating a chat card which contains follow up attack or damage roll options
* @return {Promise}
*/
async show() {
// Basic template rendering data
const token = this.actor.token;
const templateData = {
actor: this.actor,
tokenId: token ? `${token.scene._id}.${token.id}` : null,
item: this.data,
data: this.getChatData(),
labels: this.labels,
isHealing: this.isHealing,
hasDamage: this.hasDamage,
isSpell: this.data.type === "spell",
hasSave: this.hasSave,
config: CONFIG.ACKS,
};
// Render the chat card template
const template = `systems/acks/templates/chat/item-card.html`;
const html = await renderTemplate(template, templateData);
// Basic chat message data
const chatData = {
user: game.user._id,
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
content: html,
speaker: {
actor: this.actor._id,
token: this.actor.token,
alias: this.actor.name,
},
};
// Toggle default roll mode
let rollMode = game.settings.get("core", "rollMode");
if (["gmroll", "blindroll"].includes(rollMode))
chatData["whisper"] = ChatMessage.getWhisperRecipients("GM");
if (rollMode === "selfroll") chatData["whisper"] = [game.user._id];
if (rollMode === "blindroll") chatData["blind"] = true;
// Create the chat message
return ChatMessage.create(chatData);
}
/**
* Handle toggling the visibility of chat card content when the name is clicked
* @param {Event} event The originating click event
* @private
*/
static _onChatCardToggleContent(event) {
event.preventDefault();
const header = event.currentTarget;
const card = header.closest(".chat-card");
const content = card.querySelector(".card-content");
if (content.style.display == "none") {
$(content).slideDown(200);
} else {
$(content).slideUp(200);
}
}
static async _onChatCardAction(event) {
event.preventDefault();
// Extract card data
const button = event.currentTarget;
button.disabled = true;
const card = button.closest(".chat-card");
const messageId = card.closest(".message").dataset.messageId;
const message = game.messages.get(messageId);
const action = button.dataset.action;
// Validate permission to proceed with the roll
const isTargetted = action === "save";
if (!(isTargetted || game.user.isGM || message.isAuthor)) return;
// Get the Actor from a synthetic Token
const actor = this._getChatCardActor(card);
if (!actor) return;
// Get the Item
const item = actor.getOwnedItem(card.dataset.itemId);
if (!item) {
return ui.notifications.error(
`The requested item ${card.dataset.itemId} no longer exists on Actor ${actor.name}`
);
}
// Get card targets
let targets = [];
if (isTargetted) {
targets = this._getChatCardTargets(card);
}
// Attack and Damage Rolls
if (action === "damage") await item.rollDamage({ event });
else if (action === "formula") await item.rollFormula({ event });
// Saving Throws for card targets
else if (action == "save") {
if (!targets.length) {
ui.notifications.warn(
`You must have one or more controlled Tokens in order to use this option.`
);
return (button.disabled = false);
}
for (let t of targets) {
await t.rollSave(button.dataset.save, { event });
}
}
// Re-enable the button
button.disabled = false;
}
static _getChatCardActor(card) {
// Case 1 - a synthetic actor from a Token
const tokenKey = card.dataset.tokenId;
if (tokenKey) {
const [sceneId, tokenId] = tokenKey.split(".");
const scene = game.scenes.get(sceneId);
if (!scene) return null;
const tokenData = scene.getEmbeddedEntity("Token", tokenId);
if (!tokenData) return null;
const token = new Token(tokenData);
return token.actor;
}
// Case 2 - use Actor ID directory
const actorId = card.dataset.actorId;
return game.actors.get(actorId) || null;
}
static _getChatCardTargets(card) {
const character = game.user.character;
const controlled = canvas.tokens.controlled;
const targets = controlled.reduce(
(arr, t) => (t.actor ? arr.concat([t.actor]) : arr),
[]
);
if (character && controlled.length === 0) targets.push(character);
return targets;
}
}