forked from GitHub-Mirrors/foundry-sw5e

System main update to be inline with dnd5e 1.1.1 Added active effects to as many sheets as I thought applicable. Please check loot, I made an attempt but it may be broken All .less .css and actor .html updates were made to the old actors. New actors may be broken with this update removed templates\actors\oldActor\parts\actor-effects.html for newer templates\actors\parts\active-effects.html removed module\apps\cast-dialog, templates\apps\cast-cast.html, and templates\items\cast.html. I do not think they are used, I think they were deprecated when powers were treated as items, if not we can add them back in. **NOTE** REQUIRES Foundry 0.7.6
299 lines
12 KiB
JavaScript
299 lines
12 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, fightingstyles, fightingmasteries, 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 === "fightingstyle" ) arr[8].push(item);
|
|
else if ( item.type === "fightingmastery" ) arr[9].push(item);
|
|
else if ( item.type === "lightsaberform" ) arr[10].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 },
|
|
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 },
|
|
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
|
|
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
|
|
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", 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.fightingstyles.items = fightingstyles;
|
|
features.fightingmasteries.items = fightingmasteries;
|
|
features.lightsaberforms.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;
|
|
|
|
// 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 rolling a death saving throw for the Character
|
|
* @param {MouseEvent} event The originating click event
|
|
* @private
|
|
*/
|
|
_onSheetAction(event) {
|
|
event.preventDefault();
|
|
const button = event.currentTarget;
|
|
switch( button.dataset.action ) {
|
|
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.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();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @override */
|
|
async _onDropItemCreate(itemData) {
|
|
let addLevel = false;
|
|
|
|
// 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);
|
|
let priorLevel = cls?.data.data.levels ?? 0;
|
|
const hasClass = !!cls;
|
|
|
|
// Increment levels instead of creating a new item
|
|
if ( hasClass ) {
|
|
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
|
if ( next > priorLevel ) {
|
|
itemData.levels = next;
|
|
await cls.update({"data.levels": next});
|
|
addLevel = true;
|
|
}
|
|
}
|
|
|
|
// Add class features
|
|
if ( !hasClass || addLevel ) {
|
|
const features = await Actor5e.getClassFeatures({
|
|
className: itemData.name,
|
|
subclassName: itemData.data.subclass,
|
|
level: itemData.levels,
|
|
priorLevel: priorLevel
|
|
});
|
|
await this.actor.createEmbeddedEntity("OwnedItem", features);
|
|
}
|
|
}
|
|
|
|
// Default drop handling if levels were not added
|
|
if ( !addLevel ) super._onDropItemCreate(itemData);
|
|
}
|
|
}
|