Merge branch 'Develop' into cbnathanael

This commit is contained in:
Nathanael Phillips 2020-11-17 08:50:06 -07:00
commit 2d8cc8cda2
58 changed files with 1414 additions and 1704 deletions

View file

@ -1,10 +1,12 @@
import Item5e from "../../item/entity.js";
import TraitSelector from "../../apps/trait-selector.js";
import ActorSheetFlags from "../../apps/actor-flags.js";
import MovementConfig from "../../apps/movement-config.js";
import {SW5E} from '../../config.js';
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../effects.js";
/**
* Extend the basic ActorSheet class to do all the SW5e things!
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
* This sheet is an Abstract layer which is not used.
* @extends {ActorSheet}
*/
@ -94,6 +96,9 @@ export default class ActorSheet5e extends ActorSheet {
}
}
// Movement speeds
data.movement = this._getMovementSpeed(data.actor);
// Update traits
this._prepareTraits(data.actor.data.traits);
@ -101,7 +106,7 @@ export default class ActorSheet5e extends ActorSheet {
this._prepareItems(data);
// Prepare active effects
this._prepareEffects(data);
data.effects = prepareActiveEffectCategories(this.entity.effects);
// Return data to the sheet
return data
@ -109,6 +114,28 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Prepare the display of movement speed data for the Actor
* @param {object} actorData
* @returns {{primary: string, special: string}}
* @private
*/
_getMovementSpeed(actorData) {
const movement = actorData.data.attributes.movement;
const speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
].filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
return {
primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
}
}
/* -------------------------------------------- */
/**
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
* @param {object} traits The raw traits data object from the actor data
@ -147,43 +174,6 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Prepare the data structure for Active Effects which are currently applied to the Actor.
* @param {object} data The object of rendering data which is being prepared
* @private
*/
_prepareEffects(data) {
// Define effect header categories
const categories = {
temporary: {
label: "Temporary Effects",
effects: []
},
passive: {
label: "Passive Effects",
effects: []
},
inactive: {
label: "Inactive Effects",
effects: []
}
};
// Iterate over active effects, classifying them into categories
for ( let e of this.actor.effects ) {
e._getSourceName(); // Trigger a lookup for the source name
if ( e.data.disabled ) categories.inactive.effects.push(e);
else if ( e.isTemporary ) categories.temporary.effects.push(e);
else categories.passive.effects.push(e);
}
// Add the prepared categories of effects to the rendering data
return data.effects = categories;
}
/* -------------------------------------------- */
/**
* Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared
@ -242,7 +232,7 @@ export default class ActorSheet5e extends ActorSheet {
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
}
}
// Pact magic users have cantrips and a pact magic section
if ( levels.pact && levels.pact.max ) {
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
@ -363,7 +353,7 @@ export default class ActorSheet5e extends ActorSheet {
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries
html.find('.item .item-name h4').click(event => this._onItemSummary(event));
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
// Editable Only Listeners
if ( this.isEditable ) {
@ -383,6 +373,7 @@ export default class ActorSheet5e extends ActorSheet {
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find('.configure-movement').click(this._onMovementConfig.bind(this));
html.find('.configure-flags').click(this._onConfigureFlags.bind(this));
// Owned Item management
@ -393,8 +384,7 @@ export default class ActorSheet5e extends ActorSheet {
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
// Active Effect management
html.find(".effect-control").click(this._onManageActiveEffect.bind(this));
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
}
// Owner Only Listeners
@ -565,7 +555,7 @@ export default class ActorSheet5e extends ActorSheet {
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
@ -576,9 +566,7 @@ export default class ActorSheet5e extends ActorSheet {
}
// Create the owned item as normal
// TODO remove conditional logic in 0.7.x
if (isNewerVersion(game.data.version, "0.6.9")) return super._onDropItemCreate(itemData);
else return this.actor.createEmbeddedEntity("OwnedItem", itemData);
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
@ -731,28 +719,6 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Manage Active Effect instances through the Actor Sheet via effect control buttons.
* @param {MouseEvent} event The left-click event on the effect control
* @private
*/
_onManageActiveEffect(event) {
event.preventDefault();
const a = event.currentTarget;
const li = a.closest(".effect");
const effect = this.actor.effects.get(li.dataset.effectId);
switch ( a.dataset.action ) {
case "edit":
return effect.sheet.render(true);
case "delete":
return effect.delete();
case "toggle":
return effect.update({disabled: !effect.data.disabled});
}
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
@ -825,6 +791,18 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onMovementConfig(event) {
event.preventDefault();
new MovementConfig(this.object).render(true);
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
@ -839,90 +817,4 @@ export default class ActorSheet5e extends ActorSheet {
});
return buttons;
}
/* -------------------------------------------- */
/* DEPRECATED */
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _onDrop (event) {
event.preventDefault();
// Get dropped data
let data;
try {
data = JSON.parse(event.dataTransfer.getData('text/plain'));
} catch (err) {
return false;
}
if ( !data ) return false;
// Handle the drop with a Hooked function
const allowed = Hooks.call("dropActorSheetData", this.actor, this, data);
if ( allowed === false ) return;
// Case 1 - Dropped Item
if ( data.type === "Item" ) {
return this._onDropItem(event, data);
}
// Case 2 - Dropped Actor
if ( data.type === "Actor" ) {
return this._onDropActor(event, data);
}
}
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _onDropItem(event, data) {
if ( !this.actor.owner ) return false;
let itemData = await this._getItemDropData(event, data);
// Handle item sorting within the same Actor
const actor = this.actor;
let sameActor = (data.actorId === actor._id) || (actor.isToken && (data.tokenId === actor.token.id));
if (sameActor) return this._onSortItem(event, itemData);
// Create a new item
this._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _getItemDropData(event, data) {
let itemData = null;
// Case 1 - Import from a Compendium pack
if (data.pack) {
const pack = game.packs.get(data.pack);
if (pack.metadata.entity !== "Item") return;
itemData = await pack.getEntry(data.id);
}
// Case 2 - Data explicitly provided
else if (data.data) {
itemData = data.data;
}
// Case 3 - Import from World entity
else {
let item = game.items.get(data.id);
if (!item) return;
itemData = item.data;
}
// Return a copy of the extracted data
return duplicate(itemData);
}
}

