forked from GitHub-Mirrors/foundry-sw5e

Tons of updates to relect recent changes to PHB and EC source materials. AC minimum for "Hide" species trait currently does not working using DAE.
305 lines
11 KiB
JavaScript
305 lines
11 KiB
JavaScript
import ActorSheet5e from "./base.js";
|
|
import Actor5e from "../entity.js";
|
|
|
|
/**
|
|
* An Actor sheet for player character type actors in the SW5E system.
|
|
* 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: 736
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* 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(", ");
|
|
|
|
// 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, species, archetypes, classfeatures, backgrounds, lightsaberforms] = data.items.reduce((arr, item) => {
|
|
|
|
// Item details
|
|
item.img = item.img || DEFAULT_TOKEN;
|
|
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
|
|
|
// 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);
|
|
|
|
// 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 ( item.type === "species" ) arr[4].push(item);
|
|
else if ( item.type === "archetype" ) arr[5].push(item);
|
|
else if ( item.type === "classfeature" ) arr[6].push(item);
|
|
else if ( item.type === "background" ) arr[7].push(item);
|
|
else if ( item.type === "lightsaberform" ) arr[8].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 = Math.round(i.data.quantity * i.data.weight * 10) / 10;
|
|
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 },
|
|
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true },
|
|
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
|
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
|
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
|
lightsaberform: { label: "SW5E.ItemTypeLightsaberForm", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: 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.levels - a.levels);
|
|
features.classes.items = classes;
|
|
features.classfeatures.items = classfeatures;
|
|
features.archetype.items = archetypes;
|
|
features.species.items = species;
|
|
features.background.items = backgrounds;
|
|
features.lightsaberform.items = lightsaberforms;
|
|
|
|
// 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 {HTML} The prepared HTML object ready to be rendered into the DOM
|
|
*/
|
|
activateListeners(html) {
|
|
super.activateListeners(html);
|
|
if ( !this.options.editable ) return;
|
|
|
|
// Inventory Functions
|
|
html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
|
|
|
// 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));
|
|
|
|
// Death saving throws
|
|
html.find('.death-save').click(this._onDeathSave.bind(this));
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle rolling a death saving throw for the Character
|
|
* @param {MouseEvent} event The originating click event
|
|
* @private
|
|
*/
|
|
_onDeathSave(event) {
|
|
event.preventDefault();
|
|
return this.actor.rollDeathSave({event: event});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* 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.getOwnedItem(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();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle mouse click events to convert currency to the highest possible denomination
|
|
* @param {MouseEvent} event The originating click event
|
|
* @private
|
|
*/
|
|
async _onConvertCurrency(event) {
|
|
event.preventDefault();
|
|
return Dialog.confirm({
|
|
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
|
|
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
|
|
yes: () => this.actor.convertCurrency()
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @override */
|
|
async _onDropItemCreate(itemData) {
|
|
|
|
// Upgrade the number of class levels a character has and add features
|
|
if ( itemData.type === "class" ) {
|
|
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
|
const classWasAlreadyPresent = !!cls;
|
|
|
|
// Add new features for class level
|
|
if ( !classWasAlreadyPresent ) {
|
|
Actor5e.getClassFeatures(itemData).then(features => {
|
|
this.actor.createEmbeddedEntity("OwnedItem", features);
|
|
});
|
|
}
|
|
|
|
// If the actor already has the class, increment the level instead of creating a new item
|
|
// then add new features as long as level increases
|
|
if ( classWasAlreadyPresent ) {
|
|
const lvl = cls.data.data.levels;
|
|
const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
|
|
if ( !(lvl === newLvl) ) {
|
|
cls.update({"data.levels": newLvl});
|
|
itemData.data.levels = newLvl;
|
|
Actor5e.getClassFeatures(itemData).then(features => {
|
|
this.actor.createEmbeddedEntity("OwnedItem", features);
|
|
});
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
super._onDropItemCreate(itemData);
|
|
}
|
|
}
|