forked from GitHub-Mirrors/foundry-sw5e

DND5e Core 1.3.5 modded to SW5e System Combining with DND5e Core 1.3.2 to see one big commit since last core update DND5e Core 1.3.2 modded to SW5e System
286 lines
10 KiB
JavaScript
286 lines
10 KiB
JavaScript
import ActorSheet5e from "./base.js";
|
|
import Actor5e from "../entity.js";
|
|
|
|
/**
|
|
* An Actor sheet for player character type actors.
|
|
* Extends the base ActorSheet5e class.
|
|
* @type {ActorSheet5e}
|
|
*/
|
|
export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|
|
|
/**
|
|
* Define default rendering options for the NPC sheet
|
|
* @return {Object}
|
|
*/
|
|
static get defaultOptions() {
|
|
return mergeObject(super.defaultOptions, {
|
|
classes: ["sw5e", "sheet", "actor", "character"],
|
|
width: 720,
|
|
height: 680
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
|
*/
|
|
getData() {
|
|
const sheetData = super.getData();
|
|
|
|
// Temporary HP
|
|
let hp = sheetData.data.attributes.hp;
|
|
if (hp.temp === 0) delete hp.temp;
|
|
if (hp.tempmax === 0) delete hp.tempmax;
|
|
|
|
// Resources
|
|
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
|
const res = sheetData.data.resources[r] || {};
|
|
res.name = r;
|
|
res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase());
|
|
if (res && res.value === 0) delete res.value;
|
|
if (res && res.max === 0) delete res.max;
|
|
return arr.concat([res]);
|
|
}, []);
|
|
|
|
// Experience Tracking
|
|
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
|
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
|
|
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
|
|
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
|
|
}).join(', ');
|
|
|
|
// Return data for rendering
|
|
return sheetData;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Organize and classify Owned Items for Character sheets
|
|
* @private
|
|
*/
|
|
_prepareItems(data) {
|
|
|
|
// Categorize items as inventory, powerbook, features, and classes
|
|
const inventory = {
|
|
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
|
|
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} },
|
|
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} },
|
|
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} },
|
|
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
|
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
|
};
|
|
|
|
// Partition items by category
|
|
let [items, powers, feats, classes] = data.items.reduce((arr, item) => {
|
|
|
|
// Item details
|
|
item.img = item.img || CONST.DEFAULT_TOKEN;
|
|
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
|
item.attunement = {
|
|
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
|
icon: "fa-sun",
|
|
cls: "not-attuned",
|
|
title: "SW5E.AttunementRequired"
|
|
},
|
|
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
|
icon: "fa-sun",
|
|
cls: "attuned",
|
|
title: "SW5E.AttunementAttuned"
|
|
}
|
|
}[item.data.attunement];
|
|
|
|
// Item usage
|
|
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
|
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
|
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
|
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
|
|
|
// Item toggle state
|
|
this._prepareItemToggleState(item);
|
|
|
|
// Primary Class
|
|
if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass );
|
|
|
|
// Classify items into types
|
|
if ( item.type === "power" ) arr[1].push(item);
|
|
else if ( item.type === "feat" ) arr[2].push(item);
|
|
else if ( item.type === "class" ) arr[3].push(item);
|
|
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
|
|
return arr;
|
|
}, [[], [], [], []]);
|
|
|
|
// Apply active item filters
|
|
items = this._filterItems(items, this._filters.inventory);
|
|
powers = this._filterItems(powers, this._filters.powerbook);
|
|
feats = this._filterItems(feats, this._filters.features);
|
|
|
|
// Organize items
|
|
for ( let i of items ) {
|
|
i.data.quantity = i.data.quantity || 0;
|
|
i.data.weight = i.data.weight || 0;
|
|
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
|
inventory[i.type].items.push(i);
|
|
}
|
|
|
|
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
|
const powerbook = this._preparePowerbook(data, powers);
|
|
const nPrepared = powers.filter(s => {
|
|
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
|
|
}).length;
|
|
|
|
// Organize Features
|
|
const features = {
|
|
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
|
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
|
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
|
};
|
|
for ( let f of feats ) {
|
|
if ( f.data.activation.type ) features.active.items.push(f);
|
|
else features.passive.items.push(f);
|
|
}
|
|
classes.sort((a, b) => b.data.levels - a.data.levels);
|
|
features.classes.items = classes;
|
|
|
|
// Assign and return
|
|
data.inventory = Object.values(inventory);
|
|
data.powerbook = powerbook;
|
|
data.preparedPowers = nPrepared;
|
|
data.features = Object.values(features);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* A helper method to establish the displayed preparation state for an item
|
|
* @param {Item} item
|
|
* @private
|
|
*/
|
|
_prepareItemToggleState(item) {
|
|
if (item.type === "power") {
|
|
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
|
const isPrepared = getProperty(item.data, "preparation.prepared");
|
|
item.toggleClass = isPrepared ? "active" : "";
|
|
if ( isAlways ) item.toggleClass = "fixed";
|
|
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
|
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
|
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
|
}
|
|
else {
|
|
const isActive = getProperty(item.data, "equipped");
|
|
item.toggleClass = isActive ? "active" : "";
|
|
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Event Listeners and Handlers
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Activate event listeners using the prepared sheet HTML
|
|
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
|
|
*/
|
|
activateListeners(html) {
|
|
super.activateListeners(html);
|
|
if ( !this.isEditable ) return;
|
|
|
|
// Item State Toggling
|
|
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
|
|
|
// Short and Long Rest
|
|
html.find('.short-rest').click(this._onShortRest.bind(this));
|
|
html.find('.long-rest').click(this._onLongRest.bind(this));
|
|
|
|
// Rollable sheet actions
|
|
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle mouse click events for character sheet actions
|
|
* @param {MouseEvent} event The originating click event
|
|
* @private
|
|
*/
|
|
_onSheetAction(event) {
|
|
event.preventDefault();
|
|
const button = event.currentTarget;
|
|
switch( button.dataset.action ) {
|
|
case "convertCurrency":
|
|
return Dialog.confirm({
|
|
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
|
|
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
|
|
yes: () => this.actor.convertCurrency()
|
|
});
|
|
case "rollDeathSave":
|
|
return this.actor.rollDeathSave({event: event});
|
|
case "rollInitiative":
|
|
return this.actor.rollInitiative({createCombatants: true});
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle toggling the state of an Owned Item within the Actor
|
|
* @param {Event} event The triggering click event
|
|
* @private
|
|
*/
|
|
_onToggleItem(event) {
|
|
event.preventDefault();
|
|
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
|
const item = this.actor.items.get(itemId);
|
|
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
|
return item.update({[attr]: !getProperty(item.data, attr)});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Take a short rest, calling the relevant function on the Actor instance
|
|
* @param {Event} event The triggering click event
|
|
* @private
|
|
*/
|
|
async _onShortRest(event) {
|
|
event.preventDefault();
|
|
await this._onSubmit(event);
|
|
return this.actor.shortRest();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Take a long rest, calling the relevant function on the Actor instance
|
|
* @param {Event} event The triggering click event
|
|
* @private
|
|
*/
|
|
async _onLongRest(event) {
|
|
event.preventDefault();
|
|
await this._onSubmit(event);
|
|
return this.actor.longRest();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @override */
|
|
async _onDropItemCreate(itemData) {
|
|
|
|
// Increment the number of class levels a character instead of creating a new item
|
|
if ( itemData.type === "class" ) {
|
|
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
|
let priorLevel = cls?.data.data.levels ?? 0;
|
|
if ( !!cls ) {
|
|
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
|
if ( next > priorLevel ) {
|
|
itemData.levels = next;
|
|
return cls.update({"data.levels": next});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default drop handling if levels were not added
|
|
return super._onDropItemCreate(itemData);
|
|
}
|
|
}
|