View file

@ -68,7 +68,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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) => {
@ -109,7 +109,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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;
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
inventory[i.type].items.push(i);
}
@ -123,12 +123,12 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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 },
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"} }
};
@ -138,13 +138,13 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}
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;
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);
@ -189,9 +189,6 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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));
@ -199,8 +196,8 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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));
// Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
}
/* -------------------------------------------- */
@ -210,14 +207,19 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
* @param {MouseEvent} event The originating click event
* @private
*/
_onDeathSave(event) {
_onSheetAction(event) {
event.preventDefault();
return this.actor.rollDeathSave({event: event});
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
@ -259,53 +261,39 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/* -------------------------------------------- */
/**
* 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) {
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);
const classWasAlreadyPresent = !!cls;
let priorLevel = cls?.data.data.levels ?? 0;
const hasClass = !!cls;
// Add new features for class level
if ( !classWasAlreadyPresent ) {
Actor5e.getClassFeatures(itemData).then(features => {
this.actor.createEmbeddedEntity("OwnedItem", features);
});
// 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;
}
}
// 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
// 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);
}
}
super._onDropItemCreate(itemData);
// Default drop handling if levels were not added
if ( !addLevel ) super._onDropItemCreate(itemData);
}
}

View file

@ -106,7 +106,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
}
/* -------------------------------------------- */
@ -116,7 +116,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
* @param {Event} event The original click event
* @private
*/
_onRollHealthFormula(event) {
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if ( !formula ) return;

View file

@ -49,12 +49,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
// Compute overall encumbrance
const enc = {
max: actorData.data.attributes.capacity.cargo,
value: Math.round(totalWeight * 10) / 10
};
enc.pct = Math.min(enc.value * 100 / enc.max, 99);
return enc;
const max = actorData.data.attributes.capacity.cargo;
const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
return {value: totalWeight.toNearest(0.1), max, pct};
}
/* -------------------------------------------- */
@ -89,6 +86,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
_getMovementSpeed(actorData) {
return {primary: "", special: ""};
}
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the Vehicle sheet.
* @private