Merge pull request #239 from unrealkakeman89/Develop

Develop
This commit is contained in:
CK 2021-07-07 09:24:23 -04:00 committed by GitHub
commit 2212e3c7d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 13465 additions and 12707 deletions

14
.prettierrc Normal file
View file

@ -0,0 +1,14 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": false,
"jsxBracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}

View file

@ -1,2 +1,3 @@
{
"editor.formatOnSave": true
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from '../../../config.js';
import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
@ -58,7 +58,8 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
if (!game.user.isGM && this.actor.limited)
return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
}
@ -66,7 +67,6 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
getData(options) {
// Basic data
let isOwner = this.actor.isOwner;
const data = {
@ -78,7 +78,7 @@ export default class ActorSheet5e extends ActorSheet {
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === 'vehicle',
isVehicle: this.actor.type === "vehicle",
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
@ -90,7 +90,7 @@ export default class ActorSheet5e extends ActorSheet {
// Owned Items
data.items = actorData.items;
for ( let i of data.items ) {
for (let i of data.items) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
@ -101,7 +101,7 @@ export default class ActorSheet5e extends ActorSheet {
data.filters = this._filters;
// Ability Scores
for ( let [a, abl] of Object.entries(actorData.data.abilities)) {
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
@ -115,7 +115,7 @@ export default class ActorSheet5e extends ActorSheet {
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
if (data.actor.type === "starship") {
skl.label = CONFIG.SW5E.starshipSkills[s];
}else{
} else {
skl.label = CONFIG.SW5E.skills[s];
}
}
@ -137,7 +137,7 @@ export default class ActorSheet5e extends ActorSheet {
data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet
return data
return data;
}
/* -------------------------------------------- */
@ -149,38 +149,42 @@ export default class ActorSheet5e extends ActorSheet {
* @returns {{primary: string, special: string}}
* @private
*/
_getMovementSpeed(actorData, largestPrimary=false) {
_getMovementSpeed(actorData, largestPrimary = false) {
const movement = actorData.data.attributes.movement || {};
// Prepare an array of available movement speeds
let 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.fly,
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
]
if ( largestPrimary ) {
];
if (largestPrimary) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
}
// Filter and sort speeds on their values
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary
if ( largestPrimary ) {
if (largestPrimary) {
let primary = speeds.shift();
return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map(s => s[1]).join(", ")
}
special: speeds.map((s) => s[1]).join(", ")
};
}
// Case 2: Walk as primary
else {
return {
primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
}
special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
};
}
}
@ -189,12 +193,12 @@ export default class ActorSheet5e extends ActorSheet {
_getSenses(actorData) {
const senses = actorData.data.attributes.senses || {};
const tags = {};
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[k] ?? 0
if ( v === 0 ) continue;
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[k] ?? 0;
if (v === 0) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
}
if ( !!senses.special ) tags["special"] = senses.special;
if (!!senses.special) tags["special"] = senses.special;
return tags;
}
@ -207,20 +211,20 @@ export default class ActorSheet5e extends ActorSheet {
*/
_prepareTraits(traits) {
const map = {
"dr": CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies,
"weaponProf": CONFIG.SW5E.weaponProficiencies,
"toolProf": CONFIG.SW5E.toolProficiencies
dr: CONFIG.SW5E.damageResistanceTypes,
di: CONFIG.SW5E.damageResistanceTypes,
dv: CONFIG.SW5E.damageResistanceTypes,
ci: CONFIG.SW5E.conditionTypes,
languages: CONFIG.SW5E.languages,
armorProf: CONFIG.SW5E.armorProficiencies,
weaponProf: CONFIG.SW5E.weaponProficiencies,
toolProf: CONFIG.SW5E.toolProficiencies
};
for ( let [t, choices] of Object.entries(map) ) {
for (let [t, choices] of Object.entries(map)) {
const trait = traits[t];
if ( !trait ) continue;
if (!trait) continue;
let values = [];
if ( trait.value ) {
if (trait.value) {
values = trait.value instanceof Array ? trait.value : [trait.value];
}
trait.selected = values.reduce((obj, t) => {
@ -229,8 +233,8 @@ export default class ActorSheet5e extends ActorSheet {
}, {});
// Add custom entry
if ( trait.custom ) {
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
if (trait.custom) {
trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
@ -252,8 +256,8 @@ export default class ActorSheet5e extends ActorSheet {
// Define some mappings
const sections = {
"atwill": -20,
"innate": -10,
atwill: -20,
innate: -10
};
// Label power slot uses headers
@ -264,32 +268,37 @@ export default class ActorSheet5e extends ActorSheet {
};
// Format a powerbook entry for a certain indexed level
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => {
powerbook[i] = {
order: i,
label: label,
usesSlots: i > 0,
canCreate: owner,
canPrepare: (data.actor.type === "character") && (i >= 1),
canPrepare: data.actor.type === "character" && i >= 1,
powers: [],
uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0,
override: override || 0,
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode, "school": school},
dataset: {
"type": "power",
"level": prepMode in sections ? 1 : i,
"preparation.mode": prepMode,
"school": school
},
prop: sl
};
};
// Determine the maximum power level which has a slot
const maxLevel = Array.fromRange(10).reduce((max, i) => {
if ( i === 0 ) return max;
if (i === 0) return max;
const level = levels[`power${i}`];
if ( (level.max || level.override ) && ( i > max ) ) max = i;
if ((level.max || level.override) && i > max) max = i;
return max;
}, 0);
// Level-based powercasters have cantrips and leveled slots
if ( maxLevel > 0 ) {
if (maxLevel > 0) {
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
for (let lvl = 1; lvl <= maxLevel; lvl++) {
const sl = `power${lvl}`;
@ -298,15 +307,15 @@ export default class ActorSheet5e extends ActorSheet {
}
// Iterate over every power item, adding powers to the powerbook by section
powers.forEach(power => {
powers.forEach((power) => {
const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0;
const sl = `power${s}`;
// Specialized powercasting modes (if they exist)
if ( mode in sections ) {
if (mode in sections) {
s = sections[mode];
if ( !powerbook[s] ){
if (!powerbook[s]) {
const l = levels[mode] || {};
const config = CONFIG.SW5E.powerPreparationModes[mode];
registerSection(mode, s, config, {
@ -319,7 +328,7 @@ export default class ActorSheet5e extends ActorSheet {
}
// Sections for higher-level powers which the caster "should not" have, but power items exist for
else if ( !powerbook[s] ) {
else if (!powerbook[s]) {
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
}
@ -341,33 +350,33 @@ export default class ActorSheet5e extends ActorSheet {
* @private
*/
_filterItems(items, filters) {
return items.filter(item => {
return items.filter((item) => {
const data = item.data;
// Action usage
for ( let f of ["action", "bonus", "reaction"] ) {
if ( filters.has(f) ) {
if ((data.activation && (data.activation.type !== f))) return false;
for (let f of ["action", "bonus", "reaction"]) {
if (filters.has(f)) {
if (data.activation && data.activation.type !== f) return false;
}
}
// Power-specific filters
if ( filters.has("ritual") ) {
if (filters.has("ritual")) {
if (data.components.ritual !== true) return false;
}
if ( filters.has("concentration") ) {
if (filters.has("concentration")) {
if (data.components.concentration !== true) return false;
}
if ( filters.has("prepared") ) {
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
if ( this.actor.data.type === "npc" ) return true;
if ( this.actor.data.type === "starship" ) return true;
if (filters.has("prepared")) {
if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
if (this.actor.data.type === "npc") return true;
if (this.actor.data.type === "starship") return true;
return data.preparation.prepared;
}
// Equipment-specific filters
if ( filters.has("equipped") ) {
if ( data.equipped !== true ) return false;
if (filters.has("equipped")) {
if (data.equipped !== true) return false;
}
return true;
});
@ -395,64 +404,62 @@ export default class ActorSheet5e extends ActorSheet {
/** @inheritdoc */
activateListeners(html) {
// Activate Item Filters
const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
// View Item Sheets
html.find('.item-edit').click(this._onItemEdit.bind(this));
html.find(".item-edit").click(this._onItemEdit.bind(this));
// Editable Only Listeners
if ( this.isEditable ) {
if (this.isEditable) {
// Input focus and update
const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select());
inputs.focus((ev) => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency
html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find('.config-button').click(this._onConfigMenu.bind(this));
html.find(".config-button").click(this._onConfigMenu.bind(this));
// Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this));
html.find('.item-delete').click(this._onItemDelete.bind(this));
html.find('.item-collapse').click(this._onItemCollapse.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
html.find('.increment-class-level').click(this._onIncrementClassLevel.bind(this));
html.find('.decrement-class-level').click(this._onDecrementClassLevel.bind(this));
html.find(".item-create").click(this._onItemCreate.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this));
html.find(".item-collapse").click(this._onItemCollapse.bind(this));
html.find(".item-uses input")
.click((ev) => ev.target.select())
.change(this._onUsesChange.bind(this));
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this));
html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this));
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
if ( this.actor.isOwner ) {
if (this.actor.isOwner) {
// Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
// Item Rolling
html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
html.find(".item .item-image").click((event) => this._onItemRoll(event));
html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
}
// Otherwise remove rollable classes
@ -473,8 +480,8 @@ export default class ActorSheet5e extends ActorSheet {
_initializeFilterItemList(i, ul) {
const set = this._filters[ul.dataset.filter];
const filters = ul.querySelectorAll(".filter-item");
for ( let li of filters ) {
if ( set.has(li.dataset.filter) ) li.classList.add("active");
for (let li of filters) {
if (set.has(li.dataset.filter)) li.classList.add("active");
}
}
@ -490,10 +497,10 @@ export default class ActorSheet5e extends ActorSheet {
_onChangeInputDelta(event) {
const input = event.target;
const value = input.value;
if ( ["+", "-"].includes(value[0]) ) {
if (["+", "-"].includes(value[0])) {
let delta = parseFloat(value);
input.value = getProperty(this.actor.data, input.name) + delta;
} else if ( value[0] === "=" ) {
} else if (value[0] === "=") {
input.value = value.slice(1);
}
}
@ -509,7 +516,7 @@ export default class ActorSheet5e extends ActorSheet {
event.preventDefault();
const button = event.currentTarget;
let app;
switch ( button.dataset.action ) {
switch (button.dataset.action) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
@ -546,10 +553,10 @@ export default class ActorSheet5e extends ActorSheet {
let idx = levels.indexOf(level);
// Toggle next level - forward on click, backwards on right
if ( event.type === "click" ) {
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
} else if ( event.type === "contextmenu" ) {
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
if (event.type === "click") {
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
} else if (event.type === "contextmenu") {
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
}
// Update the field value and save the form
@ -560,49 +567,51 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
if (!canPolymorph) return false;
// Get the target actor
let sourceActor = null;
if (data.pack) {
const pack = game.packs.find(p => p.collection === data.pack);
const pack = game.packs.find((p) => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id);
} else {
sourceActor = game.actors.get(data.id);
}
if ( !sourceActor ) return;
if (!sourceActor) return;
// Define a function to record polymorph settings for future use
const rememberOptions = html => {
const rememberOptions = (html) => {
const options = {};
html.find('input').each((i, el) => {
html.find("input").each((i, el) => {
options[el.name] = el.checked;
});
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
game.settings.set('sw5e', 'polymorphSettings', settings);
const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
game.settings.set("sw5e", "polymorphSettings", settings);
return settings;
};
// Create and render the Dialog
return new Dialog({
title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
return new Dialog(
{
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
content: {
options: game.settings.get('sw5e', 'polymorphSettings'),
options: game.settings.get("sw5e", "polymorphSettings"),
i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken
},
default: 'accept',
default: "accept",
buttons: {
accept: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
},
wildshape: {
icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: html => this.actor.transformInto(sourceActor, {
label: game.i18n.localize("SW5E.PolymorphWildShape"),
callback: (html) =>
this.actor.transformInto(sourceActor, {
keepBio: true,
keepClass: true,
keepMental: true,
@ -613,60 +622,64 @@ export default class ActorSheet5e extends ActorSheet {
},
polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize('SW5E.Polymorph'),
callback: html => this.actor.transformInto(sourceActor, {
label: game.i18n.localize("SW5E.Polymorph"),
callback: (html) =>
this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens
})
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('Cancel')
label: game.i18n.localize("Cancel")
}
}
}, {
classes: ['dialog', 'sw5e'],
},
{
classes: ["dialog", "sw5e"],
width: 600,
template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
}).render(true);
template: "systems/sw5e/templates/apps/polymorph-prompt.html"
}
).render(true);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor
if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", {
if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
return ui.notifications.warn(
game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
}));
})
);
}
// Create a Consumable power scroll on the Inventory tab
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
if (itemData.type === "power" && this._tabs[0].active === "inventory") {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
if ( itemData.data ) {
if (itemData.data) {
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) {
const similarItem = this.actor.items.find(i => {
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && (sourceId === itemData.flags.core?.sourceId) &&
(i.type === "consumable");
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
});
if ( similarItem && itemData.name !== "Power Cell" ) { // Always create a new powercell instead of increasing quantity
if (similarItem && itemData.name !== "Power Cell") {
// Always create a new powercell instead of increasing quantity
return similarItem.update({
'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
"data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
@ -682,7 +695,7 @@ export default class ActorSheet5e extends ActorSheet {
* @param {MouseEvent} event The originating click event
* @private
*/
async _onPowerSlotOverride (event) {
async _onPowerSlotOverride(event) {
const span = event.currentTarget.parentElement;
const level = span.dataset.level;
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
@ -712,7 +725,7 @@ export default class ActorSheet5e extends ActorSheet {
const item = this.actor.items.get(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({ 'data.uses.value': uses });
return item.update({"data.uses.value": uses});
}
/* -------------------------------------------- */
@ -740,7 +753,7 @@ export default class ActorSheet5e extends ActorSheet {
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
return item.rollRecharge();
};
}
/* -------------------------------------------- */
@ -755,13 +768,13 @@ export default class ActorSheet5e extends ActorSheet {
chatData = item.getChatData({secrets: this.actor.isOwner});
// Toggle summary
if ( li.hasClass("expanded") ) {
if (li.hasClass("expanded")) {
let summary = li.children(".item-summary");
summary.slideUp(200, () => summary.remove());
} else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
chatData.properties.forEach((p) => props.append(`<span class="tag">${p}</span>`));
div.append(props);
li.append(div.hide());
div.slideDown(200);
@ -814,7 +827,7 @@ export default class ActorSheet5e extends ActorSheet {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId);
if ( item ) return item.delete();
if (item) return item.delete();
}
/**
@ -823,7 +836,7 @@ export default class ActorSheet5e extends ActorSheet {
* @private
*/
_onItemCollapse(event) {
_onItemCollapse(event) {
event.preventDefault();
event.currentTarget.classList.toggle("active");
@ -838,7 +851,7 @@ _onItemCollapse(event) {
}
}
/**
/**
* Handle incrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
@ -847,7 +860,7 @@ _onItemCollapse(event) {
_onIncrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character")
const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
@ -857,12 +870,12 @@ _onItemCollapse(event) {
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: (levels + 1) }};
const update = {_id: item.data._id, data: {levels: levels + 1}};
actor.updateEmbeddedDocuments("Item", [update]);
}
}
/**
/**
* Handle decrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
@ -871,7 +884,7 @@ _onItemCollapse(event) {
_onDecrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character")
const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
@ -881,10 +894,10 @@ _onItemCollapse(event) {
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: (levels - 1) }};
const update = {_id: item.data._id, data: {levels: levels - 1}};
actor.updateEmbeddedDocuments("Item", [update]);
}
}
/* -------------------------------------------- */
@ -937,7 +950,7 @@ _onItemCollapse(event) {
const li = event.currentTarget;
const set = this._filters[li.parentElement.dataset.filter];
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
if (set.has(filter)) set.delete(filter);
else set.add(filter);
return this.render();
}
@ -954,8 +967,8 @@ _onItemCollapse(event) {
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices };
return new TraitSelector(this.actor, options).render(true)
const options = {name: a.dataset.target, title: label.innerText, choices};
return new TraitSelector(this.actor, options).render(true);
}
/* -------------------------------------------- */
@ -965,7 +978,7 @@ _onItemCollapse(event) {
let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) {
buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation',
label: "SW5E.PolymorphRestoreTransformation",
class: "restore-transformation",
icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm()

View file

@ -7,7 +7,6 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e}
*/
export default class ActorSheet5eCharacterNew extends ActorSheet5e {
get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return "systems/sw5e/templates/actors/newActor/character-sheet.html";
@ -17,17 +16,18 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
* @return {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["swalt", "sw5e", "sheet", "actor", "character"],
blockFavTab: true,
subTabs: null,
width: 800,
tabs: [{
tabs: [
{
navSelector: ".root-tabs",
contentSelector: ".sheet-body",
initial: "attributes"
}],
}
]
});
}
@ -48,7 +48,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
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());
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]);
@ -56,10 +56,12 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// 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(', ');
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;
@ -72,23 +74,38 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
* @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"} }
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, forcepowers, techpowers, feats, classes, deployments, deploymentfeatures, ventures, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
let [
items,
forcepowers,
techpowers,
feats,
classes,
deployments,
deploymentfeatures,
ventures,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = 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.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
icon: "fa-sun",
@ -103,35 +120,39 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}[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.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 );
if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item);
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item);
else if ( item.type === "feat" ) arr[3].push(item);
else if ( item.type === "class" ) arr[4].push(item);
else if ( item.type === "deployment" ) arr[5].push(item);
else if ( item.type === "deploymentfeature" ) arr[6].push(item);
else if ( item.type === "venture" ) arr[7].push(item);
else if ( item.type === "species" ) arr[8].push(item);
else if ( item.type === "archetype" ) arr[9].push(item);
else if ( item.type === "classfeature" ) arr[10].push(item);
else if ( item.type === "background" ) arr[11].push(item);
else if ( item.type === "fightingstyle" ) arr[12].push(item);
else if ( item.type === "fightingmastery" ) arr[13].push(item);
else if ( item.type === "lightsaberform" ) arr[14].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[2].push(item);
else if (item.type === "feat") arr[3].push(item);
else if (item.type === "class") arr[4].push(item);
else if (item.type === "deployment") arr[5].push(item);
else if (item.type === "deploymentfeature") arr[6].push(item);
else if (item.type === "venture") arr[7].push(item);
else if (item.type === "species") arr[8].push(item);
else if (item.type === "archetype") arr[9].push(item);
else if (item.type === "classfeature") arr[10].push(item);
else if (item.type === "background") arr[11].push(item);
else if (item.type === "fightingstyle") arr[12].push(item);
else if (item.type === "fightingmastery") arr[13].push(item);
else if (item.type === "lightsaberform") arr[14].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);
@ -140,7 +161,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
feats = this._filterItems(feats, this._filters.features);
// Organize items
for ( let i of 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);
@ -153,22 +174,93 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Organize Features
const features = {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
deployments: { label: "SW5E.ItemTypeDeploymentPl", items: [], hasActions: false, dataset: {type: "deployment"}, isDeployment: true },
deploymentfeatures: { label: "SW5E.ItemTypeDeploymentFeaturePl", items: [], hasActions: true, dataset: {type: "deploymentfeature"}, isDeploymentfeature: true },
ventures: { label: "SW5E.ItemTypeVenturePl", items: [], hasActions: false, dataset: {type: "venture"}, isVenture: 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"} }
classes: {
label: "SW5E.ItemTypeClassPl",
items: [],
hasActions: false,
dataset: {type: "class"},
isClass: true
},
classfeatures: {
label: "SW5E.ItemTypeClassFeats",
items: [],
hasActions: true,
dataset: {type: "classfeature"},
isClassfeature: true
},
archetype: {
label: "SW5E.ItemTypeArchetype",
items: [],
hasActions: false,
dataset: {type: "archetype"},
isArchetype: true
},
deployments: {
label: "SW5E.ItemTypeDeploymentPl",
items: [],
hasActions: false,
dataset: {type: "deployment"},
isDeployment: true
},
deploymentfeatures: {
label: "SW5E.ItemTypeDeploymentFeaturePl",
items: [],
hasActions: true,
dataset: {type: "deploymentfeature"},
isDeploymentfeature: true
},
ventures: {
label: "SW5E.ItemTypeVenturePl",
items: [],
hasActions: false,
dataset: {type: "venture"},
isVenture: 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);
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);
@ -203,12 +295,11 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
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;
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 {
} else {
const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
@ -225,25 +316,27 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
*/
activateListeners(html) {
super.activateListeners(html);
if ( !this.isEditable ) return;
if (!this.isEditable) return;
// Inventory Functions
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
// Item State Toggling
html.find('.item-toggle').click(this._onToggleItem.bind(this));
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));
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));
// Send Languages to Chat onClick
html.find('[data-options="share-languages"]').click(event => {
html.find('[data-options="share-languages"]').click((event) => {
event.preventDefault();
let langs = this.actor.data.data.traits.languages.value.map(l => CONFIG.SW5E.languages[l] || l).join(", ");
let langs = this.actor.data.data.traits.languages.value
.map((l) => CONFIG.SW5E.languages[l] || l)
.join(", ");
let custom = this.actor.data.data.traits.languages.custom;
if (custom) langs += ", " + custom.replace(/;/g, ",");
let content = `
@ -279,9 +372,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
});
// Item Delete Confirmation
html.find('.item-delete').off("click");
html.find('.item-delete').click(event => {
let li = $(event.currentTarget).parents('.item');
html.find(".item-delete").off("click");
html.find(".item-delete").click((event) => {
let li = $(event.currentTarget).parents(".item");
let itemId = li.attr("data-item-id");
let item = this.actor.items.get(itemId);
new Dialog({
@ -290,17 +383,17 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
buttons: {
Yes: {
icon: '<i class="fa fa-check"></i>',
label: 'Yes',
callback: dlg => {
label: "Yes",
callback: (dlg) => {
this.actor.deleteOwnedItem(itemId);
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: 'No'
label: "No"
}
},
},
default: 'cancel'
default: "cancel"
}).render(true);
});
}
@ -315,7 +408,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
_onSheetAction(event) {
event.preventDefault();
const button = event.currentTarget;
switch( button.dataset.action ) {
switch (button.dataset.action) {
case "rollDeathSave":
return this.actor.rollDeathSave({event: event});
case "rollInitiative":
@ -325,7 +418,6 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Handle toggling the state of an Owned Item within the Actor
* @param {Event} event The triggering click event
@ -369,14 +461,13 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/** @override */
async _onDropItemCreate(itemData) {
// Increment the number of class levels of a character instead of creating a new item
if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
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 ) {
if (!!cls) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
if ( next > priorLevel ) {
if (next > priorLevel) {
itemData.levels = next;
return cls.update({"data.levels": next});
}
@ -457,9 +548,9 @@ async function addFavorites(app, html, data) {
value: data.actor.data.powers.power9.value,
max: data.actor.data.powers.power9.max
}
}
};
let powerCount = 0
let powerCount = 0;
let items = data.actor.items;
for (let item of items) {
if (item.type == "class") continue;
@ -470,24 +561,28 @@ async function addFavorites(app, html, data) {
}
let isFav = item.flags.favtab.isFavourite;
if (app.options.editable) {
let favBtn = $(`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${isFav ? "Remove from Favourites" : "Add to Favourites"}"><i class="fas fa-star"></i></a>`);
favBtn.click(ev => {
let favBtn = $(
`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${
isFav ? "Remove from Favourites" : "Add to Favourites"
}"><i class="fas fa-star"></i></a>`
);
favBtn.click((ev) => {
app.actor.items.get(item.data._id).update({
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite
});
});
html.find(`.item[data-item-id="${item.data._id}"]`).find('.item-controls').prepend(favBtn);
html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn);
}
if (isFav) {
item.powerComps = "";
if (item.data.components) {
let comps = item.data.components;
let v = (comps.vocal) ? "V" : "";
let s = (comps.somatic) ? "S" : "";
let m = (comps.material) ? "M" : "";
let c = !!(comps.concentration);
let r = !!(comps.ritual);
let v = comps.vocal ? "V" : "";
let s = comps.somatic ? "S" : "";
let m = comps.material ? "M" : "";
let c = !!comps.concentration;
let r = !!comps.ritual;
item.powerComps = `${v}${s}${m}`;
item.powerCon = c;
item.powerRit = r;
@ -495,15 +590,15 @@ async function addFavorites(app, html, data) {
item.editable = app.options.editable;
switch (item.type) {
case 'feat':
case "feat":
if (item.flags.favtab.sort === undefined) {
item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present
}
favFeats.push(item);
break;
case 'power':
case "power":
if (item.data.preparation.mode) {
item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`
item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`;
}
if (item.data.level) {
favPowers[item.data.level].powers.push(item);
@ -529,60 +624,60 @@ async function addFavorites(app, html, data) {
// html.find('.favourite .item-controls').css('flex', '0 0 22px');
// }
let tabContainer = html.find('.favtabtarget');
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
let tabContainer = html.find(".favtabtarget");
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false;
data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false;
data.favPowers = powerCount > 0 ? favPowers : false;
data.editable = app.options.editable;
await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']);
let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data));
favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event));
await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]);
let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data));
favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event));
if (app.options.editable) {
favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev));
let handler = ev => app._onDragStart(ev);
favtabHtml.find('.item').each((i, li) => {
favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev));
let handler = (ev) => app._onDragStart(ev);
favtabHtml.find(".item").each((i, li) => {
if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false);
});
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
favtabHtml.find('.item-edit').click(ev => {
let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
favtabHtml.find(".item-edit").click((ev) => {
let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
app.actor.items.get(itemId).sheet.render(true);
});
favtabHtml.find('.item-fav').click(ev => {
let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite
favtabHtml.find(".item-fav").click((ev) => {
let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite;
app.actor.items.get(itemId).update({
"flags.favtab.isFavourite": val
});
});
// Sorting
favtabHtml.find('.item').on('drop', ev => {
favtabHtml.find(".item").on("drop", (ev) => {
ev.preventDefault();
ev.stopPropagation();
let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData('text/plain'));
let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData("text/plain"));
// if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return;
if (dropData.actorId !== app.actor.id) return;
let list = null;
if (dropData.data.type === 'feat') list = favFeats;
if (dropData.data.type === "feat") list = favFeats;
else list = favItems;
let dragSource = list.find(i => i.data._id === dropData.data._id);
let siblings = list.filter(i => i.data._id !== dropData.data._id);
let targetId = ev.target.closest('.item').dataset.itemId;
let dragTarget = siblings.find(s => s.data._id === targetId);
let dragSource = list.find((i) => i.data._id === dropData.data._id);
let siblings = list.filter((i) => i.data._id !== dropData.data._id);
let targetId = ev.target.closest(".item").dataset.itemId;
let dragTarget = siblings.find((s) => s.data._id === targetId);
if (dragTarget === undefined) return;
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
target: dragTarget,
siblings: siblings,
sortKey: 'flags.favtab.sort'
sortKey: "flags.favtab.sort"
});
const updateData = sortUpdates.map(u => {
const updateData = sortUpdates.map((u) => {
const update = u.update;
update._id = u.target.data._id;
return update;
@ -608,49 +703,47 @@ async function addFavorites(app, html, data) {
Hooks.callAll("renderedSwaltSheet", app, html, data);
}
async function addSubTabs(app, html, data) {
if(data.options.subTabs == null) {
if (data.options.subTabs == null) {
//let subTabs = []; //{subgroup: '', target: '', active: false}
data.options.subTabs = {};
html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => {
let subgroup = el.getAttribute('data-subgroup');
let target = el.getAttribute('data-target');
let targetObj = {target: target, active: el.classList.contains("active")}
if(data.options.subTabs.hasOwnProperty(subgroup)) {
html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => {
let subgroup = el.getAttribute("data-subgroup");
let target = el.getAttribute("data-target");
let targetObj = {target: target, active: el.classList.contains("active")};
if (data.options.subTabs.hasOwnProperty(subgroup)) {
data.options.subTabs[subgroup].push(targetObj);
} else {
data.options.subTabs[subgroup] = [];
data.options.subTabs[subgroup].push(targetObj);
}
})
}
for(const group in data.options.subTabs) {
data.options.subTabs[group].forEach(tab => {
if(tab.active) {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active');
} else {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active');
}
})
}
html.find('[data-subgroup-selection]').children().on('click', event => {
let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup');
let target = event.target.closest('[data-target]').getAttribute('data-target');
html.find(`[data-subgroup=${subgroup}]`).removeClass('active');
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active');
let tabId = data.options.subTabs[subgroup].find(tab => {
return tab.target == target
});
data.options.subTabs[subgroup].map(el => {
}
for (const group in data.options.subTabs) {
data.options.subTabs[group].forEach((tab) => {
if (tab.active) {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active");
} else {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active");
}
});
}
html.find("[data-subgroup-selection]")
.children()
.on("click", (event) => {
let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup");
let target = event.target.closest("[data-target]").getAttribute("data-target");
html.find(`[data-subgroup=${subgroup}]`).removeClass("active");
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active");
let tabId = data.options.subTabs[subgroup].find((tab) => {
return tab.target == target;
});
data.options.subTabs[subgroup].map((el) => {
el.active = el.target == target;
return el;
})
})
});
});
}
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {

View file

@ -6,10 +6,9 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eNPCNew extends ActorSheet5e {
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
}
/** @override */
@ -17,11 +16,13 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"],
width: 800,
tabs: [{
tabs: [
{
navSelector: ".root-tabs",
contentSelector: ".sheet-body",
initial: "attributes"
}],
}
]
});
}
@ -37,28 +38,41 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
* @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
weapons: {
label: game.i18n.localize("SW5E.AttackPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
};
// Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
let [forcepowers, techpowers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
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));
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
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);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item);
return arr;
}, [[], [], []]);
},
[[], [], []]
);
// Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
@ -70,13 +84,12 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features
for ( let item of other ) {
if ( item.type === "weapon" ) features.weapons.items.push(item);
else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item);
for (let item of other) {
if (item.type === "weapon") features.weapons.items.push(item);
else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item);
}
else features.equipment.items.push(item);
} else features.equipment.items.push(item);
}
// Assign and return
@ -107,13 +120,12 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
/** @override */
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr";
let cr = formData[crv];
cr = crs[cr] || parseFloat(cr);
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
return super._updateObject(event, formData);
@ -139,10 +151,9 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if ( !formula ) return;
if (!formula) return;
const hp = new Roll(formula).roll().total;
AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
}
}

View file

@ -6,10 +6,9 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/starship.html`;
}
/** @override */
@ -17,11 +16,13 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "starship"],
width: 800,
tabs: [{
tabs: [
{
navSelector: ".root-tabs",
contentSelector: ".sheet-body",
initial: "attributes"
}],
}
]
});
}
@ -32,29 +33,47 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
* @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: { label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), items: [], hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
starshipfeatures: { label: game.i18n.localize("SW5E.StarshipfeaturePl"), items: [], hasActions: true, dataset: {type: "starshipfeature"} },
starshipmods: { label: game.i18n.localize("SW5E.StarshipmodPl"), items: [], hasActions: false, dataset: {type: "starshipmod"} }
weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
starshipfeatures: {
label: game.i18n.localize("SW5E.StarshipfeaturePl"),
items: [],
hasActions: true,
dataset: {type: "starshipfeature"}
},
starshipmods: {
label: game.i18n.localize("SW5E.StarshipmodPl"),
items: [],
hasActions: false,
dataset: {type: "starshipmod"}
}
};
// Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
let [forcepowers, techpowers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
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));
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
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);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item);
return arr;
}, [[], [], []]);
},
[[], [], []]
);
// Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
@ -62,32 +81,28 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
other = this._filterItems(other, this._filters.features);
// Organize Powerbook
// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features
for ( let item of other ) {
if ( item.type === "weapon" ) features.weapons.items.push(item);
else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item);
for (let item of other) {
if (item.type === "weapon") features.weapons.items.push(item);
else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item);
}
else if ( item.type === "starshipfeature" ) {
} else if (item.type === "starshipfeature") {
features.starshipfeatures.items.push(item);
}
else if ( item.type === "starshipmod" ) {
} else if (item.type === "starshipmod") {
features.starshipmods.items.push(item);
}
else features.equipment.items.push(item);
} else features.equipment.items.push(item);
}
// Assign and return
data.features = Object.values(features);
// data.forcePowerbook = forcePowerbook;
// data.techPowerbook = techPowerbook;
// data.forcePowerbook = forcePowerbook;
// data.techPowerbook = techPowerbook;
}
/* -------------------------------------------- */
/** @override */
@ -115,13 +130,12 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr";
let cr = formData[crv];
cr = crs[cr] || parseFloat(cr);
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
return super._updateObject(event, formData);
@ -147,7 +161,7 @@ export default class ActorSheet5eStarship extends ActorSheet5e {
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if ( !formula ) return;
if (!formula) return;
const hp = new Roll(formula).roll().total;
AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});

View file

@ -25,13 +25,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
static get newCargo() {
return {
name: '',
name: "",
quantity: 1
};
}
@ -46,7 +45,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private
*/
_computeEncumbrance(totalWeight, actorData) {
// Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
@ -63,7 +61,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
_getMovementSpeed(actorData, largestPrimary=true) {
_getMovementSpeed(actorData, largestPrimary = true) {
return super._getMovementSpeed(actorData, largestPrimary);
}
@ -75,25 +73,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private
*/
_prepareCrewedItem(item) {
// Determine crewed status
const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? 'active' : '';
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
item.toggleClass = isCrewed ? "active" : "";
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
// Handle crew actions
if (item.type === 'feat' && item.data.activation.type === 'crew') {
if (item.type === "feat" && item.data.activation.type === "crew") {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
if (item.data.cover === .5) item.cover = '½';
else if (item.data.cover === .75) item.cover = '¾';
else if (item.data.cover === null) item.cover = '—';
if (item.crew < 1 || item.crew === null) item.crew = '—';
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
if (item.data.cover === 0.5) item.cover = "½";
else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = "—";
}
// Prepare vehicle weapons
if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
if (item.type === "equipment" || item.type === "weapon") {
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
}
}
@ -104,111 +101,125 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private
*/
_prepareItems(data) {
const cargoColumns = [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'quantity',
editable: 'Number'
}];
const cargoColumns = [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "quantity",
editable: "Number"
}
];
const equipmentColumns = [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'data.quantity'
}, {
label: game.i18n.localize('SW5E.AC'),
css: 'item-ac',
property: 'data.armor.value'
}, {
label: game.i18n.localize('SW5E.HP'),
css: 'item-hp',
property: 'data.hp.value',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Threshold'),
css: 'item-threshold',
property: 'threshold'
}];
const equipmentColumns = [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "data.quantity"
},
{
label: game.i18n.localize("SW5E.AC"),
css: "item-ac",
property: "data.armor.value"
},
{
label: game.i18n.localize("SW5E.HP"),
css: "item-hp",
property: "data.hp.value",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
const features = {
actions: {
label: game.i18n.localize('SW5E.ActionPl'),
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
crewable: true,
dataset: {type: 'feat', 'activation.type': 'crew'},
columns: [{
label: game.i18n.localize('SW5E.VehicleCrew'),
css: 'item-crew',
property: 'crew'
}, {
label: game.i18n.localize('SW5E.Cover'),
css: 'item-cover',
property: 'cover'
}]
dataset: {"type": "feat", "activation.type": "crew"},
columns: [
{
label: game.i18n.localize("SW5E.VehicleCrew"),
css: "item-crew",
property: "crew"
},
{
label: game.i18n.localize("SW5E.Cover"),
css: "item-cover",
property: "cover"
}
]
},
equipment: {
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
label: game.i18n.localize("SW5E.ItemTypeEquipment"),
items: [],
crewable: true,
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
dataset: {"type": "equipment", "armor.type": "vehicle"},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize('SW5E.Features'),
label: game.i18n.localize("SW5E.Features"),
items: [],
dataset: {type: 'feat'}
dataset: {type: "feat"}
},
reactions: {
label: game.i18n.localize('SW5E.ReactionPl'),
label: game.i18n.localize("SW5E.ReactionPl"),
items: [],
dataset: {type: 'feat', 'activation.type': 'reaction'}
dataset: {"type": "feat", "activation.type": "reaction"}
},
weapons: {
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
crewable: true,
dataset: {type: 'weapon', 'weapon-type': 'siege'},
dataset: {"type": "weapon", "weapon-type": "siege"},
columns: equipmentColumns
}
};
const cargo = {
crew: {
label: game.i18n.localize('SW5E.VehicleCrew'),
label: game.i18n.localize("SW5E.VehicleCrew"),
items: data.data.cargo.crew,
css: 'cargo-row crew',
css: "cargo-row crew",
editableName: true,
dataset: {type: 'crew'},
dataset: {type: "crew"},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize('SW5E.VehiclePassengers'),
label: game.i18n.localize("SW5E.VehiclePassengers"),
items: data.data.cargo.passengers,
css: 'cargo-row passengers',
css: "cargo-row passengers",
editableName: true,
dataset: {type: 'passengers'},
dataset: {type: "passengers"},
columns: cargoColumns
},
cargo: {
label: game.i18n.localize('SW5E.VehicleCargo'),
label: game.i18n.localize("SW5E.VehicleCargo"),
items: [],
dataset: {type: 'loot'},
columns: [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'data.quantity',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Price'),
css: 'item-price',
property: 'data.price',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Weight'),
css: 'item-weight',
property: 'data.weight',
editable: 'Number'
}]
dataset: {type: "loot"},
columns: [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "data.quantity",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Price"),
css: "item-price",
property: "data.price",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Weight"),
css: "item-weight",
property: "data.weight",
editable: "Number"
}
]
}
};
@ -219,14 +230,14 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
// Handle cargo explicitly
const isCargo = item.flags.sw5e?.vehicleCargo === true;
if ( isCargo ) {
if (isCargo) {
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
continue;
}
// Handle non-cargo item types
switch ( item.type ) {
switch (item.type) {
case "weapon":
features.weapons.items.push(item);
break;
@ -234,8 +245,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
features.equipment.items.push(item);
break;
case "feat":
if ( !item.data.activation.type || (item.data.activation.type === "none") ) features.passive.items.push(item);
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
if (!item.data.activation.type || item.data.activation.type === "none")
features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
else features.actions.items.push(item);
break;
default:
@ -259,21 +271,21 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
super.activateListeners(html);
if (!this.isEditable) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find('.item-hp input')
.click(evt => evt.target.select())
html.find(".item-toggle").click(this._onToggleItem.bind(this));
html.find(".item-hp input")
.click((evt) => evt.target.select())
.change(this._onHPChange.bind(this));
html.find('.item:not(.cargo-row) input[data-property]')
.click(evt => evt.target.select())
html.find(".item:not(.cargo-row) input[data-property]")
.click((evt) => evt.target.select())
.change(this._onEditInSheet.bind(this));
html.find('.cargo-row input')
.click(evt => evt.target.select())
html.find(".cargo-row input")
.click((evt) => evt.target.select())
.change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) {
html.find('.counter.actions, .counter.action-thresholds').hide();
html.find(".counter.actions, .counter.action-thresholds").hide();
}
}
@ -288,9 +300,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_onCargoRowChange(event) {
event.preventDefault();
const target = event.currentTarget;
const row = target.closest('.item');
const row = target.closest(".item");
const idx = Number(row.dataset.itemId);
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
const property = row.classList.contains("crew") ? "crew" : "passengers";
// Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
@ -298,10 +310,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
if (!entry) return null;
// Update the cargo value
const key = target.dataset.property || 'name';
const key = target.dataset.property || "name";
const type = target.dataset.dtype;
let value = target.value;
if (type === 'Number') value = Number(value);
if (type === "Number") value = Number(value);
entry[key] = value;
// Perform the Actor update
@ -318,14 +330,18 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/
_onEditInSheet(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value;
switch (type) {
case 'Number': value = parseInt(value); break;
case 'Boolean': value = value === 'true'; break;
case "Number":
value = parseInt(value);
break;
case "Boolean":
value = value === "true";
break;
}
return item.update({[`${property}`]: value});
}
@ -342,7 +358,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
event.preventDefault();
const target = event.currentTarget;
const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') {
if (type === "crew" || type === "passengers") {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
@ -360,10 +376,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/
_onItemDelete(event) {
event.preventDefault();
const row = event.currentTarget.closest('.item');
if (row.classList.contains('cargo-row')) {
const row = event.currentTarget.closest(".item");
if (row.classList.contains("cargo-row")) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const type = row.classList.contains("crew") ? "crew" : "passengers";
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
@ -376,7 +392,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */
async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData);
}
@ -391,11 +407,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/
_onHPChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp;
return item.update({'data.hp.value': hp});
return item.update({"data.hp.value": hp});
}
/* -------------------------------------------- */
@ -408,9 +424,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/
_onToggleItem(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed;
return item.update({'data.crewed': !crewed});
return item.update({"data.crewed": !crewed});
}
};
}

View file

@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from '../../../config.js';
import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
@ -54,10 +54,9 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`;
}
@ -65,7 +64,6 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
getData(options) {
// Basic data
let isOwner = this.actor.isOwner;
const data = {
@ -77,7 +75,7 @@ export default class ActorSheet5e extends ActorSheet {
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === 'vehicle',
isVehicle: this.actor.type === "vehicle",
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
@ -89,7 +87,7 @@ export default class ActorSheet5e extends ActorSheet {
// Owned Items
data.items = actorData.items;
for ( let i of data.items ) {
for (let i of data.items) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
@ -100,7 +98,7 @@ export default class ActorSheet5e extends ActorSheet {
data.filters = this._filters;
// Ability Scores
for ( let [a, abl] of Object.entries(actorData.data.abilities)) {
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
@ -108,7 +106,7 @@ export default class ActorSheet5e extends ActorSheet {
// Skills
if (actorData.data.skills) {
for ( let [s, skl] of Object.entries(actorData.data.skills)) {
for (let [s, skl] of Object.entries(actorData.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
@ -132,7 +130,7 @@ export default class ActorSheet5e extends ActorSheet {
data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet
return data
return data;
}
/* -------------------------------------------- */
@ -144,38 +142,42 @@ export default class ActorSheet5e extends ActorSheet {
* @returns {{primary: string, special: string}}
* @private
*/
_getMovementSpeed(actorData, largestPrimary=false) {
_getMovementSpeed(actorData, largestPrimary = false) {
const movement = actorData.data.attributes.movement || {};
// Prepare an array of available movement speeds
let 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.fly,
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
]
if ( largestPrimary ) {
];
if (largestPrimary) {
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
}
// Filter and sort speeds on their values
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
// Case 1: Largest as primary
if ( largestPrimary ) {
if (largestPrimary) {
let primary = speeds.shift();
return {
primary: `${primary ? primary[1] : "0"} ${movement.units}`,
special: speeds.map(s => s[1]).join(", ")
}
special: speeds.map((s) => s[1]).join(", ")
};
}
// Case 2: Walk as primary
else {
return {
primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
}
special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
};
}
}
@ -184,12 +186,12 @@ export default class ActorSheet5e extends ActorSheet {
_getSenses(actorData) {
const senses = actorData.data.attributes.senses || {};
const tags = {};
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[k] ?? 0
if ( v === 0 ) continue;
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[k] ?? 0;
if (v === 0) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
}
if ( !!senses.special ) tags["special"] = senses.special;
if (!!senses.special) tags["special"] = senses.special;
return tags;
}
@ -202,20 +204,20 @@ export default class ActorSheet5e extends ActorSheet {
*/
_prepareTraits(traits) {
const map = {
"dr": CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies,
"weaponProf": CONFIG.SW5E.weaponProficiencies,
"toolProf": CONFIG.SW5E.toolProficiencies
dr: CONFIG.SW5E.damageResistanceTypes,
di: CONFIG.SW5E.damageResistanceTypes,
dv: CONFIG.SW5E.damageResistanceTypes,
ci: CONFIG.SW5E.conditionTypes,
languages: CONFIG.SW5E.languages,
armorProf: CONFIG.SW5E.armorProficiencies,
weaponProf: CONFIG.SW5E.weaponProficiencies,
toolProf: CONFIG.SW5E.toolProficiencies
};
for ( let [t, choices] of Object.entries(map) ) {
for (let [t, choices] of Object.entries(map)) {
const trait = traits[t];
if ( !trait ) continue;
if (!trait) continue;
let values = [];
if ( trait.value ) {
if (trait.value) {
values = trait.value instanceof Array ? trait.value : [trait.value];
}
trait.selected = values.reduce((obj, t) => {
@ -224,8 +226,8 @@ export default class ActorSheet5e extends ActorSheet {
}, {});
// Add custom entry
if ( trait.custom ) {
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
if (trait.custom) {
trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
@ -246,9 +248,9 @@ export default class ActorSheet5e extends ActorSheet {
// Define some mappings
const sections = {
"atwill": -20,
"innate": -10,
"pact": 0.5
atwill: -20,
innate: -10,
pact: 0.5
};
// Label power slot uses headers
@ -259,13 +261,13 @@ export default class ActorSheet5e extends ActorSheet {
};
// Format a powerbook entry for a certain indexed level
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => {
powerbook[i] = {
order: i,
label: label,
usesSlots: i > 0,
canCreate: owner,
canPrepare: (data.actor.type === "character") && (i >= 1),
canPrepare: data.actor.type === "character" && i >= 1,
powers: [],
uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0,
@ -277,14 +279,14 @@ export default class ActorSheet5e extends ActorSheet {
// Determine the maximum power level which has a slot
const maxLevel = Array.fromRange(10).reduce((max, i) => {
if ( i === 0 ) return max;
if (i === 0) return max;
const level = levels[`power${i}`];
if ( (level.max || level.override ) && ( i > max ) ) max = i;
if ((level.max || level.override) && i > max) max = i;
return max;
}, 0);
// Level-based powercasters have cantrips and leveled slots
if ( maxLevel > 0 ) {
if (maxLevel > 0) {
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
for (let lvl = 1; lvl <= maxLevel; lvl++) {
const sl = `power${lvl}`;
@ -294,8 +296,8 @@ export default class ActorSheet5e extends ActorSheet {
// Pact magic users have cantrips and a pact magic section
// TODO: Check if this is needed, we've removed pacts everywhere else
if ( levels.pact && levels.pact.max ) {
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
if (levels.pact && levels.pact.max) {
if (!powerbook["0"]) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
const l = levels.pact;
const config = CONFIG.SW5E.powerPreparationModes.pact;
const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
@ -309,15 +311,15 @@ export default class ActorSheet5e extends ActorSheet {
}
// Iterate over every power item, adding powers to the powerbook by section
powers.forEach(power => {
powers.forEach((power) => {
const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0;
const sl = `power${s}`;
// Specialized powercasting modes (if they exist)
if ( mode in sections ) {
if (mode in sections) {
s = sections[mode];
if ( !powerbook[s] ){
if (!powerbook[s]) {
const l = levels[mode] || {};
const config = CONFIG.SW5E.powerPreparationModes[mode];
registerSection(mode, s, config, {
@ -330,7 +332,7 @@ export default class ActorSheet5e extends ActorSheet {
}
// Sections for higher-level powers which the caster "should not" have, but power items exist for
else if ( !powerbook[s] ) {
else if (!powerbook[s]) {
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
}
@ -352,32 +354,32 @@ export default class ActorSheet5e extends ActorSheet {
* @private
*/
_filterItems(items, filters) {
return items.filter(item => {
return items.filter((item) => {
const data = item.data;
// Action usage
for ( let f of ["action", "bonus", "reaction"] ) {
if ( filters.has(f) ) {
if ((data.activation && (data.activation.type !== f))) return false;
for (let f of ["action", "bonus", "reaction"]) {
if (filters.has(f)) {
if (data.activation && data.activation.type !== f) return false;
}
}
// Power-specific filters
if ( filters.has("ritual") ) {
if (filters.has("ritual")) {
if (data.components.ritual !== true) return false;
}
if ( filters.has("concentration") ) {
if (filters.has("concentration")) {
if (data.components.concentration !== true) return false;
}
if ( filters.has("prepared") ) {
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
if ( this.actor.data.type === "npc" ) return true;
if (filters.has("prepared")) {
if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
if (this.actor.data.type === "npc") return true;
return data.preparation.prepared;
}
// Equipment-specific filters
if ( filters.has("equipped") ) {
if ( data.equipped !== true ) return false;
if (filters.has("equipped")) {
if (data.equipped !== true) return false;
}
return true;
});
@ -405,61 +407,59 @@ export default class ActorSheet5e extends ActorSheet {
/** @inheritdoc */
activateListeners(html) {
// Activate Item Filters
const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
// View Item Sheets
html.find('.item-edit').click(this._onItemEdit.bind(this));
html.find(".item-edit").click(this._onItemEdit.bind(this));
// Editable Only Listeners
if ( this.isEditable ) {
if (this.isEditable) {
// Input focus and update
const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select());
inputs.focus((ev) => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency
html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find('.config-button').click(this._onConfigMenu.bind(this));
html.find(".config-button").click(this._onConfigMenu.bind(this));
// Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this));
html.find('.item-delete').click(this._onItemDelete.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
html.find(".item-create").click(this._onItemCreate.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this));
html.find(".item-uses input")
.click((ev) => ev.target.select())
.change(this._onUsesChange.bind(this));
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
if ( this.actor.isOwner ) {
if (this.actor.isOwner) {
// Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
// Item Rolling
html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
html.find(".item .item-image").click((event) => this._onItemRoll(event));
html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
}
// Otherwise remove rollable classes
@ -480,8 +480,8 @@ export default class ActorSheet5e extends ActorSheet {
_initializeFilterItemList(i, ul) {
const set = this._filters[ul.dataset.filter];
const filters = ul.querySelectorAll(".filter-item");
for ( let li of filters ) {
if ( set.has(li.dataset.filter) ) li.classList.add("active");
for (let li of filters) {
if (set.has(li.dataset.filter)) li.classList.add("active");
}
}
@ -497,10 +497,10 @@ export default class ActorSheet5e extends ActorSheet {
_onChangeInputDelta(event) {
const input = event.target;
const value = input.value;
if ( ["+", "-"].includes(value[0]) ) {
if (["+", "-"].includes(value[0])) {
let delta = parseFloat(value);
input.value = getProperty(this.actor.data, input.name) + delta;
} else if ( value[0] === "=" ) {
} else if (value[0] === "=") {
input.value = value.slice(1);
}
}
@ -516,7 +516,7 @@ export default class ActorSheet5e extends ActorSheet {
event.preventDefault();
const button = event.currentTarget;
let app;
switch ( button.dataset.action ) {
switch (button.dataset.action) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
@ -553,10 +553,10 @@ export default class ActorSheet5e extends ActorSheet {
let idx = levels.indexOf(level);
// Toggle next level - forward on click, backwards on right
if ( event.type === "click" ) {
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
} else if ( event.type === "contextmenu" ) {
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
if (event.type === "click") {
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
} else if (event.type === "contextmenu") {
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
}
// Update the field value and save the form
@ -567,49 +567,51 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
if (!canPolymorph) return false;
// Get the target actor
let sourceActor = null;
if (data.pack) {
const pack = game.packs.find(p => p.collection === data.pack);
const pack = game.packs.find((p) => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id);
} else {
sourceActor = game.actors.get(data.id);
}
if ( !sourceActor ) return;
if (!sourceActor) return;
// Define a function to record polymorph settings for future use
const rememberOptions = html => {
const rememberOptions = (html) => {
const options = {};
html.find('input').each((i, el) => {
html.find("input").each((i, el) => {
options[el.name] = el.checked;
});
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
game.settings.set('sw5e', 'polymorphSettings', settings);
const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
game.settings.set("sw5e", "polymorphSettings", settings);
return settings;
};
// Create and render the Dialog
return new Dialog({
title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
return new Dialog(
{
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
content: {
options: game.settings.get('sw5e', 'polymorphSettings'),
options: game.settings.get("sw5e", "polymorphSettings"),
i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken
},
default: 'accept',
default: "accept",
buttons: {
accept: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
},
wildshape: {
icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: html => this.actor.transformInto(sourceActor, {
label: game.i18n.localize("SW5E.PolymorphWildShape"),
callback: (html) =>
this.actor.transformInto(sourceActor, {
keepBio: true,
keepClass: true,
keepMental: true,
@ -620,61 +622,64 @@ export default class ActorSheet5e extends ActorSheet {
},
polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize('SW5E.Polymorph'),
callback: html => this.actor.transformInto(sourceActor, {
label: game.i18n.localize("SW5E.Polymorph"),
callback: (html) =>
this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens
})
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('Cancel')
label: game.i18n.localize("Cancel")
}
}
}, {
classes: ['dialog', 'sw5e'],
},
{
classes: ["dialog", "sw5e"],
width: 600,
template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
}).render(true);
template: "systems/sw5e/templates/apps/polymorph-prompt.html"
}
).render(true);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor
if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", {
if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
return ui.notifications.warn(
game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
}));
})
);
}
// Create a Consumable power scroll on the Inventory tab
// TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
if (itemData.type === "power" && this._tabs[0].active === "inventory") {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
if ( itemData.data ) {
if (itemData.data) {
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) {
const similarItem = this.actor.items.find(i => {
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && (sourceId === itemData.flags.core?.sourceId) &&
(i.type === "consumable");
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
});
if ( similarItem ) {
if (similarItem) {
return similarItem.update({
'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
"data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
@ -690,7 +695,7 @@ export default class ActorSheet5e extends ActorSheet {
* @param {MouseEvent} event The originating click event
* @private
*/
async _onPowerSlotOverride (event) {
async _onPowerSlotOverride(event) {
const span = event.currentTarget.parentElement;
const level = span.dataset.level;
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
@ -720,7 +725,7 @@ export default class ActorSheet5e extends ActorSheet {
const item = this.actor.items.get(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({ 'data.uses.value': uses });
return item.update({"data.uses.value": uses});
}
/* -------------------------------------------- */
@ -748,7 +753,7 @@ export default class ActorSheet5e extends ActorSheet {
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId);
return item.rollRecharge();
};
}
/* -------------------------------------------- */
@ -763,13 +768,13 @@ export default class ActorSheet5e extends ActorSheet {
chatData = item.getChatData({secrets: this.actor.isOwner});
// Toggle summary
if ( li.hasClass("expanded") ) {
if (li.hasClass("expanded")) {
let summary = li.children(".item-summary");
summary.slideUp(200, () => summary.remove());
} else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
chatData.properties.forEach((p) => props.append(`<span class="tag">${p}</span>`));
div.append(props);
li.append(div.hide());
div.slideDown(200);
@ -822,7 +827,7 @@ export default class ActorSheet5e extends ActorSheet {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.items.get(li.dataset.itemId);
if ( item ) return item.delete();
if (item) return item.delete();
}
/* -------------------------------------------- */
@ -876,7 +881,7 @@ export default class ActorSheet5e extends ActorSheet {
const li = event.currentTarget;
const set = this._filters[li.parentElement.dataset.filter];
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
if (set.has(filter)) set.delete(filter);
else set.add(filter);
return this.render();
}
@ -893,8 +898,8 @@ export default class ActorSheet5e extends ActorSheet {
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices };
return new TraitSelector(this.actor, options).render(true)
const options = {name: a.dataset.target, title: label.innerText, choices};
return new TraitSelector(this.actor, options).render(true);
}
/* -------------------------------------------- */
@ -902,9 +907,9 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
if ( this.actor.isPolymorphed ) {
if (this.actor.isPolymorphed) {
buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation',
label: "SW5E.PolymorphRestoreTransformation",
class: "restore-transformation",
icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm()

View file

@ -7,7 +7,6 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e}
*/
export default class ActorSheet5eCharacter extends ActorSheet5e {
/**
* Define default rendering options for the NPC sheet
* @return {Object}
@ -37,7 +36,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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());
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]);
@ -45,10 +44,12 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// 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(', ');
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;
@ -61,23 +62,34 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
* @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"} }
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) => {
let [
items,
powers,
feats,
classes,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = 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.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
icon: "fa-sun",
@ -92,31 +104,35 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}[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.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 );
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 ( 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);
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);
@ -124,7 +140,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
feats = this._filterItems(feats, this._filters.features);
// Organize items
for ( let i of 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);
@ -133,25 +149,78 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// 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;
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: true, 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"} }
classes: {
label: "SW5E.ItemTypeClassPl",
items: [],
hasActions: false,
dataset: {type: "class"},
isClass: true
},
classfeatures: {
label: "SW5E.ItemTypeClassFeats",
items: [],
hasActions: true,
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);
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);
@ -183,12 +252,11 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
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;
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 {
} else {
const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
@ -205,14 +273,14 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
*/
activateListeners(html) {
super.activateListeners(html);
if ( !this.isEditable ) return;
if (!this.isEditable) return;
// Item State Toggling
html.find('.item-toggle').click(this._onToggleItem.bind(this));
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));
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));
@ -228,7 +296,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
_onSheetAction(event) {
event.preventDefault();
const button = event.currentTarget;
switch( button.dataset.action ) {
switch (button.dataset.action) {
case "rollDeathSave":
return this.actor.rollDeathSave({event: event});
case "rollInitiative":
@ -281,14 +349,13 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/** @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);
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 ) {
if (!!cls) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
if ( next > priorLevel ) {
if (next > priorLevel) {
itemData.levels = next;
return cls.update({"data.levels": next});
}

View file

@ -6,7 +6,6 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
@ -28,27 +27,40 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
* @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
weapons: {
label: game.i18n.localize("SW5E.AttackPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
};
// Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce((arr, item) => {
let [powers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
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));
if ( item.type === "power" ) arr[0].push(item);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
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);
if (item.type === "power") arr[0].push(item);
else arr[1].push(item);
return arr;
}, [[], []]);
},
[[], []]
);
// Apply item filters
powers = this._filterItems(powers, this._filters.powerbook);
@ -58,13 +70,12 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
const powerbook = this._preparePowerbook(data, powers);
// Organize Features
for ( let item of other ) {
if ( item.type === "weapon" ) features.weapons.items.push(item);
else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item);
for (let item of other) {
if (item.type === "weapon") features.weapons.items.push(item);
else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item);
}
else features.equipment.items.push(item);
} else features.equipment.items.push(item);
}
// Assign and return
@ -72,7 +83,6 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
data.powerbook = powerbook;
}
/* -------------------------------------------- */
/** @inheritdoc */
@ -95,13 +105,12 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr";
let cr = formData[crv];
cr = crs[cr] || parseFloat(cr);
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
return super._updateObject(event, formData);
@ -127,7 +136,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if ( !formula ) return;
if (!formula) return;
const hp = new Roll(formula).roll().total;
AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});

View file

@ -25,13 +25,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
static get newCargo() {
return {
name: '',
name: "",
quantity: 1
};
}
@ -46,7 +45,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private
*/
_computeEncumbrance(totalWeight, actorData) {
// Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
@ -63,7 +61,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
_getMovementSpeed(actorData, largestPrimary=true) {
_getMovementSpeed(actorData, largestPrimary = true) {
return super._getMovementSpeed(actorData, largestPrimary);
}
@ -75,25 +73,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private
*/
_prepareCrewedItem(item) {
// Determine crewed status
const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? 'active' : '';
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
item.toggleClass = isCrewed ? "active" : "";
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
// Handle crew actions
if (item.type === 'feat' && item.data.activation.type === 'crew') {
if (item.type === "feat" && item.data.activation.type === "crew") {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
if (item.data.cover === .5) item.cover = '½';
else if (item.data.cover === .75) item.cover = '¾';
else if (item.data.cover === null) item.cover = '—';
if (item.crew < 1 || item.crew === null) item.crew = '—';
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
if (item.data.cover === 0.5) item.cover = "½";
else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = "—";
}
// Prepare vehicle weapons
if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
if (item.type === "equipment" || item.type === "weapon") {
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
}
}
@ -104,111 +101,125 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private
*/
_prepareItems(data) {
const cargoColumns = [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'quantity',
editable: 'Number'
}];
const cargoColumns = [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "quantity",
editable: "Number"
}
];
const equipmentColumns = [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'data.quantity'
}, {
label: game.i18n.localize('SW5E.AC'),
css: 'item-ac',
property: 'data.armor.value'
}, {
label: game.i18n.localize('SW5E.HP'),
css: 'item-hp',
property: 'data.hp.value',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Threshold'),
css: 'item-threshold',
property: 'threshold'
}];
const equipmentColumns = [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "data.quantity"
},
{
label: game.i18n.localize("SW5E.AC"),
css: "item-ac",
property: "data.armor.value"
},
{
label: game.i18n.localize("SW5E.HP"),
css: "item-hp",
property: "data.hp.value",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
const features = {
actions: {
label: game.i18n.localize('SW5E.ActionPl'),
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
crewable: true,
dataset: {type: 'feat', 'activation.type': 'crew'},
columns: [{
label: game.i18n.localize('SW5E.VehicleCrew'),
css: 'item-crew',
property: 'crew'
}, {
label: game.i18n.localize('SW5E.Cover'),
css: 'item-cover',
property: 'cover'
}]
dataset: {"type": "feat", "activation.type": "crew"},
columns: [
{
label: game.i18n.localize("SW5E.VehicleCrew"),
css: "item-crew",
property: "crew"
},
{
label: game.i18n.localize("SW5E.Cover"),
css: "item-cover",
property: "cover"
}
]
},
equipment: {
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
label: game.i18n.localize("SW5E.ItemTypeEquipment"),
items: [],
crewable: true,
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
dataset: {"type": "equipment", "armor.type": "vehicle"},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize('SW5E.Features'),
label: game.i18n.localize("SW5E.Features"),
items: [],
dataset: {type: 'feat'}
dataset: {type: "feat"}
},
reactions: {
label: game.i18n.localize('SW5E.ReactionPl'),
label: game.i18n.localize("SW5E.ReactionPl"),
items: [],
dataset: {type: 'feat', 'activation.type': 'reaction'}
dataset: {"type": "feat", "activation.type": "reaction"}
},
weapons: {
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
crewable: true,
dataset: {type: 'weapon', 'weapon-type': 'siege'},
dataset: {"type": "weapon", "weapon-type": "siege"},
columns: equipmentColumns
}
};
const cargo = {
crew: {
label: game.i18n.localize('SW5E.VehicleCrew'),
label: game.i18n.localize("SW5E.VehicleCrew"),
items: data.data.cargo.crew,
css: 'cargo-row crew',
css: "cargo-row crew",
editableName: true,
dataset: {type: 'crew'},
dataset: {type: "crew"},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize('SW5E.VehiclePassengers'),
label: game.i18n.localize("SW5E.VehiclePassengers"),
items: data.data.cargo.passengers,
css: 'cargo-row passengers',
css: "cargo-row passengers",
editableName: true,
dataset: {type: 'passengers'},
dataset: {type: "passengers"},
columns: cargoColumns
},
cargo: {
label: game.i18n.localize('SW5E.VehicleCargo'),
label: game.i18n.localize("SW5E.VehicleCargo"),
items: [],
dataset: {type: 'loot'},
columns: [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'data.quantity',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Price'),
css: 'item-price',
property: 'data.price',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Weight'),
css: 'item-weight',
property: 'data.weight',
editable: 'Number'
}]
dataset: {type: "loot"},
columns: [
{
label: game.i18n.localize("SW5E.Quantity"),
css: "item-qty",
property: "data.quantity",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Price"),
css: "item-price",
property: "data.price",
editable: "Number"
},
{
label: game.i18n.localize("SW5E.Weight"),
css: "item-weight",
property: "data.weight",
editable: "Number"
}
]
}
};
@ -219,14 +230,14 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
// Handle cargo explicitly
const isCargo = item.flags.sw5e?.vehicleCargo === true;
if ( isCargo ) {
if (isCargo) {
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
continue;
}
// Handle non-cargo item types
switch ( item.type ) {
switch (item.type) {
case "weapon":
features.weapons.items.push(item);
break;
@ -234,8 +245,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
features.equipment.items.push(item);
break;
case "feat":
if (!item.data.activation.type || (item.data.activation.type === "none")) features.passive.items.push(item);
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
if (!item.data.activation.type || item.data.activation.type === "none")
features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
else features.actions.items.push(item);
break;
default:
@ -259,21 +271,21 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
super.activateListeners(html);
if (!this.isEditable) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find('.item-hp input')
.click(evt => evt.target.select())
html.find(".item-toggle").click(this._onToggleItem.bind(this));
html.find(".item-hp input")
.click((evt) => evt.target.select())
.change(this._onHPChange.bind(this));
html.find('.item:not(.cargo-row) input[data-property]')
.click(evt => evt.target.select())
html.find(".item:not(.cargo-row) input[data-property]")
.click((evt) => evt.target.select())
.change(this._onEditInSheet.bind(this));
html.find('.cargo-row input')
.click(evt => evt.target.select())
html.find(".cargo-row input")
.click((evt) => evt.target.select())
.change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) {
html.find('.counter.actions, .counter.action-thresholds').hide();
html.find(".counter.actions, .counter.action-thresholds").hide();
}
}
@ -288,9 +300,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_onCargoRowChange(event) {
event.preventDefault();
const target = event.currentTarget;
const row = target.closest('.item');
const row = target.closest(".item");
const idx = Number(row.dataset.itemId);
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
const property = row.classList.contains("crew") ? "crew" : "passengers";
// Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
@ -298,10 +310,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
if (!entry) return null;
// Update the cargo value
const key = target.dataset.property || 'name';
const key = target.dataset.property || "name";
const type = target.dataset.dtype;
let value = target.value;
if (type === 'Number') value = Number(value);
if (type === "Number") value = Number(value);
entry[key] = value;
// Perform the Actor update
@ -318,14 +330,18 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/
_onEditInSheet(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value;
switch (type) {
case 'Number': value = parseInt(value); break;
case 'Boolean': value = value === 'true'; break;
case "Number":
value = parseInt(value);
break;
case "Boolean":
value = value === "true";
break;
}
return item.update({[`${property}`]: value});
}
@ -342,7 +358,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
event.preventDefault();
const target = event.currentTarget;
const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') {
if (type === "crew" || type === "passengers") {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
@ -360,10 +376,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/
_onItemDelete(event) {
event.preventDefault();
const row = event.currentTarget.closest('.item');
if (row.classList.contains('cargo-row')) {
const row = event.currentTarget.closest(".item");
if (row.classList.contains("cargo-row")) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const type = row.classList.contains("crew") ? "crew" : "passengers";
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
@ -376,7 +392,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */
async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData);
}
@ -391,11 +407,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/
_onHPChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp;
return item.update({'data.hp.value': hp});
return item.update({"data.hp.value": hp});
}
/* -------------------------------------------- */
@ -408,9 +424,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/
_onToggleItem(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed;
return item.update({'data.crewed': !crewed});
return item.update({"data.crewed": !crewed});
}
};
}

View file

@ -3,7 +3,7 @@
* @type {Dialog}
*/
export default class AbilityUseDialog extends Dialog {
constructor(item, dialogData={}, options={}) {
constructor(item, dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog"];
@ -25,7 +25,7 @@ export default class AbilityUseDialog extends Dialog {
* @return {Promise}
*/
static async create(item) {
if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item");
// Prepare data
const actorData = item.actor.data.data;
@ -39,17 +39,20 @@ export default class AbilityUseDialog extends Dialog {
// Prepare dialog form data
const data = {
item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", {type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), name: item.name}),
title: game.i18n.format("SW5E.AbilityUseHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
name: item.name
}),
note: this._getAbilityUseNote(item.data, uses, recharge),
consumePowerSlot: false,
consumeRecharge: recharges,
consumeResource: !!itemData.consume.target,
consumeUses: uses.per && (uses.max > 0),
consumeUses: uses.per && uses.max > 0,
canUse: recharges ? recharge.charged : sufficientUses,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: []
};
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
if (item.data.type === "power") this._getPowerData(actorData, itemData, data);
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
@ -65,7 +68,7 @@ export default class AbilityUseDialog extends Dialog {
use: {
icon: `<i class="fas ${icon}"></i>`,
label: label,
callback: html => {
callback: (html) => {
const fd = new FormDataExtended(html[0].querySelector("form"));
resolve(fd.toObject());
}
@ -87,14 +90,13 @@ export default class AbilityUseDialog extends Dialog {
* @private
*/
static _getPowerData(actorData, itemData, data) {
// Determine whether the power may be up-cast
const lvl = itemData.level;
const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// If can't upcast, return early and don't bother calculating available power slots
if (!consumePowerSlot) {
mergeObject(data, { isPower: true, consumePowerSlot });
mergeObject(data, {isPower: true, consumePowerSlot});
return;
}
@ -102,74 +104,78 @@ export default class AbilityUseDialog extends Dialog {
let lmax = 0;
let points;
let powerType;
switch (itemData.school){
switch (itemData.school) {
case "lgt":
case "uni":
case "drk": {
powerType = "force"
powerType = "force";
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
break;
}
case "tec": {
powerType = "tech"
powerType = "tech";
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
break;
}
}
// eliminate point usage for innate casters
if (actorData.attributes.powercasting === 'innate') points = 999;
if (actorData.attributes.powercasting === "innate") points = 999;
let powerLevels
if (powerType === "force"){
powerLevels = Array.fromRange(10).reduce((arr, i) => {
if ( i < lvl ) return arr;
let powerLevels;
if (powerType === "force") {
powerLevels = Array.fromRange(10)
.reduce((arr, i) => {
if (i < lvl) return arr;
const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power"+i] || {fmax: 0, foverride: null};
const l = actorData.powers["power" + i] || {fmax: 0, foverride: null};
let max = parseInt(l.foverride || l.fmax || 0);
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
if ( max > 0 ) lmax = i;
if ((max > 0) && (slots > 0) && (points > i)){
if (max > 0) lmax = i;
if (max > 0 && slots > 0 && points > i) {
arr.push({
level: i,
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
canCast: max > 0,
hasSlots: slots > 0
});
}
return arr;
}, []).filter(sl => sl.level <= lmax);
}else if (powerType === "tech"){
powerLevels = Array.fromRange(10).reduce((arr, i) => {
if ( i < lvl ) return arr;
}, [])
.filter((sl) => sl.level <= lmax);
} else if (powerType === "tech") {
powerLevels = Array.fromRange(10)
.reduce((arr, i) => {
if (i < lvl) return arr;
const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power"+i] || {tmax: 0, toverride: null};
const l = actorData.powers["power" + i] || {tmax: 0, toverride: null};
let max = parseInt(l.override || l.tmax || 0);
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
if ( max > 0 ) lmax = i;
if ((max > 0) && (slots > 0) && (points > i)){
if (max > 0) lmax = i;
if (max > 0 && slots > 0 && points > i) {
arr.push({
level: i,
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
canCast: max > 0,
hasSlots: slots > 0
});
}
return arr;
}, []).filter(sl => sl.level <= lmax);
}, [])
.filter((sl) => sl.level <= lmax);
}
const canCast = powerLevels.some(l => l.hasSlots);
if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", {
const canCast = powerLevels.some((l) => l.hasSlots);
if (!canCast)
data.errors.push(
game.i18n.format("SW5E.PowerCastNoSlots", {
level: CONFIG.SW5E.powerLevels[lvl],
name: data.item.name
}));
})
);
// Merge power casting data
return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
}
/* -------------------------------------------- */
@ -179,27 +185,26 @@ export default class AbilityUseDialog extends Dialog {
* @private
*/
static _getAbilityUseNote(item, uses, recharge) {
// Zero quantity
const quantity = item.data.quantity;
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
// Abilities which use Recharge
if ( !!recharge.value ) {
if (!!recharge.value) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
})
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
});
}
// Does not use any resource
if ( !uses.per || !uses.max ) return "";
if (!uses.per || !uses.max) return "";
// Consumables
if ( item.type === "consumable" ) {
if (item.type === "consumable") {
let str = "SW5E.AbilityUseNormalHint";
if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint";
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
if (uses.value > 1) str = "SW5E.AbilityUseConsumableChargeHint";
else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint";
else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint";
return game.i18n.format(str, {
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
value: uses.value,

View file

@ -17,7 +17,7 @@ export default class ActorSheetFlags extends DocumentSheet {
/** @override */
get title() {
return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
}
/* -------------------------------------------- */
@ -40,8 +40,10 @@ export default class ActorSheetFlags extends DocumentSheet {
* @private
*/
_getClasses() {
const classes = this.object.items.filter(i => i.type === "class");
return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => {
const classes = this.object.items.filter((i) => i.type === "class");
return classes
.sort((a, b) => a.name.localeCompare(b.name))
.reduce((obj, i) => {
obj[i.id] = i.name;
return obj;
}, {});
@ -58,12 +60,12 @@ export default class ActorSheetFlags extends DocumentSheet {
_getFlags() {
const flags = {};
const baseData = this.document.toJSON();
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) {
if (!flags.hasOwnProperty(v.section)) flags[v.section] = {};
let flag = foundry.utils.deepClone(v);
flag.type = v.type.name;
flag.isCheckbox = v.type === Boolean;
flag.isSelect = v.hasOwnProperty('choices');
flag.isSelect = v.hasOwnProperty("choices");
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
flags[v.section][`flags.sw5e.${k}`] = flag;
}
@ -96,7 +98,7 @@ export default class ActorSheetFlags extends DocumentSheet {
{name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
{name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
];
for ( let b of bonuses ) {
for (let b of bonuses) {
b.value = getProperty(this.object._data, b.name) || "";
}
return bonuses;
@ -113,11 +115,11 @@ export default class ActorSheetFlags extends DocumentSheet {
let unset = false;
const flags = updateData.flags.sw5e;
//clone flags to dnd5e for module compatability
updateData.flags.dnd5e = updateData.flags.sw5e
for ( let [k, v] of Object.entries(flags) ) {
if ( [undefined, null, "", false, 0].includes(v) ) {
updateData.flags.dnd5e = updateData.flags.sw5e;
for (let [k, v] of Object.entries(flags)) {
if ([undefined, null, "", false, 0].includes(v)) {
delete flags[k];
if ( hasProperty(actor._data.flags, `sw5e.${k}`) ) {
if (hasProperty(actor._data.flags, `sw5e.${k}`)) {
unset = true;
flags[`-=${k}`] = null;
}
@ -125,8 +127,8 @@ export default class ActorSheetFlags extends DocumentSheet {
}
// Clear any bonuses which are whitespace only
for ( let b of Object.values(updateData.data.bonuses ) ) {
for ( let [k, v] of Object.entries(b) ) {
for (let b of Object.values(updateData.data.bonuses)) {
for (let [k, v] of Object.entries(b)) {
b[k] = v.trim();
}
}

View file

@ -5,7 +5,6 @@ import Actor5e from "../actor/entity.js";
* @extends {FormApplication}
*/
export default class ActorTypeConfig extends FormApplication {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
@ -32,11 +31,11 @@ export default class ActorTypeConfig extends FormApplication {
/** @override */
getData(options) {
// Get current value or new default
let attr = foundry.utils.getProperty(this.object.data.data, 'details.type');
if ( foundry.utils.getType(attr) !== "Object" ) attr = {
value: (attr in CONFIG.SW5E.creatureTypes) ? attr : "humanoid",
let attr = foundry.utils.getProperty(this.object.data.data, "details.type");
if (foundry.utils.getType(attr) !== "Object")
attr = {
value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid",
subtype: "",
swarm: "",
custom: ""
@ -44,11 +43,11 @@ export default class ActorTypeConfig extends FormApplication {
// Populate choices
const types = {};
for ( let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes) ) {
for (let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes)) {
types[k] = {
label: game.i18n.localize(v),
chosen: attr.value === k
}
};
}
// Return data for rendering
@ -61,12 +60,14 @@ export default class ActorTypeConfig extends FormApplication {
},
subtype: attr.subtype,
swarm: attr.swarm,
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)).reverse().reduce((obj, e) => {
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes))
.reverse()
.reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {}),
preview: Actor5e.formatCreatureType(attr) || ""
}
};
}
/* -------------------------------------------- */
@ -74,7 +75,7 @@ export default class ActorTypeConfig extends FormApplication {
/** @override */
async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData);
return this.object.update({ 'data.details.type': typeObject });
return this.object.update({"data.details.type": typeObject});
}
/* -------------------------------------------- */

View file

@ -3,7 +3,6 @@
* @implements {DocumentSheet}
*/
export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
@ -26,7 +25,8 @@ export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */
getData(options) {
return {
classes: this.object.items.reduce((classes, item) => {
classes: this.object.items
.reduce((classes, item) => {
if (item.data.type === "class") {
// Add the appropriate data only if this item is a "class"
classes.push({
@ -35,11 +35,12 @@ export default class ActorHitDiceConfig extends DocumentSheet {
diceDenom: item.data.data.hitDice,
currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
maxHitDice: item.data.data.levels,
canRoll: (item.data.data.levels - item.data.data.hitDiceUsed) > 0
canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0
});
}
return classes;
}, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
}, [])
.sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
};
}
@ -50,7 +51,7 @@ export default class ActorHitDiceConfig extends DocumentSheet {
super.activateListeners(html);
// Hook up -/+ buttons to adjust the current value in the form
html.find("button.increment,button.decrement").click(event => {
html.find("button.increment,button.decrement").click((event) => {
const button = event.currentTarget;
const current = button.parentElement.querySelector(".current");
const max = button.parentElement.querySelector(".max");
@ -67,8 +68,8 @@ export default class ActorHitDiceConfig extends DocumentSheet {
async _updateObject(event, formData) {
const actorItems = this.object.items;
const classUpdates = Object.entries(formData).map(([id, hd]) => ({
_id: id,
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd,
"_id": id,
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd
}));
return this.object.updateEmbeddedDocuments("Item", classUpdates);
}

View file

@ -37,7 +37,7 @@ export default class LongRestDialog extends Dialog {
* @param {Actor5e} actor
* @return {Promise}
*/
static async longRestDialog({ actor } = {}) {
static async longRestDialog({actor} = {}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: game.i18n.localize("SW5E.LongRest"),
@ -45,7 +45,7 @@ export default class LongRestDialog extends Dialog {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"),
callback: html => {
callback: (html) => {
let newDay = true;
if (game.settings.get("sw5e", "restVariant") !== "gritty")
newDay = html.find('input[name="newDay"]')[0].checked;
@ -58,7 +58,7 @@ export default class LongRestDialog extends Dialog {
callback: reject
}
},
default: 'rest',
default: "rest",
close: reject
});
dlg.render(true);

View file

@ -3,7 +3,6 @@
* @extends {DocumentSheet}
*/
export default class ActorMovementConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
@ -30,8 +29,8 @@ export default class ActorMovementConfig extends DocumentSheet {
movement: foundry.utils.deepClone(sourceMovement),
units: CONFIG.SW5E.movementUnits
};
for ( let [k, v] of Object.entries(data.movement) ) {
if ( ["units", "hover"].includes(k) ) continue;
for (let [k, v] of Object.entries(data.movement)) {
if (["units", "hover"].includes(k)) continue;
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
}
return data;

View file

@ -3,7 +3,7 @@
* @type {Dialog}
*/
export default class SelectItemsPrompt extends Dialog {
constructor(items, dialogData={}, options={}) {
constructor(items, dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"];
@ -18,11 +18,11 @@ export default class SelectItemsPrompt extends Dialog {
super.activateListeners(html);
// render the item's sheet if its image is clicked
html.on('click', '.item-image', (event) => {
html.on("click", ".item-image", (event) => {
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
item?.sheet.render(true);
})
});
}
/**
@ -33,29 +33,27 @@ export default class SelectItemsPrompt extends Dialog {
* @param {string} options.hint - Localized hint to display at the top of the prompt
* @return {Promise<string[]>} - list of item ids which the user has selected
*/
static async create(items, {
hint
}) {
static async create(items, {hint}) {
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint});
return new Promise((resolve) => {
const dlg = new this(items, {
title: game.i18n.localize('SW5E.SelectItemsPromptTitle'),
title: game.i18n.localize("SW5E.SelectItemsPromptTitle"),
content: html,
buttons: {
apply: {
icon: `<i class="fas fa-user-plus"></i>`,
label: game.i18n.localize('SW5E.Apply'),
callback: html => {
label: game.i18n.localize("SW5E.Apply"),
callback: (html) => {
const fd = new FormDataExtended(html[0].querySelector("form")).toObject();
const selectedIds = Object.keys(fd).filter(itemId => fd[itemId]);
const selectedIds = Object.keys(fd).filter((itemId) => fd[itemId]);
resolve(selectedIds);
}
},
cancel: {
icon: '<i class="fas fa-forward"></i>',
label: game.i18n.localize('SW5E.Skip'),
label: game.i18n.localize("SW5E.Skip"),
callback: () => resolve([])
}
},

View file

@ -3,7 +3,6 @@
* @extends {DocumentSheet}
*/
export default class ActorSensesConfig extends DocumentSheet {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
@ -29,14 +28,15 @@ export default class ActorSensesConfig extends DocumentSheet {
const data = {
senses: {},
special: senses.special ?? "",
units: senses.units, movementUnits: CONFIG.SW5E.movementUnits
units: senses.units,
movementUnits: CONFIG.SW5E.movementUnits
};
for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) {
for (let [name, label] of Object.entries(CONFIG.SW5E.senses)) {
const v = senses[name];
data.senses[name] = {
label: game.i18n.localize(label),
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
}
};
}
return data;
}

View file

@ -5,7 +5,7 @@ import LongRestDialog from "./long-rest.js";
* @extends {Dialog}
*/
export default class ShortRestDialog extends Dialog {
constructor(actor, dialogData={}, options={}) {
constructor(actor, dialogData = {}, options = {}) {
super(dialogData, options);
/**
@ -39,7 +39,7 @@ export default class ShortRestDialog extends Dialog {
// Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) {
if (item.type === "class") {
const d = item.data.data;
const denom = d.hitDice || "d6";
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
@ -59,7 +59,6 @@ export default class ShortRestDialog extends Dialog {
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
@ -90,7 +89,7 @@ export default class ShortRestDialog extends Dialog {
* @param {Actor5e} actor
* @return {Promise}
*/
static async shortRestDialog({actor}={}) {
static async shortRestDialog({actor} = {}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: game.i18n.localize("SW5E.ShortRest"),
@ -98,7 +97,7 @@ export default class ShortRestDialog extends Dialog {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"),
callback: html => {
callback: (html) => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty")
newDay = html.find('input[name="newDay"]')[0].checked;
@ -126,8 +125,10 @@ export default class ShortRestDialog extends Dialog {
* @param {Actor5e} actor
* @return {Promise}
*/
static async longRestDialog({actor}={}) {
console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
static async longRestDialog({actor} = {}) {
console.warn(
"WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
);
return LongRestDialog.longRestDialog(...arguments);
}
}

View file

@ -3,7 +3,6 @@
* @extends {DocumentSheet}
*/
export default class TraitSelector extends DocumentSheet {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
@ -38,22 +37,22 @@ export default class TraitSelector extends DocumentSheet {
getData() {
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
const o = this.options;
const value = (o.valueKey) ? attr[o.valueKey] ?? [] : attr;
const custom = (o.customKey) ? attr[o.customKey] ?? "" : "";
const value = o.valueKey ? attr[o.valueKey] ?? [] : attr;
const custom = o.customKey ? attr[o.customKey] ?? "" : "";
// Populate choices
const choices = Object.entries(o.choices).reduce((obj, e) => {
let [k, v] = e;
obj[k] = { label: v, chosen: attr ? value.includes(k) : false };
obj[k] = {label: v, chosen: attr ? value.includes(k) : false};
return obj;
}, {})
}, {});
// Return data
return {
allowCustom: o.allowCustom,
choices: choices,
custom: custom
}
};
}
/* -------------------------------------------- */
@ -64,21 +63,21 @@ export default class TraitSelector extends DocumentSheet {
// Obtain choices
const chosen = [];
for ( let [k, v] of Object.entries(formData) ) {
if ( (k !== "custom") && v ) chosen.push(k);
for (let [k, v] of Object.entries(formData)) {
if (k !== "custom" && v) chosen.push(k);
}
// Object including custom data
const updateData = {};
if ( o.valueKey ) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
if (o.valueKey) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
else updateData[this.attribute] = chosen;
if ( o.allowCustom ) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
// Validate the number chosen
if ( o.minimum && (chosen.length < o.minimum) ) {
if (o.minimum && chosen.length < o.minimum) {
return ui.notifications.error(`You must choose at least ${o.minimum} options`);
}
if ( o.maximum && (chosen.length > o.maximum) ) {
if (o.maximum && chosen.length > o.maximum) {
return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
}

View file

@ -1,6 +1,6 @@
/** @override */
export const measureDistances = function(segments, options={}) {
if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
export const measureDistances = function (segments, options = {}) {
if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options);
// Track the total number of diagonals
let nDiagonal = 0;
@ -8,7 +8,7 @@ export const measureDistances = function(segments, options={}) {
const d = canvas.dimensions;
// Iterate over measured segments
return segments.map(s => {
return segments.map((s) => {
let r = s.ray;
// Determine the total distance traveled
@ -23,7 +23,7 @@ export const measureDistances = function(segments, options={}) {
// Alternative DMG Movement
if (rule === "5105") {
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
let spaces = (nd10 * 2) + (nd - nd10) + ns;
let spaces = nd10 * 2 + (nd - nd10) + ns;
return spaces * canvas.dimensions.distance;
}

View file

@ -151,13 +151,15 @@ export default class CharacterImporter {
result.forEach((prof) => {
let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
assignedProfession.data.data.levels = prof.level;
actor.createEmbeddedDocuments("Item", [assignedProfession.data], { displaySheet: false });
actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false});
});
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
this.addPowers(
sourceCharacter.attribs.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1).map((e) => e.current),
sourceCharacter.attribs
.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
.map((e) => e.current),
actor
);
@ -180,7 +182,7 @@ export default class CharacterImporter {
let classes = await game.packs.get("sw5e.classes").getDocuments();
let assignedClass = classes.find((c) => c.name === profession);
assignedClass.data.data.levels = level;
await actor.createEmbeddedDocuments("Item", [assignedClass.data], { displaySheet: false });
await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false});
}
static classOrMulticlass(name) {
@ -213,7 +215,7 @@ export default class CharacterImporter {
const species = await game.packs.get("sw5e.species").getDocuments();
const assignedSpecies = species.find((c) => c.name === race);
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
const actorData = { data: { abilities: { ...actor.data.data.abilities } } };
const actorData = {data: {abilities: {...actor.data.data.abilities}}};
activeEffects.map((effect) => {
switch (effect.key) {
@ -248,7 +250,7 @@ export default class CharacterImporter {
actor.update(actorData);
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], { displaySheet: false });
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
}
static async addPowers(powers, actor) {
@ -259,7 +261,7 @@ export default class CharacterImporter {
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
if (createdPower) {
await actor.createEmbeddedDocuments("Item", [createdPower.data], { displaySheet: false });
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
}
}
}
@ -280,7 +282,7 @@ export default class CharacterImporter {
createdItem.data.data.quantity = item.quantity;
}
await actor.createEmbeddedDocuments("Item", [createdItem.data], { displaySheet: false });
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
}
}
}

View file

@ -1,28 +1,27 @@
/**
* Highlight critical success or failure on d20 rolls
*/
export const highlightCriticalSuccessFailure = function(message, html, data) {
if ( !message.isRoll || !message.isContentVisible ) return;
export const highlightCriticalSuccessFailure = function (message, html, data) {
if (!message.isRoll || !message.isContentVisible) return;
// Highlight rolls where the first part is a d20 roll
const roll = message.roll;
if ( !roll.dice.length ) return;
if (!roll.dice.length) return;
const d = roll.dice[0];
// Ensure it is an un-modified d20 roll
const isD20 = (d.faces === 20) && ( d.values.length === 1 );
if ( !isD20 ) return;
const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
if ( isModifiedRoll ) return;
const isD20 = d.faces === 20 && d.values.length === 1;
if (!isD20) return;
const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure;
if (isModifiedRoll) return;
// Highlight successes and failures
const critical = d.options.critical || 20;
const fumble = d.options.fumble || 1;
if ( d.total >= critical ) html.find(".dice-total").addClass("critical");
else if ( d.total <= fumble ) html.find(".dice-total").addClass("fumble");
else if ( d.options.target ) {
if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
if (d.total >= critical) html.find(".dice-total").addClass("critical");
else if (d.total <= fumble) html.find(".dice-total").addClass("fumble");
else if (d.options.target) {
if (roll.total >= d.options.target) html.find(".dice-total").addClass("success");
else html.find(".dice-total").addClass("failure");
}
};
@ -32,22 +31,22 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
/**
* Optionally hide the display of chat card action buttons which cannot be performed by the user
*/
export const displayChatActionButtons = function(message, html, data) {
export const displayChatActionButtons = function (message, html, data) {
const chatCard = html.find(".sw5e.chat-card");
if ( chatCard.length > 0 ) {
if (chatCard.length > 0) {
const flavor = html.find(".flavor-text");
if ( flavor.text() === html.find(".item-name").text() ) flavor.remove();
if (flavor.text() === html.find(".item-name").text()) flavor.remove();
// If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor);
if ( actor && actor.isOwner ) return;
else if ( game.user.isGM || (data.author.id === game.user.id)) return;
if (actor && actor.isOwner) return;
else if (game.user.isGM || data.author.id === game.user.id) return;
// Otherwise conceal action buttons except for saving throw
const buttons = chatCard.find("button[data-action]");
buttons.each((i, btn) => {
if ( btn.dataset.action === "save" ) return;
btn.style.display = "none"
if (btn.dataset.action === "save") return;
btn.style.display = "none";
});
}
};
@ -63,8 +62,8 @@ export const displayChatActionButtons = function(message, html, data) {
*
* @return {Array} The extended options Array including new context choices
*/
export const addChatMessageContextOptions = function(html, options) {
let canApply = li => {
export const addChatMessageContextOptions = function (html, options) {
let canApply = (li) => {
const message = game.messages.get(li.data("messageId"));
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
};
@ -73,25 +72,25 @@ export const addChatMessageContextOptions = function(html, options) {
name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>',
condition: canApply,
callback: li => applyChatCardDamage(li, 1)
callback: (li) => applyChatCardDamage(li, 1)
},
{
name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>',
condition: canApply,
callback: li => applyChatCardDamage(li, -1)
callback: (li) => applyChatCardDamage(li, -1)
},
{
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>',
condition: canApply,
callback: li => applyChatCardDamage(li, 2)
callback: (li) => applyChatCardDamage(li, 2)
},
{
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>',
condition: canApply,
callback: li => applyChatCardDamage(li, 0.5)
callback: (li) => applyChatCardDamage(li, 0.5)
}
);
return options;
@ -110,10 +109,12 @@ export const addChatMessageContextOptions = function(html, options) {
function applyChatCardDamage(li, multiplier) {
const message = game.messages.get(li.data("messageId"));
const roll = message.roll;
return Promise.all(canvas.tokens.controlled.map(t => {
return Promise.all(
canvas.tokens.controlled.map((t) => {
const a = t.actor;
return a.applyDamage(roll.total, multiplier);
}));
})
);
}
/* -------------------------------------------- */

View file

@ -1,4 +1 @@
export const ClassFeatures = {
};
export const ClassFeatures = {};

View file

@ -1,13 +1,12 @@
/**
* Override the default Initiative formula to customize special behaviors of the SW5e system.
* Apply advantage, proficiency, or bonuses where appropriate
* Apply the dexterity score as a decimal tiebreaker if requested
* See Combat._getInitiativeFormula for more detail.
*/
export const _getInitiativeFormula = function() {
export const _getInitiativeFormula = function () {
const actor = this.actor;
if ( !actor ) return "1d20";
if (!actor) return "1d20";
const init = actor.data.data.attributes.init;
// Construct initiative formula parts
@ -18,10 +17,15 @@ export const _getInitiativeFormula = function() {
nd = 2;
mods += "kh";
}
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
const parts = [
`${nd}d20${mods}`,
init.mod,
init.prof !== 0 ? init.prof : null,
init.bonus !== 0 ? init.bonus : null
];
// Optionally apply Dexterity tiebreaker
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
return parts.filter(p => p !== null).join(" + ");
if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100);
return parts.filter((p) => p !== null).join(" + ");
};

File diff suppressed because it is too large Load diff

View file

@ -23,14 +23,19 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
const constantTerms = []; // Terms that are constant, and their associated operators
let operators = []; // Temporary storage for operators before they are moved to one of the above
for (let term of terms) { // For each term
if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
else { // Otherwise the term is not an operator
if (term instanceof DiceTerm) { // If the term is something rollable
for (let term of terms) {
// For each term
if (term instanceof OperatorTerm) operators.push(term);
// If the term is an addition/subtraction operator, push the term into the operators array
else {
// Otherwise the term is not an operator
if (term instanceof DiceTerm) {
// If the term is something rollable
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
rollableTerms.push(term); // Then place this rollable term into it as well
} //
else { // Otherwise, this must be a constant
else {
// Otherwise, this must be a constant
constantTerms.push(...operators); // Place the operators into the constantTerms array
constantTerms.push(term); // Then also add this constant term to that array.
} //
@ -43,9 +48,9 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
// Mathematically evaluate the constant formula to produce a single constant term
let constantPart = undefined;
if ( constantFormula ) {
if (constantFormula) {
try {
constantPart = Roll.safeEval(constantFormula)
constantPart = Roll.safeEval(constantFormula);
} catch (err) {
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
}
@ -111,17 +116,33 @@ function _isUnsupportedTerm(term) {
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
*/
export async function d20Roll({
parts=[], data={}, // Roll creation
advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization
chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration
chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization
}={}) {
parts = [],
data = {}, // Roll creation
advantage,
disadvantage,
fumble = 1,
critical = 20,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent, // Roll customization
chooseModifier = false,
fastForward = false,
event,
template,
title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker,
flavor // Chat Message customization
} = {}) {
// Handle input arguments
const formula = ["1d20"].concat(parts).join(" + ");
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
if ( chooseModifier && !isFF ) data["mod"] = "@mod";
if (chooseModifier && !isFF) data["mod"] = "@mod";
// Construct the D20Roll instance
const roll = new CONFIG.Dice.D20Roll(formula, data, {
@ -137,27 +158,32 @@ export async function d20Roll({
});
// Prompt a Dialog to further configure the D20Roll
if ( !isFF ) {
const configured = await roll.configureDialog({
if (!isFF) {
const configured = await roll.configureDialog(
{
title,
chooseModifier,
defaultRollMode: defaultRollMode,
defaultAction: advantageMode,
defaultAbility: data?.item?.ability,
template
}, dialogOptions);
if ( configured === null ) return null;
},
dialogOptions
);
if (configured === null) return null;
}
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if ( speaker ) {
console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`);
if (speaker) {
console.warn(
`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`
);
messageData.speaker = speaker;
}
if ( roll && chatMessage ) await roll.toMessage(messageData);
if (roll && chatMessage) await roll.toMessage(messageData);
return roll;
}
@ -167,11 +193,12 @@ export async function d20Roll({
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
*/
function _determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward=false}={}) {
function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
if ( advantage || event?.altKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
else if (disadvantage || event?.ctrlKey || event?.metaKey)
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
return {isFF, advantageMode};
}
@ -210,12 +237,25 @@ function _determineAdvantageMode({event, advantage=false, disadvantage=false, fa
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
*/
export async function damageRoll({
parts=[], data, // Roll creation
critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization
fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration
chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization
}={}) {
parts = [],
data, // Roll creation
critical = false,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical, // Damage customization
fastForward = false,
event,
allowCritical = true,
template,
title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker,
flavor // Chat Message customization
} = {}) {
// Handle input arguments
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
@ -232,26 +272,31 @@ export async function damageRoll({
});
// Prompt a Dialog to further configure the DamageRoll
if ( !isFF ) {
const configured = await roll.configureDialog({
if (!isFF) {
const configured = await roll.configureDialog(
{
title,
defaultRollMode: defaultRollMode,
defaultCritical: isCritical,
template,
allowCritical
}, dialogOptions);
if ( configured === null ) return null;
},
dialogOptions
);
if (configured === null) return null;
}
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if ( speaker ) {
console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`);
if (speaker) {
console.warn(
`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`
);
messageData.speaker = speaker;
}
if ( roll && chatMessage ) await roll.toMessage(messageData);
if (roll && chatMessage) await roll.toMessage(messageData);
return roll;
}
@ -261,8 +306,8 @@ export async function damageRoll({
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
*/
function _determineCriticalMode({event, critical=false, fastForward=false}={}) {
function _determineCriticalMode({event, critical = false, fastForward = false} = {}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if ( event?.altKey ) critical = true;
if (event?.altKey) critical = true;
return {isFF, isCritical: critical};
}

View file

@ -16,7 +16,7 @@
export default class D20Roll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
if ( !((this.terms[0] instanceof Die) && (this.terms[0].faces === 20)) ) {
if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) {
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
}
this.configureModifiers();
@ -31,8 +31,8 @@ export default class D20Roll extends Roll {
static ADV_MODE = {
NORMAL: 0,
ADVANTAGE: 1,
DISADVANTAGE: -1,
}
DISADVANTAGE: -1
};
/**
* The HTML template path used to configure evaluation of this Roll
@ -71,28 +71,26 @@ export default class D20Roll extends Roll {
d20.modifiers = [];
// Halfling Lucky
if ( this.options.halflingLucky ) d20.modifiers.push("r1=1");
if (this.options.halflingLucky) d20.modifiers.push("r1=1");
// Reliable Talent
if ( this.options.reliableTalent ) d20.modifiers.push("min10");
if (this.options.reliableTalent) d20.modifiers.push("min10");
// Handle Advantage or Disadvantage
if ( this.hasAdvantage ) {
if (this.hasAdvantage) {
d20.number = this.options.elvenAccuracy ? 3 : 2;
d20.modifiers.push("kh");
d20.options.advantage = true;
}
else if ( this.hasDisadvantage ) {
} else if (this.hasDisadvantage) {
d20.number = 2;
d20.modifiers.push("kl");
d20.options.disadvantage = true;
}
else d20.number = 1;
} else d20.number = 1;
// Assign critical and fumble thresholds
if ( this.options.critical ) d20.options.critical = this.options.critical;
if ( this.options.fumble ) d20.options.fumble = this.options.fumble;
if ( this.options.targetValue ) d20.options.target = this.options.targetValue;
if (this.options.critical) d20.options.critical = this.options.critical;
if (this.options.fumble) d20.options.fumble = this.options.fumble;
if (this.options.targetValue) d20.options.target = this.options.targetValue;
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
@ -101,22 +99,21 @@ export default class D20Roll extends Roll {
/* -------------------------------------------- */
/** @inheritdoc */
async toMessage(messageData={}, options={}) {
async toMessage(messageData = {}, options = {}) {
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play
if ( !this._evaluated ) await this.evaluate({async: true});
if (!this._evaluated) await this.evaluate({async: true});
// Add appropriate advantage mode message flavor and sw5e roll flags
messageData.flavor = messageData.flavor || this.options.flavor;
if ( this.hasAdvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
else if ( this.hasDisadvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
if (this.hasAdvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
else if (this.hasDisadvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
// Add reliable talent to the d20-term flavor text if it applied
if ( this.options.reliableTalent ) {
if (this.options.reliableTalent) {
const d20 = this.dice[0];
const isRT = d20.results.every(r => !r.active || (r.result < 10));
const isRT = d20.results.every((r) => !r.active || r.result < 10);
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
if ( isRT ) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
}
// Record the preferred rollMode
@ -140,8 +137,17 @@ export default class D20Roll extends Roll {
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false, defaultAbility, template}={}, options={}) {
async configureDialog(
{
title,
defaultRollMode,
defaultAction = D20Roll.ADV_MODE.NORMAL,
chooseModifier = false,
defaultAbility,
template
} = {},
options = {}
) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
@ -154,32 +160,39 @@ export default class D20Roll extends Roll {
let defaultButton = "normal";
switch (defaultAction) {
case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break;
case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break;
case D20Roll.ADV_MODE.ADVANTAGE:
defaultButton = "advantage";
break;
case D20Roll.ADV_MODE.DISADVANTAGE:
defaultButton = "disadvantage";
break;
}
// Create the Dialog window and await submission of the form
return new Promise(resolve => {
new Dialog({
return new Promise((resolve) => {
new Dialog(
{
title,
content,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
}
},
default: defaultButton,
close: () => resolve(null)
}, options).render(true);
},
options
).render(true);
});
}
@ -195,16 +208,16 @@ export default class D20Roll extends Roll {
const form = html[0].querySelector("form");
// Append a situational bonus term
if ( form.bonus.value ) {
if (form.bonus.value) {
const bonus = new Roll(form.bonus.value, this.data);
if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Customize the modifier
if ( form.ability?.value ) {
if (form.ability?.value) {
const abl = this.data.abilities[form.ability.value];
this.terms.findSplice(t => t.term === "@mod", new NumericTerm({number: abl.mod}));
this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod}));
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
}

View file

@ -13,7 +13,7 @@ export default class DamageRoll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
// For backwards compatibility, skip rolls which do not have the "critical" option defined
if ( this.options.critical !== undefined ) this.configureDamage();
if (this.options.critical !== undefined) this.configureDamage();
}
/**
@ -42,44 +42,44 @@ export default class DamageRoll extends Roll {
*/
configureDamage() {
let flatBonus = 0;
for ( let [i, term] of this.terms.entries() ) {
for (let [i, term] of this.terms.entries()) {
// Multiply dice terms
if ( term instanceof DiceTerm ) {
if (term instanceof DiceTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if ( this.isCritical ) {
if (this.isCritical) {
let cm = this.options.criticalMultiplier ?? 2;
// Powerful critical - maximize damage and reduce the multiplier by 1
if ( this.options.powerfulCritical ) {
flatBonus += (term.number * term.faces);
cm = Math.max(1, cm-1);
if (this.options.powerfulCritical) {
flatBonus += term.number * term.faces;
cm = Math.max(1, cm - 1);
}
// Alter the damage term
let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0;
let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0;
term.alter(cm, cb);
term.options.critical = true;
}
}
// Multiply numeric terms
else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) {
else if (this.options.multiplyNumeric && term instanceof NumericTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if ( this.isCritical ) {
term.number *= (this.options.criticalMultiplier ?? 2);
if (this.isCritical) {
term.number *= this.options.criticalMultiplier ?? 2;
term.options.critical = true;
}
}
}
// Add powerful critical bonus
if ( this.options.powerfulCritical && (flatBonus > 0) ) {
if (this.options.powerfulCritical && flatBonus > 0) {
this.terms.push(new OperatorTerm({operator: "+"}));
this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")}));
this.terms.push(
new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})
);
}
// Re-compile the underlying formula
@ -89,9 +89,9 @@ export default class DamageRoll extends Roll {
/* -------------------------------------------- */
/** @inheritdoc */
toMessage(messageData={}, options={}) {
toMessage(messageData = {}, options = {}) {
messageData.flavor = messageData.flavor || this.options.flavor;
if ( this.isCritical ) {
if (this.isCritical) {
const label = game.i18n.localize("SW5E.CriticalHit");
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
}
@ -114,34 +114,39 @@ export default class DamageRoll extends Roll {
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) {
async configureDialog(
{title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {},
options = {}
) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
rollModes: CONFIG.Dice.rollModes,
rollModes: CONFIG.Dice.rollModes
});
// Create the Dialog window and await submission of the form
return new Promise(resolve => {
new Dialog({
return new Promise((resolve) => {
new Dialog(
{
title,
content,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(this._onDialogSubmit(html, true))
callback: (html) => resolve(this._onDialogSubmit(html, true))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(this._onDialogSubmit(html, false))
callback: (html) => resolve(this._onDialogSubmit(html, false))
}
},
default: defaultCritical ? "critical" : "normal",
close: () => resolve(null)
}, options).render(true);
},
options
).render(true);
});
}
@ -157,9 +162,9 @@ export default class DamageRoll extends Roll {
const form = html[0].querySelector("form");
// Append a situational bonus term
if ( form.bonus.value ) {
if (form.bonus.value) {
const bonus = new Roll(form.bonus.value, this.data);
if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}

23
module/effects.js vendored
View file

@ -8,15 +8,17 @@ export function onManageActiveEffect(event, owner) {
const a = event.currentTarget;
const li = a.closest("li");
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
switch ( a.dataset.action ) {
switch (a.dataset.action) {
case "create":
return owner.createEmbeddedDocuments("ActiveEffect", [{
label: game.i18n.localize("SW5E.EffectNew"),
icon: "icons/svg/aura.svg",
origin: owner.uuid,
return owner.createEmbeddedDocuments("ActiveEffect", [
{
"label": game.i18n.localize("SW5E.EffectNew"),
"icon": "icons/svg/aura.svg",
"origin": owner.uuid,
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
disabled: li.dataset.effectType === "inactive"
}]);
"disabled": li.dataset.effectType === "inactive"
}
]);
case "edit":
return effect.sheet.render(true);
case "delete":
@ -32,7 +34,6 @@ export function onManageActiveEffect(event, owner) {
* @return {object} Data for rendering
*/
export function prepareActiveEffectCategories(effects) {
// Define effect header categories
const categories = {
temporary: {
@ -53,10 +54,10 @@ export function prepareActiveEffectCategories(effects) {
};
// Iterate over active effects, classifying them into categories
for ( let e of effects ) {
for (let e of 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);
if (e.data.disabled) categories.inactive.effects.push(e);
else if (e.isTemporary) categories.temporary.effects.push(e);
else categories.passive.effects.push(e);
}
return categories;

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import TraitSelector from "../apps/trait-selector.js";
import { onManageActiveEffect, prepareActiveEffectCategories } from "../effects.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
/**
* Override and extend the core ItemSheet implementation to handle specific item types
@ -26,7 +26,7 @@ export default class ItemSheet5e extends ItemSheet {
classes: ["sw5e", "sheet", "item"],
resizable: true,
scrollY: [".tab.details"],
tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description" }]
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
@ -64,7 +64,7 @@ export default class ItemSheet5e extends ItemSheet {
// Original maximum uses formula
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
if ( sourceMax ) itemData.data.uses.max = sourceMax;
if (sourceMax) itemData.data.uses.max = sourceMax;
// Vehicles
data.isCrewed = itemData.data.activation?.type === "crew";
@ -102,14 +102,14 @@ export default class ItemSheet5e extends ItemSheet {
}
return ammo;
},
{ [item._id]: `${item.name} (${item.data.quantity})` }
{[item._id]: `${item.name} (${item.data.quantity})`}
);
}
// Attributes
else if (consume.type === "attribute") {
const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
attributes.bar.forEach(a => a.push("value"));
attributes.bar.forEach((a) => a.push("value"));
return attributes.bar.concat(attributes.value).reduce((obj, a) => {
let k = a.join(".");
obj[k] = k;
@ -135,8 +135,11 @@ export default class ItemSheet5e extends ItemSheet {
if (uses.per && uses.max) {
const label =
uses.per === "charges"
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})`
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`;
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
max: uses.max,
per: uses.per
})})`;
obj[i.id] = i.name + label;
}
@ -261,7 +264,7 @@ export default class ItemSheet5e extends ItemSheet {
/** @inheritdoc */
_getSubmitData(updateData = {}) {
// Create the expanded update data object
const fd = new FormDataExtended(this.form, { editors: this.editors });
const fd = new FormDataExtended(this.form, {editors: this.editors});
let data = fd.toObject();
if (updateData) data = mergeObject(data, updateData);
else data = expandObject(data);
@ -283,7 +286,10 @@ export default class ItemSheet5e extends ItemSheet {
html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this));
html.find(".effect-control").click((ev) => {
if (this.item.isOwned) return ui.notifications.warn("Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update.");
if (this.item.isOwned)
return ui.notifications.warn(
"Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."
);
onManageActiveEffect(ev, this.item);
});
}
@ -305,7 +311,7 @@ export default class ItemSheet5e extends ItemSheet {
if (a.classList.contains("add-damage")) {
await this._onSubmit(event); // Submit any unsaved changes
const damage = this.item.data.data.damage;
return this.item.update({ "data.damage.parts": damage.parts.concat([["", ""]]) });
return this.item.update({"data.damage.parts": damage.parts.concat([["", ""]])});
}
// Remove a damage component
@ -314,7 +320,7 @@ export default class ItemSheet5e extends ItemSheet {
const li = a.closest(".damage-part");
const damage = foundry.utils.deepClone(this.item.data.data.damage);
damage.parts.splice(Number(li.dataset.damagePart), 1);
return this.item.update({ "data.damage.parts": damage.parts });
return this.item.update({"data.damage.parts": damage.parts});
}
}
@ -336,15 +342,18 @@ export default class ItemSheet5e extends ItemSheet {
allowCustom: false
};
switch(a.dataset.options) {
case 'saves':
switch (a.dataset.options) {
case "saves":
options.choices = CONFIG.SW5E.abilities;
options.valueKey = null;
break;
case 'skills':
case "skills":
const skills = this.item.data.data.skills;
const choiceSet = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
options.choices = Object.fromEntries(Object.entries(CONFIG.SW5E.skills).filter(skill => choiceSet.includes(skill[0])));
const choiceSet =
skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
options.choices = Object.fromEntries(
Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
);
options.maximum = skills.number;
break;
}

View file

@ -1,4 +1,3 @@
/* -------------------------------------------- */
/* Hotbar Macros */
/* -------------------------------------------- */
@ -11,14 +10,14 @@
* @returns {Promise}
*/
export async function create5eMacro(data, slot) {
if ( data.type !== "Item" ) return;
if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items");
if (data.type !== "Item") return;
if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items");
const item = data.data;
// Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
if ( !macro ) {
let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
if (!macro) {
macro = await Macro.create({
name: item.name,
type: "script",
@ -42,14 +41,16 @@ export async function create5eMacro(data, slot) {
export function rollItemMacro(itemName) {
const speaker = ChatMessage.getSpeaker();
let actor;
if ( speaker.token ) actor = game.actors.tokens[speaker.token];
if ( !actor ) actor = game.actors.get(speaker.actor);
if (speaker.token) actor = game.actors.tokens[speaker.token];
if (!actor) actor = game.actors.get(speaker.actor);
// Get matching items
const items = actor ? actor.items.filter(i => i.name === itemName) : [];
if ( items.length > 1 ) {
ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`);
} else if ( items.length === 0 ) {
const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
if (items.length > 1) {
ui.notifications.warn(
`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`
);
} else if (items.length === 0) {
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
}
const item = items[0];

View file

@ -2,59 +2,62 @@
* Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
* @return {Promise} A Promise which resolves once the migration is completed
*/
export const migrateWorld = async function() {
ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
export const migrateWorld = async function () {
ui.notifications.info(
`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`,
{permanent: true}
);
// Migrate World Actors
for await ( let a of game.actors.contents ) {
for await (let a of game.actors.contents) {
try {
console.log(`Checking Actor entity ${a.name} for migration needs`);
const updateData = await migrateActorData(a.data);
if ( !foundry.utils.isObjectEmpty(updateData) ) {
if (!foundry.utils.isObjectEmpty(updateData)) {
console.log(`Migrating Actor entity ${a.name}`);
await a.update(updateData, {enforceTypes: false});
}
} catch(err) {
} catch (err) {
err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`;
console.error(err);
}
}
// Migrate World Items
for ( let i of game.items.contents ) {
for (let i of game.items.contents) {
try {
const updateData = migrateItemData(i.toObject());
if ( !foundry.utils.isObjectEmpty(updateData) ) {
if (!foundry.utils.isObjectEmpty(updateData)) {
console.log(`Migrating Item entity ${i.name}`);
await i.update(updateData, {enforceTypes: false});
}
} catch(err) {
} catch (err) {
err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`;
console.error(err);
}
}
// Migrate Actor Override Tokens
for ( let s of game.scenes.contents ) {
for (let s of game.scenes.contents) {
try {
const updateData = await migrateSceneData(s.data);
if ( !foundry.utils.isObjectEmpty(updateData) ) {
if (!foundry.utils.isObjectEmpty(updateData)) {
console.log(`Migrating Scene entity ${s.name}`);
await s.update(updateData, {enforceTypes: false});
// If we do not do this, then synthetic token actors remain in cache
// with the un-updated actorData.
s.tokens.contents.forEach(t => t._actor = null);
s.tokens.contents.forEach((t) => (t._actor = null));
}
} catch(err) {
} catch (err) {
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
console.error(err);
}
}
// Migrate World Compendium Packs
for ( let p of game.packs ) {
if ( p.metadata.package !== "world" ) continue;
if ( !["Actor", "Item", "Scene"].includes(p.metadata.entity) ) continue;
for (let p of game.packs) {
if (p.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(p.metadata.entity)) continue;
await migrateCompendium(p);
}
@ -70,9 +73,9 @@ export const migrateWorld = async function() {
* @param pack
* @return {Promise}
*/
export const migrateCompendium = async function(pack) {
export const migrateCompendium = async function (pack) {
const entity = pack.metadata.entity;
if ( !["Actor", "Item", "Scene"].includes(entity) ) return;
if (!["Actor", "Item", "Scene"].includes(entity)) return;
// Unlock the pack for editing
const wasLocked = pack.locked;
@ -83,7 +86,7 @@ export const migrateCompendium = async function(pack) {
const documents = await pack.getDocuments();
// Iterate over compendium entries - applying fine-tuned migration functions
for await ( let doc of documents ) {
for await (let doc of documents) {
let updateData = {};
try {
switch (entity) {
@ -97,15 +100,13 @@ export const migrateCompendium = async function(pack) {
updateData = await migrateSceneData(doc.data);
break;
}
if ( foundry.utils.isObjectEmpty(updateData) ) continue;
if (foundry.utils.isObjectEmpty(updateData)) continue;
// Save the entry, if data was changed
await doc.update(updateData);
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
}
} catch (err) {
// Handle migration failures
catch(err) {
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
console.error(err);
}
@ -126,18 +127,18 @@ export const migrateCompendium = async function(pack) {
* @param {object} actor The actor data object to update
* @return {Object} The updateData to apply
*/
export const migrateActorData = async function(actor) {
export const migrateActorData = async function (actor) {
const updateData = {};
// Actor Data Updates
if(actor.data) {
if (actor.data) {
_migrateActorMovement(actor, updateData);
_migrateActorSenses(actor, updateData);
_migrateActorType(actor, updateData);
}
// Migrate Owned Items
if ( !!actor.items ) {
if (!!actor.items) {
const items = await actor.items.reduce(async (memo, i) => {
const results = await memo;
@ -146,14 +147,15 @@ export const migrateActorData = async function(actor) {
let itemUpdate = await migrateActorItemData(itemData, actor);
// Prepared, Equipped, and Proficient for NPC actors
if ( actor.type === "npc" ) {
if (getProperty(itemData.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
if (actor.type === "npc") {
if (getProperty(itemData.data, "preparation.prepared") === false)
itemUpdate["data.preparation.prepared"] = true;
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
}
// Update the Owned Item
if ( !isObjectEmpty(itemUpdate) ) {
if (!isObjectEmpty(itemUpdate)) {
itemUpdate._id = itemData._id;
console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
results.push(expandObject(itemUpdate));
@ -162,7 +164,7 @@ export const migrateActorData = async function(actor) {
return results;
}, []);
if ( items.length > 0 ) updateData.items = items;
if (items.length > 0) updateData.items = items;
}
// Update NPC data with new datamodel information
@ -178,14 +180,12 @@ export const migrateActorData = async function(actor) {
/* -------------------------------------------- */
/**
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template
* @param {Object} actorData The data object for an Actor
* @return {Object} The scrubbed Actor data
*/
function cleanActorData(actorData) {
// Scrub system data
const model = game.system.model.Actor[actorData.type];
actorData.data = filterObject(actorData.data, model);
@ -195,7 +195,7 @@ function cleanActorData(actorData) {
obj[f] = null;
return obj;
}, {});
if ( actorData.flags.sw5e ) {
if (actorData.flags.sw5e) {
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
}
@ -203,7 +203,6 @@ function cleanActorData(actorData) {
return actorData;
}
/* -------------------------------------------- */
/**
@ -212,7 +211,7 @@ function cleanActorData(actorData) {
* @param {object} item Item data to migrate
* @return {object} The updateData to apply
*/
export const migrateItemData = function(item) {
export const migrateItemData = function (item) {
const updateData = {};
_migrateItemClassPowerCasting(item, updateData);
_migrateItemAttunement(item, updateData);
@ -226,7 +225,7 @@ export const migrateItemData = function(item) {
* @param item
* @param actor
*/
export const migrateActorItemData = async function(item, actor) {
export const migrateActorItemData = async function (item, actor) {
const updateData = {};
_migrateItemClassPowerCasting(item, updateData);
_migrateItemAttunement(item, updateData);
@ -242,23 +241,23 @@ export const migrateActorItemData = async function(item, actor) {
* @param {Object} scene The Scene data to Update
* @return {Object} The updateData to apply
*/
export const migrateSceneData = async function(scene) {
const tokens = await Promise.all(scene.tokens.map(async token => {
export const migrateSceneData = async function (scene) {
const tokens = await Promise.all(
scene.tokens.map(async (token) => {
const t = token.toJSON();
if (!t.actorId || t.actorLink) {
t.actorData = {};
}
else if (!game.actors.has(t.actorId)) {
} else if (!game.actors.has(t.actorId)) {
t.actorId = null;
t.actorData = {};
} else if ( !t.actorLink ) {
} else if (!t.actorLink) {
const actorData = duplicate(t.actorData);
actorData.type = token.actor?.type;
const update = migrateActorData(actorData);
['items', 'effects'].forEach(embeddedName => {
["items", "effects"].forEach((embeddedName) => {
if (!update[embeddedName]?.length) return;
const updates = new Map(update[embeddedName].map(u => [u._id, u]));
t.actorData[embeddedName].forEach(original => {
const updates = new Map(update[embeddedName].map((u) => [u._id, u]));
t.actorData[embeddedName].forEach((original) => {
const update = updates.get(original._id);
if (update) mergeObject(original, update);
});
@ -268,7 +267,8 @@ export const migrateActorItemData = async function(item, actor) {
mergeObject(t.actorData, update);
}
return t;
}));
})
);
return {tokens};
};
@ -284,7 +284,6 @@ export const migrateActorItemData = async function(item, actor) {
* @return {Object} The updated Actor
*/
function _updateNPCData(actor) {
let actorData = actor.data;
const updateData = {};
// check for flag.core, if not there is no compendium monster so exit
@ -292,13 +291,20 @@ function _updateNPCData(actor) {
if (!hasSource) return actor;
// shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined;
if (hasDataVersion && (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))) return actor;
if (
hasDataVersion &&
(actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))
)
return actor;
// Check to see what the source of NPC is
const sourceId = actor.flags.core.sourceId;
const coreSource = sourceId.substr(0,sourceId.length-17);
const core_id = sourceId.substr(sourceId.length-16,16);
if (coreSource === "Compendium.sw5e.monsters"){
game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => {
const coreSource = sourceId.substr(0, sourceId.length - 17);
const core_id = sourceId.substr(sourceId.length - 16, 16);
if (coreSource === "Compendium.sw5e.monsters") {
game.packs
.get("sw5e.monsters")
.getEntity(core_id)
.then((monster) => {
const monsterData = monster.data.data;
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
updateData["data.attributes.movement"] = monsterData.attributes.movement;
@ -310,11 +316,13 @@ function _updateNPCData(actor) {
updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel;
// push missing powers onto actor
let newPowers = [];
for ( let i of monster.items ) {
for (let i of monster.items) {
const itemData = i.data;
if ( itemData.type === "power" ) {
if (itemData.type === "power") {
const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
let hasPower = !!actor.items.find(item => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id);
let hasPower = !!actor.items.find(
(item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id
);
if (!hasPower) {
// Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness.
const newPower = JSON.parse(JSON.stringify(itemData));
@ -331,17 +339,15 @@ function _updateNPCData(actor) {
// set flag to check to see if migration has been done so we don't do it again.
liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
})
});
}
//merge object
actorData = mergeObject(actorData, updateData);
// Return the scrubbed data
return actor;
}
/**
* Migrate the actor speed string to movement object
* @private
@ -350,21 +356,21 @@ function _migrateActorMovement(actorData, updateData) {
const ad = actorData.data;
// Work is needed if old data is present
const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
const hasOld = old !== undefined;
if ( hasOld ) {
if (hasOld) {
// If new data is not present, migrate the old data
const hasNew = ad?.attributes?.movement?.walk !== undefined;
if ( !hasNew && (typeof old === "string") ) {
if (!hasNew && typeof old === "string") {
const s = (old || "").split(" ");
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
if (s.length > 0)
updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
}
// Remove the old attribute
updateData["data.attributes.-=speed"] = null;
}
return updateData
return updateData;
}
/* -------------------------------------------- */
@ -378,7 +384,7 @@ function _migrateActorPowers(actorData, updateData) {
// If new Force & Tech data is not present, create it
let hasNewAttrib = ad?.attributes?.force?.level !== undefined;
if ( !hasNewAttrib ) {
if (!hasNewAttrib) {
updateData["data.attributes.force.known.value"] = 0;
updateData["data.attributes.force.known.max"] = 0;
updateData["data.attributes.force.points.value"] = 0;
@ -399,14 +405,14 @@ function _migrateActorPowers(actorData, updateData) {
// If new Power F/T split data is not present, create it
const hasNewLimit = ad?.powers?.power1?.foverride !== undefined;
if ( !hasNewLimit ) {
if (!hasNewLimit) {
for (let i = 1; i <= 9; i++) {
// add new
updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers,"power" + i + ".value");
updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers,"power" + i + ".max");
updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers, "power" + i + ".value");
updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers, "power" + i + ".max");
updateData["data.powers.power" + i + ".foverride"] = null;
updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers,"power" + i + ".value");
updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers,"power" + i + ".max");
updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers, "power" + i + ".value");
updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers, "power" + i + ".max");
updateData["data.powers.power" + i + ".toverride"] = null;
//remove old
updateData["data.powers.power" + i + ".-=value"] = null;
@ -415,7 +421,7 @@ function _migrateActorPowers(actorData, updateData) {
}
// If new Bonus Power DC data is not present, create it
const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined;
if ( !hasNewBonus ) {
if (!hasNewBonus) {
updateData["data.bonuses.power.forceLightDC"] = "";
updateData["data.bonuses.power.forceDarkDC"] = "";
updateData["data.bonuses.power.forceUnivDC"] = "";
@ -425,7 +431,7 @@ function _migrateActorPowers(actorData, updateData) {
// Remove the Power DC Bonus
updateData["data.bonuses.power.-=dc"] = null;
return updateData
return updateData;
}
/* -------------------------------------------- */
@ -436,28 +442,28 @@ function _migrateActorPowers(actorData, updateData) {
*/
function _migrateActorSenses(actor, updateData) {
const ad = actor.data;
if ( ad?.traits?.senses === undefined ) return;
if (ad?.traits?.senses === undefined) return;
const original = ad.traits.senses || "";
if ( typeof original !== "string" ) return;
if (typeof original !== "string") return;
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
let wasMatched = false;
// Match each comma-separated term
for ( let s of original.split(",") ) {
for (let s of original.split(",")) {
s = s.trim();
const match = s.match(pattern);
if ( !match ) continue;
if (!match) continue;
const type = match[1].toLowerCase();
if ( type in CONFIG.SW5E.senses ) {
if (type in CONFIG.SW5E.senses) {
updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
wasMatched = true;
}
}
// If nothing was matched, but there was an old string - put the whole thing in "special"
if ( !wasMatched && !!original ) {
if (!wasMatched && !!original) {
updateData["data.attributes.senses.special"] = original;
}
@ -475,15 +481,15 @@ function _migrateActorSenses(actor, updateData) {
function _migrateActorType(actor, updateData) {
const ad = actor.data;
const original = ad.details?.type;
if ( typeof original !== "string" ) return;
if (typeof original !== "string") return;
// New default data structure
let data = {
"value": "",
"subtype": "",
"swarm": "",
"custom": ""
}
value: "",
subtype: "",
swarm: "",
custom: ""
};
// Specifics
// (Some of these have weird names, these need to be addressed individually)
@ -507,13 +513,14 @@ function _migrateActorType(actor, updateData) {
const pattern = /^(?:swarm of (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i;
const match = original.trim().match(pattern);
if (match) {
// Match a known creature type
const typeLc = match.groups.type.trim().toLowerCase();
const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => {
return (typeLc === k) ||
(typeLc === game.i18n.localize(v).toLowerCase()) ||
(typeLc === game.i18n.localize(`${v}Pl`).toLowerCase());
return (
typeLc === k ||
typeLc === game.i18n.localize(v).toLowerCase() ||
typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()
);
});
if (typeMatch) data.value = typeMatch[0];
else {
@ -527,7 +534,7 @@ function _migrateActorType(actor, updateData) {
if (match.groups.size || isNamedSwarm) {
const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => {
return (sizeLc === k) || (sizeLc === game.i18n.localize(v).toLowerCase());
return sizeLc === k || sizeLc === game.i18n.localize(v).toLowerCase();
});
data.swarm = sizeMatch ? sizeMatch[0] : "tiny";
} else data.swarm = "";
@ -551,8 +558,8 @@ function _migrateActorType(actor, updateData) {
* @private
*/
function _migrateItemClassPowerCasting(item, updateData) {
if (item.type === "class"){
switch (item.name){
if (item.type === "class") {
switch (item.name) {
case "Consular":
updateData["data.powercasting"] = {
progression: "consular",
@ -560,7 +567,6 @@ function _migrateItemClassPowerCasting(item, updateData) {
};
break;
case "Engineer":
updateData["data.powercasting"] = {
progression: "engineer",
ability: ""
@ -608,7 +614,11 @@ async function _migrateItemPower(item, actor, updateData) {
// shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined;
if (hasDataVersion && (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))) return updateData;
if (
hasDataVersion &&
(item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))
)
return updateData;
// Check to see what the source of Power is
const sourceId = item.flags.core.sourceId;
@ -626,11 +636,10 @@ async function _migrateItemPower(item, actor, updateData) {
const corePowerData = corePower.data;
// copy Core Power Data over original Power
updateData["data"] = corePowerData;
updateData["flags"] = {"sw5e": {"dataVersion": "1.2.4"}};
updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}};
return updateData;
//game.packs.get(powerType).getEntity(core_id).then(corePower => {
//})
@ -647,7 +656,7 @@ async function _migrateItemPower(item, actor, updateData) {
* @private
*/
function _migrateItemAttunement(item, updateData) {
if ( item.data?.attuned === undefined ) return updateData;
if (item.data?.attuned === undefined) return updateData;
updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
updateData["data.-=attuned"] = null;
return updateData;
@ -667,13 +676,13 @@ export async function purgeFlags(pack) {
};
await pack.configure({locked: false});
const content = await pack.getContent();
for ( let entity of content ) {
for (let entity of content) {
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
if ( pack.entity === "Actor" ) {
update.items = entity.data.items.map(i => {
if (pack.entity === "Actor") {
update.items = entity.data.items.map((i) => {
i.flags = cleanFlags(i.flags);
return i;
})
});
}
await pack.updateEntity(update, {recursive: false});
console.log(`Purged flags from ${entity.name}`);
@ -683,20 +692,18 @@ export async function purgeFlags(pack) {
/* -------------------------------------------- */
/**
* Purge the data model of any inner objects which have been flagged as _deprecated.
* @param {object} data The data to clean
* @private
*/
export function removeDeprecatedObjects(data) {
for ( let [k, v] of Object.entries(data) ) {
if ( getType(v) === "Object" ) {
for (let [k, v] of Object.entries(data)) {
if (getType(v) === "Object") {
if (v._deprecated === true) {
console.log(`Deleting deprecated object key ${k}`);
delete data[k];
}
else removeDeprecatedObjects(v);
} else removeDeprecatedObjects(v);
}
}
return data;

View file

@ -1,11 +1,10 @@
import { SW5E } from "../config.js";
import {SW5E} from "../config.js";
/**
* A helper class for building MeasuredTemplates for 5e powers and abilities
* @extends {MeasuredTemplate}
*/
export default class AbilityTemplate extends MeasuredTemplate {
/**
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
* @param {Item5e} item The Item object for which to construct the template
@ -14,7 +13,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
static fromItem(item) {
const target = getProperty(item.data, "data.target") || {};
const templateShape = SW5E.areaTargetTypes[target.type];
if ( !templateShape ) return null;
if (!templateShape) return null;
// Prepare template data
const templateData = {
@ -28,7 +27,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
};
// Additional type-specific data
switch ( templateShape ) {
switch (templateShape) {
case "cone":
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
break;
@ -67,7 +66,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
this.layer.preview.addChild(this);
// Hide the sheet that originated the preview
if ( this.actorSheet ) this.actorSheet.minimize();
if (this.actorSheet) this.actorSheet.minimize();
// Activate interactivity
this.activatePreviewListeners(initialLayer);
@ -84,10 +83,10 @@ export default class AbilityTemplate extends MeasuredTemplate {
let moveTime = 0;
// Update placement (mouse-move)
handlers.mm = event => {
handlers.mm = (event) => {
event.stopPropagation();
let now = Date.now(); // Apply a 20ms throttle
if ( now - moveTime <= 20 ) return;
if (now - moveTime <= 20) return;
const center = event.data.getLocalPosition(this.layer);
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
this.data.update({x: snapped.x, y: snapped.y});
@ -96,7 +95,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
};
// Cancel the workflow (right-click)
handlers.rc = event => {
handlers.rc = (event) => {
this.layer.preview.removeChildren();
canvas.stage.off("mousemove", handlers.mm);
canvas.stage.off("mousedown", handlers.lc);
@ -107,7 +106,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
};
// Confirm the workflow (left-click)
handlers.lc = event => {
handlers.lc = (event) => {
handlers.rc(event);
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
this.data.update(destination);
@ -115,12 +114,12 @@ export default class AbilityTemplate extends MeasuredTemplate {
};
// Rotate the template by 3 degree increments (mouse-wheel)
handlers.mw = event => {
if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
handlers.mw = (event) => {
if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
event.stopPropagation();
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
let snap = event.shiftKey ? delta : 5;
this.data.update({direction: this.data.direction + (snap * Math.sign(event.deltaY))});
this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)});
this.refresh();
};

View file

@ -1,5 +1,4 @@
export const registerSystemSettings = function() {
export const registerSystemSettings = function () {
/**
* Track the system version upon which point a migration was last applied
*/
@ -22,9 +21,9 @@ export const registerSystemSettings = function() {
default: "normal",
type: String,
choices: {
"normal": "SETTINGS.5eRestPHB",
"gritty": "SETTINGS.5eRestGritty",
"epic": "SETTINGS.5eRestEpic",
normal: "SETTINGS.5eRestPHB",
gritty: "SETTINGS.5eRestGritty",
epic: "SETTINGS.5eRestEpic"
}
});
@ -39,11 +38,11 @@ export const registerSystemSettings = function() {
default: "555",
type: String,
choices: {
"555": "SETTINGS.5eDiagPHB",
"5105": "SETTINGS.5eDiagDMG",
"EUCL": "SETTINGS.5eDiagEuclidean",
555: "SETTINGS.5eDiagPHB",
5105: "SETTINGS.5eDiagDMG",
EUCL: "SETTINGS.5eDiagEuclidean"
},
onChange: rule => canvas.grid.diagonalRule = rule
onChange: (rule) => (canvas.grid.diagonalRule = rule)
});
/**
@ -79,7 +78,7 @@ export const registerSystemSettings = function() {
scope: "world",
config: true,
default: false,
type: Boolean,
type: Boolean
});
/**
@ -92,7 +91,7 @@ export const registerSystemSettings = function() {
config: true,
default: false,
type: Boolean,
onChange: s => {
onChange: (s) => {
ui.chat.render();
}
});
@ -100,10 +99,10 @@ export const registerSystemSettings = function() {
/**
* Option to allow GMs to restrict polymorphing to GMs only.
*/
game.settings.register('sw5e', 'allowPolymorphing', {
name: 'SETTINGS.5eAllowPolymorphingN',
hint: 'SETTINGS.5eAllowPolymorphingL',
scope: 'world',
game.settings.register("sw5e", "allowPolymorphing", {
name: "SETTINGS.5eAllowPolymorphingN",
hint: "SETTINGS.5eAllowPolymorphingL",
scope: "world",
config: true,
default: false,
type: Boolean
@ -112,8 +111,8 @@ export const registerSystemSettings = function() {
/**
* Remember last-used polymorph settings.
*/
game.settings.register('sw5e', 'polymorphSettings', {
scope: 'client',
game.settings.register("sw5e", "polymorphSettings", {
scope: "client",
default: {
keepPhysical: false,
keepMental: false,
@ -138,8 +137,8 @@ export const registerSystemSettings = function() {
default: "light",
type: String,
choices: {
"light": "SETTINGS.SWColorLight",
"dark": "SETTINGS.SWColorDark"
light: "SETTINGS.SWColorLight",
dark: "SETTINGS.SWColorDark"
}
});
};

View file

@ -3,9 +3,8 @@
* Pre-loaded templates are compiled and cached for fast access when rendering
* @return {Promise}
*/
export const preloadHandlebarsTemplates = async function() {
export const preloadHandlebarsTemplates = async function () {
return loadTemplates([
// Shared Partials
"systems/sw5e/templates/actors/parts/active-effects.html",

View file

@ -3,11 +3,10 @@
* @extends {TokenDocument}
*/
export class TokenDocument5e extends TokenDocument {
/** @inheritdoc */
getBarAttribute(...args) {
const data = super.getBarAttribute(...args);
if ( data && (data.attribute === "attributes.hp") ) {
if (data && data.attribute === "attributes.hp") {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
@ -15,19 +14,16 @@ export class TokenDocument5e extends TokenDocument {
}
}
/* -------------------------------------------- */
/**
* Extend the base Token class to implement additional system-specific logic.
* @extends {Token}
*/
export class Token5e extends Token {
/** @inheritdoc */
_drawBar(number, bar, data) {
if ( data.attribute === "attributes.hp" ) return this._drawHPBar(number, bar, data);
if (data.attribute === "attributes.hp") return this._drawHPBar(number, bar, data);
return super._drawBar(number, bar, data);
}
@ -41,7 +37,6 @@ export class Token5e extends Token {
* @private
*/
_drawHPBar(number, bar, data) {
// Extract health data
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
temp = Number(temp || 0);
@ -58,42 +53,50 @@ export class Token5e extends Token {
// Determine colors to use
const blk = 0x000000;
const hpColor = PIXI.utils.rgb2hex([(1-(colorPct/2)), colorPct, 0]);
const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]);
const c = CONFIG.SW5E.tokenHPColors;
// Determine the container size (logic borrowed from core)
const w = this.w;
let h = Math.max((canvas.dimensions.size / 12), 8);
if ( this.data.height >= 2 ) h *= 1.6;
let h = Math.max(canvas.dimensions.size / 12, 8);
if (this.data.height >= 2) h *= 1.6;
const bs = Math.clamped(h / 8, 1, 2);
const bs1 = bs+1;
const bs1 = bs + 1;
// Overall bar container
bar.clear()
bar.clear();
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
// Temporary maximum HP
if (tempmax > 0) {
const pct = max / effectiveMax;
bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
bar.beginFill(c.tempmax, 1.0)
.lineStyle(1, blk, 1.0)
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Maximum HP penalty
else if (tempmax < 0) {
const pct = (max + tempmax) / max;
bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
bar.beginFill(c.negmax, 1.0)
.lineStyle(1, blk, 1.0)
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Health bar
bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, valuePct*w, h, 2)
bar.beginFill(hpColor, 1.0)
.lineStyle(bs, blk, 1.0)
.drawRoundedRect(0, 0, valuePct * w, h, 2);
// Temporary hit points
if ( temp > 0 ) {
bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1);
if (temp > 0) {
bar.beginFill(c.temp, 1.0)
.lineStyle(0)
.drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1);
}
// Set position
let posY = (number === 0) ? (this.h - h) : 0;
let posY = number === 0 ? this.h - h : 0;
bar.position.set(0, posY);
}
}

173
sw5e.js
View file

@ -8,17 +8,17 @@
*/
// Import Modules
import { SW5E } from "./module/config.js";
import { registerSystemSettings } from "./module/settings.js";
import { preloadHandlebarsTemplates } from "./module/templates.js";
import { _getInitiativeFormula } from "./module/combat.js";
import { measureDistances } from "./module/canvas.js";
import {SW5E} from "./module/config.js";
import {registerSystemSettings} from "./module/settings.js";
import {preloadHandlebarsTemplates} from "./module/templates.js";
import {_getInitiativeFormula} from "./module/combat.js";
import {measureDistances} from "./module/canvas.js";
// Import Documents
import Actor5e from "./module/actor/entity.js";
import Item5e from "./module/item/entity.js";
import CharacterImporter from "./module/characterImporter.js";
import { TokenDocument5e, Token5e } from "./module/token.js"
import {TokenDocument5e, Token5e} from "./module/token.js";
// Import Applications
import AbilityTemplate from "./module/pixi/ability-template.js";
@ -46,10 +46,7 @@ import * as migrations from "./module/migration.js";
/* Foundry VTT Initialization */
/* -------------------------------------------- */
// Keep on while migrating to Foundry version 0.8
CONFIG.debug.hooks = true;
Hooks.once("init", function() {
Hooks.once("init", function () {
console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
// Create a SW5E namespace within the game global
@ -77,7 +74,7 @@ Hooks.once("init", function() {
Actor5e,
Item5e,
TokenDocument5e,
Token5e,
Token5e
},
macros: macros,
migrations: migrations,
@ -91,11 +88,7 @@ Hooks.once("init", function() {
CONFIG.Token.documentClass = TokenDocument5e;
CONFIG.Token.objectClass = Token5e;
CONFIG.time.roundTime = 6;
CONFIG.fontFamilies = [
"Engli-Besh",
"Open Sans",
"Russo One"
];
CONFIG.fontFamilies = ["Engli-Besh", "Open Sans", "Russo One"];
CONFIG.Dice.DamageRoll = dice.DamageRoll;
CONFIG.Dice.D20Roll = dice.D20Roll;
@ -145,14 +138,37 @@ Hooks.once("init", function() {
// makeDefault: true,
// label: "SW5E.SheetClassStarship"
// });
Actors.registerSheet('sw5e', ActorSheet5eVehicle, {
types: ['vehicle'],
Actors.registerSheet("sw5e", ActorSheet5eVehicle, {
types: ["vehicle"],
makeDefault: true,
label: "SW5E.SheetClassVehicle"
});
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("sw5e", ItemSheet5e, {
types: ['weapon', 'equipment', 'consumable', 'tool', 'loot', 'class', 'power', 'feat', 'species', 'backpack', 'archetype', 'classfeature', 'background', 'fightingmastery', 'fightingstyle', 'lightsaberform', 'deployment', 'deploymentfeature', 'starship', 'starshipfeature', 'starshipmod', 'venture'],
types: [
"weapon",
"equipment",
"consumable",
"tool",
"loot",
"class",
"power",
"feat",
"species",
"backpack",
"archetype",
"classfeature",
"background",
"fightingmastery",
"fightingstyle",
"lightsaberform",
"deployment",
"deploymentfeature",
"starship",
"starshipfeature",
"starshipmod",
"venture"
],
makeDefault: true,
label: "SW5E.SheetClassItem"
});
@ -161,7 +177,6 @@ Hooks.once("init", function() {
return preloadHandlebarsTemplates();
});
/* -------------------------------------------- */
/* Foundry VTT Setup */
/* -------------------------------------------- */
@ -169,31 +184,77 @@ Hooks.once("init", function() {
/**
* This function runs after game data has been requested and loaded from the servers, so entities exist
*/
Hooks.once("setup", function() {
Hooks.once("setup", function () {
// Localize CONFIG objects once up-front
const toLocalize = [
"abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments",
"armorProficiencies", "armorPropertiesTypes", "conditionTypes", "consumableTypes", "cover", "currencies", "damageResistanceTypes",
"damageTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", "languages",
"limitedUsePeriods", "movementTypes", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills",
"starshipRolessm", "starshipRolesmed", "starshipRoleslg", "starshipRoleshuge", "starshipRolesgrg", "starshipSkills",
"powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes",
"timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponSizes", "weaponTypes"
"abilities",
"abilityAbbreviations",
"abilityActivationTypes",
"abilityConsumptionTypes",
"actorSizes",
"alignments",
"armorProficiencies",
"armorPropertiesTypes",
"conditionTypes",
"consumableTypes",
"cover",
"currencies",
"damageResistanceTypes",
"damageTypes",
"distanceUnits",
"equipmentTypes",
"healingTypes",
"itemActionTypes",
"languages",
"limitedUsePeriods",
"movementTypes",
"movementUnits",
"polymorphSettings",
"proficiencyLevels",
"senses",
"skills",
"starshipRolessm",
"starshipRolesmed",
"starshipRoleslg",
"starshipRoleshuge",
"starshipRolesgrg",
"starshipSkills",
"powerComponents",
"powerLevels",
"powerPreparationModes",
"powerScalingModes",
"powerSchools",
"targetTypes",
"timePeriods",
"toolProficiencies",
"weaponProficiencies",
"weaponProperties",
"weaponSizes",
"weaponTypes"
];
// Exclude some from sorting where the default order matters
const noSort = [
"abilities", "alignments", "currencies", "distanceUnits", "movementUnits", "itemActionTypes", "proficiencyLevels",
"limitedUsePeriods", "powerComponents", "powerLevels", "powerPreparationModes", "weaponTypes"
"abilities",
"alignments",
"currencies",
"distanceUnits",
"movementUnits",
"itemActionTypes",
"proficiencyLevels",
"limitedUsePeriods",
"powerComponents",
"powerLevels",
"powerPreparationModes",
"weaponTypes"
];
// Localize and sort CONFIG objects
for ( let o of toLocalize ) {
const localized = Object.entries(CONFIG.SW5E[o]).map(e => {
for (let o of toLocalize) {
const localized = Object.entries(CONFIG.SW5E[o]).map((e) => {
return [e[0], game.i18n.localize(e[1])];
});
if ( !noSort.includes(o) ) localized.sort((a, b) => a[1].localeCompare(b[1]));
if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1]));
CONFIG.SW5E[o] = localized.reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
@ -202,7 +263,7 @@ Hooks.once("setup", function() {
// add DND5E translation for module compatability
game.i18n.translations.DND5E = game.i18n.translations.SW5E;
// console.log(game.settings.get("sw5e", "colorTheme"));
let theme = game.settings.get("sw5e", "colorTheme") + '-theme';
let theme = game.settings.get("sw5e", "colorTheme") + "-theme";
document.body.classList.add(theme);
});
@ -210,23 +271,25 @@ Hooks.once("setup", function() {
/**
* Once the entire VTT framework is initialized, check to see if we should perform a data migration
*/
Hooks.once("ready", function() {
Hooks.once("ready", function () {
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot));
// Determine whether a system migration is required and feasible
if ( !game.user.isGM ) return;
if (!game.user.isGM) return;
const currentVersion = game.settings.get("sw5e", "systemMigrationVersion");
const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6";
// Check for R1 SW5E versions
const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6";
const COMPATIBLE_MIGRATION_VERSION = 0.80;
const needsMigration = currentVersion && (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion));
const COMPATIBLE_MIGRATION_VERSION = 0.8;
const needsMigration =
currentVersion &&
(isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) ||
isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion));
if (!needsMigration && needsMigration !== "") return;
// Perform the migration
if ( currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion) ) {
if (currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion)) {
const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`;
ui.notifications.error(warning, {permanent: true});
}
@ -237,19 +300,17 @@ Hooks.once("ready", function() {
/* Canvas Initialization */
/* -------------------------------------------- */
Hooks.on("canvasInit", function() {
Hooks.on("canvasInit", function () {
// Extend Diagonal Measurement
canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement");
SquareGrid.prototype.measureDistances = measureDistances;
});
/* -------------------------------------------- */
/* Other Hooks */
/* -------------------------------------------- */
Hooks.on("renderChatMessage", (app, html, data) => {
// Display action buttons
chat.displayChatActionButtons(app, html, data);
@ -262,38 +323,36 @@ Hooks.on("renderChatMessage", (app, html, data) => {
Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions);
Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html));
Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions);
Hooks.on("renderSceneDirectory", (app, html, data)=> {
Hooks.on("getActorDirectoryEntryContext", Actor5e.addDirectoryContextOptions);
Hooks.on("renderSceneDirectory", (app, html, data) => {
//console.log(html.find("header.folder-header"));
setFolderBackground(html);
});
Hooks.on("renderActorDirectory", (app, html, data)=> {
Hooks.on("renderActorDirectory", (app, html, data) => {
setFolderBackground(html);
CharacterImporter.addImportButton(html);
});
Hooks.on("renderItemDirectory", (app, html, data)=> {
Hooks.on("renderItemDirectory", (app, html, data) => {
setFolderBackground(html);
});
Hooks.on("renderJournalDirectory", (app, html, data)=> {
Hooks.on("renderJournalDirectory", (app, html, data) => {
setFolderBackground(html);
});
Hooks.on("renderRollTableDirectory", (app, html, data)=> {
Hooks.on("renderRollTableDirectory", (app, html, data) => {
setFolderBackground(html);
});
Hooks.on("ActorSheet5eCharacterNew", (app, html, data) => {
console.log("renderSwaltSheet");
});
// FIXME: This helper is needed for the vehicle sheet. It should probably be refactored.
Handlebars.registerHelper('getProperty', function (data, property) {
Handlebars.registerHelper("getProperty", function (data, property) {
return getProperty(data, property);
});
function setFolderBackground(html) {
html.find("header.folder-header").each(function() {
html.find("header.folder-header").each(function () {
let bgColor = $(this).css("background-color");
if(bgColor == undefined)
bgColor = "rgb(255,255,255)";
$(this).closest('li').css("background-color", bgColor);
})
if (bgColor == undefined) bgColor = "rgb(255,255,255)";
$(this).closest("li").css("background-color", bgColor);
});
}