forked from GitHub-Mirrors/foundry-sw5e
Spot the link / entityClass error!
This commit is contained in:
parent
5f5a145626
commit
d392b568db
56 changed files with 6353 additions and 3288 deletions
File diff suppressed because it is too large
Load diff
|
@ -1,14 +1,14 @@
|
|||
import {TraitSelector} from "../../apps/trait-selector.js";
|
||||
import {ActorSheetFlags} from "../../apps/actor-flags.js";
|
||||
import Item5e from "../../item/entity.js";
|
||||
import TraitSelector from "../../apps/trait-selector.js";
|
||||
import ActorSheetFlags from "../../apps/actor-flags.js";
|
||||
import {SW5E} from '../../config.js';
|
||||
|
||||
/**
|
||||
* Extend the basic ActorSheet class to do all the D&D5e things!
|
||||
* Extend the basic ActorSheet class to do all the SW5e things!
|
||||
* This sheet is an Abstract layer which is not used.
|
||||
*
|
||||
* @type {ActorSheet}
|
||||
* @extends {ActorSheet}
|
||||
*/
|
||||
export class ActorSheet5e extends ActorSheet {
|
||||
export default class ActorSheet5e extends ActorSheet {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
|
@ -19,7 +19,8 @@ export class ActorSheet5e extends ActorSheet {
|
|||
this._filters = {
|
||||
inventory: new Set(),
|
||||
powerbook: new Set(),
|
||||
features: new Set()
|
||||
features: new Set(),
|
||||
effects: new Set()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -31,12 +32,20 @@ export class ActorSheet5e extends ActorSheet {
|
|||
scrollY: [
|
||||
".inventory .inventory-list",
|
||||
".features .inventory-list",
|
||||
".powerbook .inventory-list"
|
||||
".powerbook .inventory-list",
|
||||
".effects .inventory-list"
|
||||
],
|
||||
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/${this.actor.data.type}-sheet.html`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
@ -53,6 +62,7 @@ export class ActorSheet5e extends ActorSheet {
|
|||
cssClass: isOwner ? "editable" : "locked",
|
||||
isCharacter: this.entity.data.type === "character",
|
||||
isNPC: this.entity.data.type === "npc",
|
||||
isVehicle: this.entity.data.type === 'vehicle',
|
||||
config: CONFIG.SW5E,
|
||||
};
|
||||
|
||||
|
@ -74,12 +84,14 @@ export class ActorSheet5e extends ActorSheet {
|
|||
abl.label = CONFIG.SW5E.abilities[a];
|
||||
}
|
||||
|
||||
// Update skill labels
|
||||
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
|
||||
skl.ability = data.actor.data.abilities[skl.ability].label.substring(0, 3);
|
||||
skl.icon = this._getProficiencyIcon(skl.value);
|
||||
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
|
||||
skl.label = CONFIG.SW5E.skills[s];
|
||||
// Skills
|
||||
if (data.actor.data.skills) {
|
||||
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
|
||||
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
|
||||
skl.icon = this._getProficiencyIcon(skl.value);
|
||||
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
|
||||
skl.label = CONFIG.SW5E.skills[s];
|
||||
}
|
||||
}
|
||||
|
||||
// Update traits
|
||||
|
@ -88,17 +100,25 @@ export class ActorSheet5e extends ActorSheet {
|
|||
// Prepare owned items
|
||||
this._prepareItems(data);
|
||||
|
||||
// Prepare active effects
|
||||
this._prepareEffects(data);
|
||||
|
||||
// Return data to the sheet
|
||||
return data
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
|
||||
* @param {object} traits The raw traits data object from the actor data
|
||||
* @private
|
||||
*/
|
||||
_prepareTraits(traits) {
|
||||
const map = {
|
||||
"dr": CONFIG.SW5E.damageTypes,
|
||||
"di": CONFIG.SW5E.damageTypes,
|
||||
"dv": CONFIG.SW5E.damageTypes,
|
||||
"dr": CONFIG.SW5E.damageResistanceTypes,
|
||||
"di": CONFIG.SW5E.damageResistanceTypes,
|
||||
"dv": CONFIG.SW5E.damageResistanceTypes,
|
||||
"ci": CONFIG.SW5E.conditionTypes,
|
||||
"languages": CONFIG.SW5E.languages,
|
||||
"armorProf": CONFIG.SW5E.armorProficiencies,
|
||||
|
@ -127,6 +147,43 @@ export class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare the data structure for Active Effects which are currently applied to the Actor.
|
||||
* @param {object} data The object of rendering data which is being prepared
|
||||
* @private
|
||||
*/
|
||||
_prepareEffects(data) {
|
||||
|
||||
// Define effect header categories
|
||||
const categories = {
|
||||
temporary: {
|
||||
label: "Temporary Effects",
|
||||
effects: []
|
||||
},
|
||||
passive: {
|
||||
label: "Passive Effects",
|
||||
effects: []
|
||||
},
|
||||
inactive: {
|
||||
label: "Inactive Effects",
|
||||
effects: []
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over active effects, classifying them into categories
|
||||
for ( let e of this.actor.effects ) {
|
||||
e._getSourceName(); // Trigger a lookup for the source name
|
||||
if ( e.data.disabled ) categories.inactive.effects.push(e);
|
||||
else if ( e.isTemporary ) categories.temporary.effects.push(e);
|
||||
else categories.passive.effects.push(e);
|
||||
}
|
||||
|
||||
// Add the prepared categories of effects to the rendering data
|
||||
return data.effects = categories;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Insert a power into the powerbook object when rendering the character sheet
|
||||
* @param {Object} data The Actor data being prepared
|
||||
|
@ -153,18 +210,18 @@ export class ActorSheet5e extends ActorSheet {
|
|||
};
|
||||
|
||||
// Format a powerbook entry for a certain indexed level
|
||||
const registerSection = (sl, i, label, level={}) => {
|
||||
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
|
||||
powerbook[i] = {
|
||||
order: i,
|
||||
label: label,
|
||||
usesSlots: i > 0,
|
||||
canCreate: owner && (i >= 1),
|
||||
canCreate: owner,
|
||||
canPrepare: (data.actor.type === "character") && (i >= 1),
|
||||
powers: [],
|
||||
uses: useLabels[i] || level.value || 0,
|
||||
slots: useLabels[i] || level.max || 0,
|
||||
override: level.override || 0,
|
||||
dataset: {"type": "power", "level": i},
|
||||
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},
|
||||
prop: sl
|
||||
};
|
||||
};
|
||||
|
@ -177,7 +234,7 @@ export class ActorSheet5e extends ActorSheet {
|
|||
return max;
|
||||
}, 0);
|
||||
|
||||
// Structure the powerbook for every level up to the maximum which has a slot
|
||||
// Level-based powercasters have cantrips and leveled slots
|
||||
if ( maxLevel > 0 ) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
for (let lvl = 1; lvl <= maxLevel; lvl++) {
|
||||
|
@ -185,9 +242,18 @@ export class ActorSheet5e extends ActorSheet {
|
|||
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pact magic users have cantrips and a pact magic section
|
||||
if ( levels.pact && levels.pact.max ) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
registerSection("pact", sections.pact, CONFIG.SW5E.powerPreparationModes.pact, levels.pact);
|
||||
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
const l = levels.pact;
|
||||
const config = CONFIG.SW5E.powerPreparationModes.pact;
|
||||
registerSection("pact", sections.pact, config, {
|
||||
prepMode: "pact",
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
override: l.override
|
||||
});
|
||||
}
|
||||
|
||||
// Iterate over every power item, adding powers to the powerbook by section
|
||||
|
@ -196,17 +262,24 @@ export class ActorSheet5e extends ActorSheet {
|
|||
let s = power.data.level || 0;
|
||||
const sl = `power${s}`;
|
||||
|
||||
// Powercasting mode specific headings
|
||||
// Specialized powercasting modes (if they exist)
|
||||
if ( mode in sections ) {
|
||||
s = sections[mode];
|
||||
if ( !powerbook[s] ){
|
||||
registerSection(sl, s, CONFIG.SW5E.powerPreparationModes[mode], levels[mode]);
|
||||
const l = levels[mode] || {};
|
||||
const config = CONFIG.SW5E.powerPreparationModes[mode];
|
||||
registerSection(mode, s, config, {
|
||||
prepMode: mode,
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
override: l.override
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Higher-level power headings
|
||||
// Sections for higher-level powers which the caster "should not" have, but power items exist for
|
||||
else if ( !powerbook[s] ) {
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], levels[sl]);
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
|
||||
}
|
||||
|
||||
// Add the power to the relevant heading
|
||||
|
@ -252,7 +325,7 @@ export class ActorSheet5e extends ActorSheet {
|
|||
|
||||
// Equipment-specific filters
|
||||
if ( filters.has("equipped") ) {
|
||||
if (data.equipped && data.equipped !== true) return false;
|
||||
if ( data.equipped !== true ) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
@ -295,8 +368,10 @@ export class ActorSheet5e extends ActorSheet {
|
|||
// Editable Only Listeners
|
||||
if ( this.isEditable ) {
|
||||
|
||||
// Relative updates for numeric fields
|
||||
html.find('input[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
|
||||
// Input focus and update
|
||||
const inputs = html.find("input");
|
||||
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));
|
||||
|
@ -316,6 +391,10 @@ export class ActorSheet5e extends ActorSheet {
|
|||
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(this._onManageActiveEffect.bind(this));
|
||||
|
||||
}
|
||||
|
||||
// Owner Only Listeners
|
||||
|
@ -328,14 +407,6 @@ export class ActorSheet5e extends ActorSheet {
|
|||
// Roll Skill Checks
|
||||
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
|
||||
|
||||
// Item Dragging
|
||||
let handler = ev => this._onDragItemStart(ev);
|
||||
html.find('li.item').each((i, li) => {
|
||||
if ( li.classList.contains("inventory-header") ) return;
|
||||
li.setAttribute("draggable", true);
|
||||
li.addEventListener("dragstart", handler, false);
|
||||
});
|
||||
|
||||
// Item Rolling
|
||||
html.find('.item .item-image').click(event => this._onItemRoll(event));
|
||||
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
|
||||
|
@ -424,37 +495,9 @@ export class ActorSheet5e extends ActorSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDrop (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Get dropped data
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.dataTransfer.getData('text/plain'));
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle a polymorph
|
||||
if (data && (data.type === "Actor")) {
|
||||
if (game.user.isGM || (game.settings.get('sw5e', 'allowPolymorphing') && this.actor.owner)) {
|
||||
return this._onDropPolymorph(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Call parent on drop logic
|
||||
return super._onDrop(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle dropping an Actor on the sheet to trigger a Polymorph workflow
|
||||
* @param {DragEvent} event The drop event
|
||||
* @param {Object} data The data transfer
|
||||
* @private
|
||||
*/
|
||||
async _onDropPolymorph(event, data) {
|
||||
async _onDropActor(event, data) {
|
||||
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
|
||||
if ( !canPolymorph ) return false;
|
||||
|
||||
// Get the target actor
|
||||
let sourceActor = null;
|
||||
|
@ -521,6 +564,23 @@ export class ActorSheet5e extends ActorSheet {
|
|||
}).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
// Create a Consumable power scroll on the Inventory tab
|
||||
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
|
||||
const scroll = await Item5e.createScrollFromPower(itemData);
|
||||
itemData = scroll.data;
|
||||
}
|
||||
|
||||
// Create the owned item as normal
|
||||
// TODO remove conditional logic in 0.7.x
|
||||
if (isNewerVersion(game.data.version, "0.6.9")) return super._onDropItemCreate(itemData);
|
||||
else return this.actor.createEmbeddedEntity("OwnedItem", itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
@ -634,7 +694,7 @@ export class ActorSheet5e extends ActorSheet {
|
|||
const header = event.currentTarget;
|
||||
const type = header.dataset.type;
|
||||
const itemData = {
|
||||
name: `New ${type.capitalize()}`,
|
||||
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
|
||||
type: type,
|
||||
data: duplicate(header.dataset)
|
||||
};
|
||||
|
@ -671,6 +731,28 @@ export class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Manage Active Effect instances through the Actor Sheet via effect control buttons.
|
||||
* @param {MouseEvent} event The left-click event on the effect control
|
||||
* @private
|
||||
*/
|
||||
_onManageActiveEffect(event) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
const li = a.closest(".effect");
|
||||
const effect = this.actor.effects.get(li.dataset.effectId);
|
||||
switch ( a.dataset.action ) {
|
||||
case "edit":
|
||||
return effect.sheet.render(true);
|
||||
case "delete":
|
||||
return effect.delete();
|
||||
case "toggle":
|
||||
return effect.update({disabled: !effect.data.disabled});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling an Ability check, either a test or a saving throw
|
||||
* @param {Event} event The originating click event
|
||||
|
@ -736,11 +818,8 @@ export class ActorSheet5e extends ActorSheet {
|
|||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
const label = a.parentElement.querySelector("label");
|
||||
const options = {
|
||||
name: label.getAttribute("for"),
|
||||
title: label.innerText,
|
||||
choices: CONFIG.SW5E[a.dataset.options]
|
||||
};
|
||||
const choices = CONFIG.SW5E[a.dataset.options];
|
||||
const options = { name: a.dataset.target, title: label.innerText, choices };
|
||||
new TraitSelector(this.actor, options).render(true)
|
||||
}
|
||||
|
||||
|
@ -760,4 +839,90 @@ export class ActorSheet5e extends ActorSheet {
|
|||
});
|
||||
return buttons;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* DEPRECATED */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* TODO: Remove once 0.7.x is release
|
||||
* @deprecated since 0.7.0
|
||||
*/
|
||||
async _onDrop (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Get dropped data
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.dataTransfer.getData('text/plain'));
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
if ( !data ) return false;
|
||||
|
||||
// Handle the drop with a Hooked function
|
||||
const allowed = Hooks.call("dropActorSheetData", this.actor, this, data);
|
||||
if ( allowed === false ) return;
|
||||
|
||||
// Case 1 - Dropped Item
|
||||
if ( data.type === "Item" ) {
|
||||
return this._onDropItem(event, data);
|
||||
}
|
||||
|
||||
// Case 2 - Dropped Actor
|
||||
if ( data.type === "Actor" ) {
|
||||
return this._onDropActor(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* TODO: Remove once 0.7.x is release
|
||||
* @deprecated since 0.7.0
|
||||
*/
|
||||
async _onDropItem(event, data) {
|
||||
if ( !this.actor.owner ) return false;
|
||||
let itemData = await this._getItemDropData(event, data);
|
||||
|
||||
// Handle item sorting within the same Actor
|
||||
const actor = this.actor;
|
||||
let sameActor = (data.actorId === actor._id) || (actor.isToken && (data.tokenId === actor.token.id));
|
||||
if (sameActor) return this._onSortItem(event, itemData);
|
||||
|
||||
// Create a new item
|
||||
this._onDropItemCreate(itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* TODO: Remove once 0.7.x is release
|
||||
* @deprecated since 0.7.0
|
||||
*/
|
||||
async _getItemDropData(event, data) {
|
||||
let itemData = null;
|
||||
|
||||
// Case 1 - Import from a Compendium pack
|
||||
if (data.pack) {
|
||||
const pack = game.packs.get(data.pack);
|
||||
if (pack.metadata.entity !== "Item") return;
|
||||
itemData = await pack.getEntry(data.id);
|
||||
}
|
||||
|
||||
// Case 2 - Data explicitly provided
|
||||
else if (data.data) {
|
||||
itemData = data.data;
|
||||
}
|
||||
|
||||
// Case 3 - Import from World entity
|
||||
else {
|
||||
let item = game.items.get(data.id);
|
||||
if (!item) return;
|
||||
itemData = item.data;
|
||||
}
|
||||
|
||||
// Return a copy of the extracted data
|
||||
return duplicate(itemData);
|
||||
}
|
||||
}
|
|
@ -1,37 +1,21 @@
|
|||
import { ActorSheet5e } from "../sheets/base.js";
|
||||
import ActorSheet5e from "../sheets/base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for NPC type characters in the D&D5E system.
|
||||
* An Actor sheet for NPC type characters in the SW5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
* @extends {ActorSheet5e}
|
||||
*/
|
||||
export class ActorSheet5eNPC extends ActorSheet5e {
|
||||
export default class ActorSheet5eNPC extends ActorSheet5e {
|
||||
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
*/
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 600,
|
||||
height: 658
|
||||
height: 680
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the correct HTML template path to use for rendering this particular sheet
|
||||
* @type {String}
|
||||
*/
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
|
||||
return "systems/sw5e/templates/actors/npc-sheet.html";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
@ -42,16 +26,16 @@ export class ActorSheet5eNPC extends ActorSheet5e {
|
|||
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: { label: "Attacks", items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
||||
actions: { label: "Actions", items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "Features", items: [], dataset: {type: "feat"} },
|
||||
equipment: { label: "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) => {
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = item.data.quantity ? item.data.quantity > 1 : false;
|
||||
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));
|
||||
|
@ -86,9 +70,7 @@ export class ActorSheet5eNPC extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
|
||||
|
@ -103,12 +85,7 @@ export class ActorSheet5eNPC extends ActorSheet5e {
|
|||
/* Object Updates */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This method is called upon form submission after form data is validated
|
||||
* @param event {Event} The initial triggering submission event
|
||||
* @param formData {Object} The object of validated form data with which to update the object
|
||||
* @private
|
||||
*/
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
|
||||
// Format NPC Challenge Rating
|
||||
|
@ -126,14 +103,9 @@ export class ActorSheet5eNPC extends ActorSheet5e {
|
|||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
||||
// Rollable Health Formula
|
||||
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
|
||||
}
|
||||
|
||||
|
@ -152,4 +124,4 @@ export class ActorSheet5eNPC extends ActorSheet5e {
|
|||
AudioHelper.play({src: CONFIG.sounds.dice});
|
||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
381
module/actor/sheets/vehicle.js
Normal file
381
module/actor/sheets/vehicle.js
Normal file
|
@ -0,0 +1,381 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for Vehicle type actors.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eVehicle extends ActorSheet5e {
|
||||
/**
|
||||
* Define default rendering options for the Vehicle sheet.
|
||||
* @returns {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "vehicle"],
|
||||
width: 605,
|
||||
height: 680
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Creates a new cargo entry for a vehicle Actor.
|
||||
*/
|
||||
static get newCargo() {
|
||||
return {
|
||||
name: '',
|
||||
quantity: 1
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Compute the total weight of the vehicle's cargo.
|
||||
* @param {Number} totalWeight The cumulative item weight from inventory items
|
||||
* @param {Object} actorData The data object for the Actor being rendered
|
||||
* @returns {{max: number, value: number, pct: number}}
|
||||
* @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;
|
||||
|
||||
// Vehicle weights are an order of magnitude greater.
|
||||
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
|
||||
|
||||
// Compute overall encumbrance
|
||||
const enc = {
|
||||
max: actorData.data.attributes.capacity.cargo,
|
||||
value: Math.round(totalWeight * 10) / 10
|
||||
};
|
||||
enc.pct = Math.min(enc.value * 100 / enc.max, 99);
|
||||
return enc;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare items that are mounted to a vehicle and require one or more crew
|
||||
* to operate.
|
||||
* @private
|
||||
*/
|
||||
_prepareCrewedItem(item) {
|
||||
|
||||
// Determine crewed status
|
||||
const isCrewed = item.data.crewed;
|
||||
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') {
|
||||
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 = '—';
|
||||
}
|
||||
|
||||
// Prepare vehicle weapons
|
||||
if (item.type === 'equipment' || item.type === 'weapon') {
|
||||
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the Vehicle sheet.
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
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 features = {
|
||||
actions: {
|
||||
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'
|
||||
}]
|
||||
},
|
||||
equipment: {
|
||||
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
|
||||
columns: equipmentColumns
|
||||
},
|
||||
passive: {
|
||||
label: game.i18n.localize('SW5E.Features'),
|
||||
items: [],
|
||||
dataset: {type: 'feat'}
|
||||
},
|
||||
reactions: {
|
||||
label: game.i18n.localize('SW5E.ReactionPl'),
|
||||
items: [],
|
||||
dataset: {type: 'feat', 'activation.type': 'reaction'}
|
||||
},
|
||||
weapons: {
|
||||
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'weapon', 'weapon-type': 'siege'},
|
||||
columns: equipmentColumns
|
||||
}
|
||||
};
|
||||
|
||||
const cargo = {
|
||||
crew: {
|
||||
label: game.i18n.localize('SW5E.VehicleCrew'),
|
||||
items: data.data.cargo.crew,
|
||||
css: 'cargo-row crew',
|
||||
editableName: true,
|
||||
dataset: {type: 'crew'},
|
||||
columns: cargoColumns
|
||||
},
|
||||
passengers: {
|
||||
label: game.i18n.localize('SW5E.VehiclePassengers'),
|
||||
items: data.data.cargo.passengers,
|
||||
css: 'cargo-row passengers',
|
||||
editableName: true,
|
||||
dataset: {type: 'passengers'},
|
||||
columns: cargoColumns
|
||||
},
|
||||
cargo: {
|
||||
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'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
let totalWeight = 0;
|
||||
for (const item of data.items) {
|
||||
this._prepareCrewedItem(item);
|
||||
if (item.type === 'weapon') features.weapons.items.push(item);
|
||||
else if (item.type === 'equipment') features.equipment.items.push(item);
|
||||
else if (item.type === 'loot') {
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
}
|
||||
else if (item.type === '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);
|
||||
else features.actions.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
data.features = Object.values(features);
|
||||
data.cargo = Object.values(cargo);
|
||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.options.editable) return;
|
||||
|
||||
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())
|
||||
.change(this._onEditInSheet.bind(this));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle saving a cargo row (i.e. crew or passenger) in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor>|null}
|
||||
* @private
|
||||
*/
|
||||
_onCargoRowChange(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const row = target.closest('.item');
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
|
||||
// Get the cargo entry
|
||||
const cargo = duplicate(this.actor.data.data.cargo[property]);
|
||||
const entry = cargo[idx];
|
||||
if (!entry) return null;
|
||||
|
||||
// Update the cargo value
|
||||
const key = target.dataset.property || 'name';
|
||||
const type = target.dataset.dtype;
|
||||
let value = target.value;
|
||||
if (type === 'Number') value = Number(value);
|
||||
entry[key] = value;
|
||||
|
||||
// Perform the Actor update
|
||||
return this.actor.update({[`data.cargo.${property}`]: cargo});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle editing certain values like quantity, price, and weight in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onEditInSheet(event) {
|
||||
event.preventDefault();
|
||||
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;
|
||||
}
|
||||
return item.update({[`${property}`]: value});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemCreate(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const type = target.dataset.type;
|
||||
if (type === 'crew' || type === 'passengers') {
|
||||
const cargo = duplicate(this.actor.data.data.cargo[type]);
|
||||
cargo.push(this.constructor.newCargo);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
return super._onItemCreate(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle deleting a crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
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 cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
||||
return super._onItemDelete(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special handling for editing HP to clamp it within appropriate range.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onHPChange(event) {
|
||||
event.preventDefault();
|
||||
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});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle toggling an item's crewed status.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
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});
|
||||
}
|
||||
};
|
|
@ -2,7 +2,7 @@
|
|||
* A specialized Dialog subclass for ability usage
|
||||
* @type {Dialog}
|
||||
*/
|
||||
export class AbilityUseDialog extends Dialog {
|
||||
export default class AbilityUseDialog extends Dialog {
|
||||
constructor(item, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
this.options.classes = ["sw5e", "dialog"];
|
||||
|
@ -25,40 +25,156 @@ export 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");
|
||||
|
||||
const uses = item.data.data.uses;
|
||||
const recharge = item.data.data.recharge;
|
||||
// Prepare data
|
||||
const actorData = item.actor.data.data;
|
||||
const itemData = item.data.data;
|
||||
const uses = itemData.uses || {};
|
||||
const quantity = itemData.quantity || 0;
|
||||
const recharge = itemData.recharge || {};
|
||||
const recharges = !!recharge.value;
|
||||
|
||||
// Render the ability usage template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", {
|
||||
// Prepare dialog form data
|
||||
const data = {
|
||||
item: item.data,
|
||||
canUse: recharges ? recharge.charged : uses.value > 0,
|
||||
consume: true,
|
||||
uses: uses,
|
||||
recharges: !!recharge.value,
|
||||
isCharged: recharge.charged,
|
||||
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
|
||||
note: this._getAbilityUseNote(item.data, uses, recharge),
|
||||
hasLimitedUses: uses.max || recharges,
|
||||
canUse: recharges ? recharge.charged : (quantity > 0 && !uses.value) || uses.value > 0,
|
||||
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
||||
perLabel: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
errors: []
|
||||
};
|
||||
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);
|
||||
|
||||
// Create the Dialog and return as a Promise
|
||||
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
|
||||
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
|
||||
return new Promise((resolve) => {
|
||||
let formData = null;
|
||||
const dlg = new this(item, {
|
||||
title: `${item.name}: Ability Configuration`,
|
||||
title: `${item.name}: Usage Configuration`,
|
||||
content: html,
|
||||
buttons: {
|
||||
use: {
|
||||
icon: '<i class="fas fa-fist-raised"></i>',
|
||||
label: "Use Ability",
|
||||
callback: html => formData = new FormData(html[0].querySelector("#ability-use-form"))
|
||||
icon: `<i class="fas ${icon}"></i>`,
|
||||
label: label,
|
||||
callback: html => resolve(new FormData(html[0].querySelector("form")))
|
||||
}
|
||||
},
|
||||
default: "use",
|
||||
close: () => resolve(formData)
|
||||
close: () => resolve(null)
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get dialog data related to limited power slots
|
||||
* @private
|
||||
*/
|
||||
static _getPowerData(actorData, itemData, data) {
|
||||
|
||||
// Determine whether the power may be up-cast
|
||||
const lvl = itemData.level;
|
||||
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
|
||||
// If can't upcast, return early and don't bother calculating available power slots
|
||||
if (!canUpcast) {
|
||||
data = mergeObject(data, { isPower: true, canUpcast });
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const label = CONFIG.SW5E.powerLevels[i];
|
||||
const l = actorData.powers["power"+i] || {max: 0, override: null};
|
||||
let max = parseInt(l.override || l.max || 0);
|
||||
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
arr.push({
|
||||
level: i,
|
||||
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);
|
||||
|
||||
// If this character has pact slots, present them as an option for casting the power.
|
||||
const pact = actorData.powers.pact;
|
||||
if (pact.level >= lvl) {
|
||||
powerLevels.push({
|
||||
level: 'pact',
|
||||
label: `${game.i18n.format('SW5E.PowerLevelPact', {level: pact.level, n: pact.value})}`,
|
||||
canCast: true,
|
||||
hasSlots: pact.value > 0
|
||||
});
|
||||
}
|
||||
const canCast = powerLevels.some(l => l.hasSlots);
|
||||
|
||||
// Return merged data
|
||||
data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
|
||||
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the ability usage note that is displayed
|
||||
* @private
|
||||
*/
|
||||
static _getAbilityUseNote(item, uses, recharge) {
|
||||
|
||||
// Zero quantity
|
||||
const quantity = item.data.quantity;
|
||||
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
|
||||
|
||||
// Abilities which use Recharge
|
||||
if ( !!recharge.value ) {
|
||||
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
|
||||
type: item.type,
|
||||
})
|
||||
}
|
||||
|
||||
// Does not use any resource
|
||||
if ( !uses.per || !uses.max ) return "";
|
||||
|
||||
// Consumables
|
||||
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";
|
||||
return game.i18n.format(str, {
|
||||
type: item.data.consumableType,
|
||||
value: uses.value,
|
||||
quantity: item.data.quantity,
|
||||
});
|
||||
}
|
||||
|
||||
// Other Items
|
||||
else {
|
||||
return game.i18n.format("SW5E.AbilityUseNormalHint", {
|
||||
type: item.type,
|
||||
value: uses.value,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static _handleSubmit(formData, item) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
export class ActorSheetFlags extends BaseEntitySheet {
|
||||
static get defaultOptions() {
|
||||
/**
|
||||
* An application class which provides advanced configuration for special character flags which modify an Actor
|
||||
* @extends {BaseEntitySheet}
|
||||
*/
|
||||
export default class ActorSheetFlags extends BaseEntitySheet {
|
||||
static get defaultOptions() {
|
||||
const options = super.defaultOptions;
|
||||
return mergeObject(options, {
|
||||
id: "actor-flags",
|
||||
|
@ -68,10 +72,10 @@ export class ActorSheetFlags extends BaseEntitySheet {
|
|||
{name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
|
||||
{name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
|
||||
{name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
|
||||
{name: "data.bonuses.msak.attack", label: "SW5E.BonusMSAttack"},
|
||||
{name: "data.bonuses.msak.damage", label: "SW5E.BonusMSDamage"},
|
||||
{name: "data.bonuses.rsak.attack", label: "SW5E.BonusRSAttack"},
|
||||
{name: "data.bonuses.rsak.damage", label: "SW5E.BonusRSDamage"},
|
||||
{name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
|
||||
{name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
|
||||
{name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
|
||||
{name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
|
||||
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
|
||||
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
|
||||
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
|
||||
|
@ -91,7 +95,7 @@ export class ActorSheetFlags extends BaseEntitySheet {
|
|||
*/
|
||||
async _updateObject(event, formData) {
|
||||
const actor = this.object;
|
||||
const updateData = expandObject(formData);
|
||||
let updateData = expandObject(formData);
|
||||
|
||||
// Unset any flags which are "false"
|
||||
let unset = false;
|
||||
|
@ -106,7 +110,18 @@ export class ActorSheetFlags extends BaseEntitySheet {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply the changes
|
||||
// Clear any bonuses which are whitespace only
|
||||
for ( let b of Object.values(updateData.data.bonuses ) ) {
|
||||
for ( let [k, v] of Object.entries(b) ) {
|
||||
b[k] = v.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Diff the data against any applied overrides and apply
|
||||
// TODO: Remove this logical gate once 0.7.x is release channel
|
||||
if ( !isNewerVersion("0.7.1", game.data.version) ){
|
||||
updateData = diffObject(this.object.data, updateData);
|
||||
}
|
||||
await actor.update(updateData, {diff: false});
|
||||
}
|
||||
}
|
||||
|
|
69
module/apps/long-rest.js
Normal file
69
module/apps/long-rest.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* A helper Dialog subclass for completing a long rest
|
||||
* @extends {Dialog}
|
||||
*/
|
||||
export default class LongRestDialog extends Dialog {
|
||||
constructor(actor, dialogData = {}, options = {}) {
|
||||
super(dialogData, options);
|
||||
this.actor = actor;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
template: "systems/sw5e/templates/apps/long-rest.html",
|
||||
classes: ["sw5e", "dialog"]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
const variant = game.settings.get("sw5e", "restVariant");
|
||||
data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
|
||||
data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
|
||||
* workflow has been resolved.
|
||||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async longRestDialog({ actor } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, {
|
||||
title: "Long Rest",
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: "Rest",
|
||||
callback: html => {
|
||||
let newDay = false;
|
||||
if (game.settings.get("sw5e", "restVariant") === "normal")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
else if(game.settings.get("sw5e", "restVariant") === "gritty")
|
||||
newDay = true;
|
||||
resolve(newDay);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: "Cancel",
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
default: 'rest',
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import LongRestDialog from "./long-rest.js";
|
||||
|
||||
/**
|
||||
* A helper Dialog subclass for rolling Hit Dice on short rest
|
||||
* @type {Dialog}
|
||||
* @extends {Dialog}
|
||||
*/
|
||||
export class ShortRestDialog extends Dialog {
|
||||
export default class ShortRestDialog extends Dialog {
|
||||
constructor(actor, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
|
||||
|
@ -34,6 +36,8 @@ export class ShortRestDialog extends Dialog {
|
|||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
|
||||
// Determine Hit Dice
|
||||
data.availableHD = this.actor.data.items.reduce((hd, item) => {
|
||||
if ( item.type === "class" ) {
|
||||
const d = item.data;
|
||||
|
@ -45,6 +49,11 @@ export class ShortRestDialog extends Dialog {
|
|||
}, {});
|
||||
data.canRoll = this.actor.data.data.attributes.hd > 0;
|
||||
data.denomination = this._denom;
|
||||
|
||||
// Determine rest type
|
||||
const variant = game.settings.get("sw5e", "restVariant");
|
||||
data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
|
||||
data.newDay = false; // It may be a new day, but not by default
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -56,7 +65,6 @@ export class ShortRestDialog extends Dialog {
|
|||
super.activateListeners(html);
|
||||
let btn = html.find("#roll-hd");
|
||||
btn.click(this._onRollHitDie.bind(this));
|
||||
super.activateListeners(html);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -83,21 +91,27 @@ export class ShortRestDialog extends Dialog {
|
|||
* @return {Promise}
|
||||
*/
|
||||
static async shortRestDialog({actor}={}) {
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, {
|
||||
title: "Short Rest",
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: "Rest",
|
||||
callback: () => resolve(true)
|
||||
callback: html => {
|
||||
let newDay = false;
|
||||
if (game.settings.get("sw5e", "restVariant") === "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
resolve(newDay);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: "Cancel",
|
||||
callback: () => resolve(false)
|
||||
callback: reject
|
||||
}
|
||||
}
|
||||
},
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
|
@ -108,31 +122,12 @@ export class ShortRestDialog extends Dialog {
|
|||
/**
|
||||
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
|
||||
* workflow has been resolved.
|
||||
* @deprecated
|
||||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async longRestDialog({actor}={}) {
|
||||
const content = `<p>Take a long rest?</p><p>On a long rest you will recover hit points, half your maximum hit dice,
|
||||
class resources, limited use item charges, and power slots.</p>`;
|
||||
return new Promise((resolve, reject) => {
|
||||
new Dialog({
|
||||
title: "Long Rest",
|
||||
content: content,
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: "Rest",
|
||||
callback: resolve
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: "Cancel",
|
||||
callback: reject
|
||||
},
|
||||
},
|
||||
default: 'rest',
|
||||
close: reject
|
||||
}, {classes: ["sw5e", "dialog"]}).render(true);
|
||||
});
|
||||
console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
|
||||
return LongRestDialog.longRestDialog(...arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
/**
|
||||
* A specialized Dialog subclass for casting a spell item at a certain level
|
||||
* @type {Dialog}
|
||||
*/
|
||||
export class SpellCastDialog extends Dialog {
|
||||
constructor(actor, item, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
this.options.classes = ["dnd5e", "dialog"];
|
||||
|
||||
/**
|
||||
* Store a reference to the Actor entity which is casting the spell
|
||||
* @type {Actor5e}
|
||||
*/
|
||||
this.actor = actor;
|
||||
|
||||
/**
|
||||
* Store a reference to the Item entity which is the spell being cast
|
||||
* @type {Item5e}
|
||||
*/
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A constructor function which displays the Spell Cast Dialog app for a given Actor and Item.
|
||||
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
|
||||
* @param {Actor5e} actor
|
||||
* @param {Item5e} item
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async create(actor, item) {
|
||||
const ad = actor.data.data;
|
||||
const id = item.data.data;
|
||||
|
||||
// Determine whether the spell may be upcast
|
||||
const lvl = id.level;
|
||||
const canUpcast = (lvl > 0) && CONFIG.DND5E.spellUpcastModes.includes(id.preparation.mode);
|
||||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
const spellLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const l = ad.spells["spell"+i] || {max: 0, override: null};
|
||||
let max = parseInt(l.override || l.max || 0);
|
||||
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? `${CONFIG.DND5E.spellLevels[i]} (${slots} Slots)` : CONFIG.DND5E.spellLevels[i],
|
||||
canCast: canUpcast && (max > 0),
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
|
||||
const pact = ad.spells.pact;
|
||||
if (pact.level >= lvl) {
|
||||
// If this character has pact slots, present them as an option for
|
||||
// casting the spell.
|
||||
spellLevels.push({
|
||||
level: 'pact',
|
||||
label: game.i18n.localize('DND5E.SpellLevelPact')
|
||||
+ ` (${game.i18n.localize('DND5E.Level')} ${pact.level}) `
|
||||
+ `(${pact.value} ${game.i18n.localize('DND5E.Slots')})`,
|
||||
canCast: canUpcast,
|
||||
hasSlots: pact.value > 0
|
||||
});
|
||||
}
|
||||
|
||||
const canCast = spellLevels.some(l => l.hasSlots);
|
||||
|
||||
// Render the Spell casting template
|
||||
const html = await renderTemplate("systems/dnd5e/templates/apps/spell-cast.html", {
|
||||
item: item.data,
|
||||
canCast: canCast,
|
||||
canUpcast: canUpcast,
|
||||
spellLevels,
|
||||
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget
|
||||
});
|
||||
|
||||
// Create the Dialog and return as a Promise
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, item, {
|
||||
title: `${item.name}: Spell Configuration`,
|
||||
content: html,
|
||||
buttons: {
|
||||
cast: {
|
||||
icon: '<i class="fas fa-magic"></i>',
|
||||
label: "Cast",
|
||||
callback: html => resolve(new FormData(html[0].querySelector("#spell-config-form")))
|
||||
}
|
||||
},
|
||||
default: "cast",
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* A specialized form used to select from a checklist of attributes, traits, or properties
|
||||
* @extends {FormApplication}
|
||||
*/
|
||||
export class TraitSelector extends FormApplication {
|
||||
export default class TraitSelector extends FormApplication {
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
|
|
|
@ -1,32 +1,40 @@
|
|||
/**
|
||||
* Measure the distance between two pixel coordinates
|
||||
* See BaseGrid.measureDistance for more details
|
||||
*
|
||||
* @param {Object} p0 The origin coordinate {x, y}
|
||||
* @param {Object} p1 The destination coordinate {x, y}
|
||||
* @param {boolean} gridSpaces Enforce grid distance (if true) vs. direct point-to-point (if false)
|
||||
* @return {number} The distance between p1 and p0
|
||||
*/
|
||||
export const measureDistance = function(p0, p1, {gridSpaces=true}={}) {
|
||||
if ( !gridSpaces ) return BaseGrid.prototype.measureDistance.bind(this)(p0, p1, {gridSpaces});
|
||||
let gs = canvas.dimensions.size,
|
||||
ray = new Ray(p0, p1),
|
||||
nx = Math.abs(Math.ceil(ray.dx / gs)),
|
||||
ny = Math.abs(Math.ceil(ray.dy / gs));
|
||||
/** @override */
|
||||
export const measureDistances = function(segments, options={}) {
|
||||
if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
|
||||
|
||||
// Get the number of straight and diagonal moves
|
||||
let nDiagonal = Math.min(nx, ny),
|
||||
nStraight = Math.abs(ny - nx);
|
||||
// Track the total number of diagonals
|
||||
let nDiagonal = 0;
|
||||
const rule = this.parent.diagonalRule;
|
||||
const d = canvas.dimensions;
|
||||
|
||||
// Alternative DMG Movement
|
||||
if ( this.parent.diagonalRule === "5105" ) {
|
||||
let nd10 = Math.floor(nDiagonal / 2);
|
||||
let spaces = (nd10 * 2) + (nDiagonal - nd10) + nStraight;
|
||||
return spaces * canvas.dimensions.distance;
|
||||
}
|
||||
// Iterate over measured segments
|
||||
return segments.map(s => {
|
||||
let r = s.ray;
|
||||
|
||||
// Standard PHB Movement
|
||||
else return (nStraight + nDiagonal) * canvas.scene.data.gridDistance;
|
||||
// Determine the total distance traveled
|
||||
let nx = Math.abs(Math.ceil(r.dx / d.size));
|
||||
let ny = Math.abs(Math.ceil(r.dy / d.size));
|
||||
|
||||
// Determine the number of straight and diagonal moves
|
||||
let nd = Math.min(nx, ny);
|
||||
let ns = Math.abs(ny - nx);
|
||||
nDiagonal += nd;
|
||||
|
||||
// Alternative DMG Movement
|
||||
if (rule === "5105") {
|
||||
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
|
||||
let spaces = (nd10 * 2) + (nd - nd10) + ns;
|
||||
return spaces * canvas.dimensions.distance;
|
||||
}
|
||||
|
||||
// Euclidean Measurement
|
||||
else if (rule === "EUCL") {
|
||||
return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
|
||||
}
|
||||
|
||||
// Standard PHB Movement
|
||||
else return (ns + nd) * canvas.scene.data.gridDistance;
|
||||
});
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -39,8 +47,8 @@ const _TokenGetBarAttribute = Token.prototype.getBarAttribute;
|
|||
export const getBarAttribute = function(...args) {
|
||||
const data = _TokenGetBarAttribute.bind(this)(...args);
|
||||
if ( data && (data.attribute === "attributes.hp") ) {
|
||||
data.value += parseInt(data['temp'] || 0);
|
||||
data.max += parseInt(data['tempmax'] || 0);
|
||||
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
|
||||
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import {Actor5e} from "./actor/entity.js";
|
||||
|
||||
/**
|
||||
* Highlight critical success or failure on d20 rolls
|
||||
*/
|
||||
export const highlightCriticalSuccessFailure = function(message, html, data) {
|
||||
if ( !message.isRoll || !message.isRollVisible || !message.roll.parts.length ) return;
|
||||
if ( !message.isRoll || !message.isContentVisible ) return;
|
||||
|
||||
// Highlight rolls where the first part is a d20 roll
|
||||
const roll = message.roll;
|
||||
let d = roll.parts[0];
|
||||
const isD20Roll = d instanceof Die && (d.faces === 20) && (d.results.length === 1);
|
||||
if ( !isD20Roll ) return;
|
||||
if ( !roll.dice.length ) return;
|
||||
const d = roll.dice[0];
|
||||
|
||||
// Ensure it is not a modified roll
|
||||
const isModifiedRoll = ("success" in d.rolls[0]) || d.options.marginSuccess || d.options.marginFailure;
|
||||
// Ensure it is an un-modified d20 roll
|
||||
const isD20 = (d.faces === 20) && ( d.results.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
|
||||
|
@ -33,6 +33,7 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
|
|||
export const displayChatActionButtons = function(message, html, data) {
|
||||
const chatCard = html.find(".sw5e.chat-card");
|
||||
if ( chatCard.length > 0 ) {
|
||||
html.find(".flavor-text").remove();
|
||||
|
||||
// If the user is the message author or the actor owner, proceed
|
||||
let actor = game.actors.get(data.message.speaker.actor);
|
||||
|
@ -60,32 +61,55 @@ 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 => canvas.tokens.controlledTokens.length && li.find(".dice-roll").length;
|
||||
let canApply = li => {
|
||||
const message = game.messages.get(li.data("messageId"));
|
||||
return message.isRoll && message.isContentVisible && canvas.tokens.controlled.length;
|
||||
};
|
||||
options.push(
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDamage"),
|
||||
icon: '<i class="fas fa-user-minus"></i>',
|
||||
condition: canApply,
|
||||
callback: li => Actor5e.applyDamage(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 => Actor5e.applyDamage(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 => Actor5e.applyDamage(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 => Actor5e.applyDamage(li, 0.5)
|
||||
callback: li => applyChatCardDamage(li, 0.5)
|
||||
}
|
||||
);
|
||||
return options;
|
||||
};
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Apply rolled dice damage to the token or tokens which are currently controlled.
|
||||
* This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance
|
||||
*
|
||||
* @param {HTMLElement} roll The chat entry which contains the roll data
|
||||
* @param {Number} multiplier A damage multiplier to apply to the rolled damage.
|
||||
* @return {Promise}
|
||||
*/
|
||||
function applyChatCardDamage(roll, multiplier) {
|
||||
const amount = roll.find('.dice-total').text();
|
||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
||||
const a = t.actor;
|
||||
return a.applyDamage(amount, multiplier);
|
||||
}));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
35
module/classFeatures.js
Normal file
35
module/classFeatures.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
export const ClassFeatures = {
|
||||
"berserker": {
|
||||
"archetypes": {
|
||||
"addicted-approach": {
|
||||
"label": "Addicted Approach",
|
||||
"source": "PHB",
|
||||
"features": {
|
||||
"3": ["Compendium.sw5e.archetypes.PCwepUZqHYlxr4T3", "Compendium.sw5e.classfeatures.efOA0nrvUqKJOOeP", "Compendium.sw5e.classfeatures.nT6AfpQXSZ4IeChO"],
|
||||
"6": ["Compendium.sw5e.classfeatures.GbJDWzoTKWL7sEpR"],
|
||||
"10": ["Compendium.sw5e.classfeatures.3jqPPd5qJBBnonPw"],
|
||||
"14": ["Compendium.sw5e.classfeatures.xzRNHB2M2HdOZzr7"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"1": ["Compendium.sw5e.classfeatures.IDt6duVrBzL8euRc", "Compendium.sw5e.classfeatures.rPOLy96fW96N2UPg"],
|
||||
"2": ["Compendium.sw5e.classfeatures.DlYiCiG39R0goG9u", "Compendium.sw5e.classfeatures.FbSpxpXm1xONn0na", "Compendium.sw5e.classfeatures.KDiQ8O2evV2Z1YTo", "Compendium.sw5e.classfeatures.Q1JyHnVs9iIEBs91", "Compendium.sw5e.classfeatures.ROdICoWR82v6A2Rf", "Compendium.sw5e.classfeatures.cdCx5Hvq2rYRMzRj", "Compendium.sw5e.classfeatures.dTdbL8dypa6BAdnP", "Compendium.sw5e.classfeatures.h1uDhP1tEOuvjRw6", "Compendium.sw5e.classfeatures.hMiA075EKBBOL2cv", "Compendium.sw5e.classfeatures.sgJdISZMtwv08WPJ", "Compendium.sw5e.classfeatures.v4CZJ8LBMl5PYZCO"],
|
||||
"3": ["Compendium.sw5e.classfeatures.kzwSN9SabKgWZZvU"],
|
||||
"4": ["Compendium.sw5e.classfeatures.9oyy0MMqEws2qoil"],
|
||||
"5": ["Compendium.sw5e.classfeatures.dPWmHiWmpnhHTsgd"],
|
||||
"7": ["Compendium.sw5e.classfeatures.Cid5ujSdnooH0vMm", "Compendium.sw5e.classfeatures.WTBhKJgkArQI3Tgv", "Compendium.sw5e.classfeatures.oiT3TJxzRWPKAX9E", "Compendium.sw5e.classfeatures.pMEmIt3NWThbee8k", "Compendium.sw5e.classfeatures.qWV5YogZcpZ3Y3xj"],
|
||||
"9": ["Compendium.sw5e.classfeatures.bi8G8H5Ur9B3BAyM"],
|
||||
"11": ["Compendium.sw5e.classfeatures.eWbTifdXJvvXT4CV"],
|
||||
"13": ["Compendium.sw5e.classfeatures.Hg8zYh1iXL0DGUVq", "Compendium.sw5e.classfeatures.QRnYiJmRk18ekE9v", "Compendium.sw5e.classfeatures.sfEr8ZBFVddlfLeF", "Compendium.sw5e.classfeatures.yGC9VzT840qQWxca"],
|
||||
"15": ["Compendium.sw5e.classfeatures.YHPUv9lN3nCapAgP"],
|
||||
"18": ["Compendium.sw5e.classfeatures.fFKNqUAWh0ZOhvRc"],
|
||||
"20": ["Compendium.sw5e.classfeatures.IWTDawTUf79eWbEV"]
|
||||
}
|
||||
},
|
||||
"consular": {
|
||||
"features": {
|
||||
"20": ["Compendium.sw5e.classfeatures.gSGeitc98ItAwhfF"]
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
/**
|
||||
* Override the default Initiative formula to customize special behaviors of the D&D5e system.
|
||||
* 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.
|
||||
|
@ -9,8 +9,59 @@ export const _getInitiativeFormula = function(combatant) {
|
|||
const actor = combatant.actor;
|
||||
if ( !actor ) return "1d20";
|
||||
const init = actor.data.data.attributes.init;
|
||||
const parts = ["1d20", init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
|
||||
if ( actor.getFlag("sw5e", "initiativeAdv") ) parts[0] = "2d20kh";
|
||||
if ( CONFIG.Combat.initiative.tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
|
||||
|
||||
let nd = 1;
|
||||
let mods = "";
|
||||
|
||||
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r=1";
|
||||
if (actor.getFlag("sw5e", "initiativeAdv")) {
|
||||
nd = 2;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
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(" + ");
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* TODO: A temporary shim until 0.7.x becomes stable
|
||||
* @override
|
||||
*/
|
||||
TokenConfig.getTrackedAttributes = function(data, _path=[]) {
|
||||
|
||||
// Track the path and record found attributes
|
||||
const attributes = {
|
||||
"bar": [],
|
||||
"value": []
|
||||
};
|
||||
|
||||
// Recursively explore the object
|
||||
for ( let [k, v] of Object.entries(data) ) {
|
||||
let p = _path.concat([k]);
|
||||
|
||||
// Check objects for both a "value" and a "max"
|
||||
if ( v instanceof Object ) {
|
||||
const isBar = ("value" in v) && ("max" in v);
|
||||
if ( isBar ) attributes.bar.push(p);
|
||||
else {
|
||||
const inner = this.getTrackedAttributes(data[k], p);
|
||||
attributes.bar.push(...inner.bar);
|
||||
attributes.value.push(...inner.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise identify values which are numeric or null
|
||||
else if ( Number.isNumeric(v) || (v === null) ) {
|
||||
attributes.value.push(p);
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
};
|
|
@ -1,839 +0,0 @@
|
|||
// Namespace D&D5e Configuration Values
|
||||
export const SW5E = {};
|
||||
|
||||
// ASCII Artwork
|
||||
SW5E.ASCII = `_______________________________
|
||||
_
|
||||
| |
|
||||
___| |_ __ _ _ ____ ____ _ _ __ ___
|
||||
/ __| __/ _\ | |__\ \ /\ / / _\ | |__/ __|
|
||||
\__ \ || (_) | | \ V V / (_) | | \__ \
|
||||
|___/\__\__/_|_| \_/\_/ \__/_|_| |___/
|
||||
_______________________________`;
|
||||
|
||||
|
||||
/**
|
||||
* The set of Ability Scores used within the system
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.abilities = {
|
||||
"str": "SW5E.AbilityStr",
|
||||
"dex": "SW5E.AbilityDex",
|
||||
"con": "SW5E.AbilityCon",
|
||||
"int": "SW5E.AbilityInt",
|
||||
"wis": "SW5E.AbilityWis",
|
||||
"cha": "SW5E.AbilityCha"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Character alignment options
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.alignments = {
|
||||
'll': "SW5E.AlignmentLL",
|
||||
'nl': "SW5E.AlignmentNL",
|
||||
'cl': "SW5E.AlignmentCL",
|
||||
'lb': "SW5E.AlignmentLB",
|
||||
'bn': "SW5E.AlignmentBN",
|
||||
'cb': "SW5E.AlignmentCB",
|
||||
'ld': "SW5E.AlignmentLD",
|
||||
'nd': "SW5E.AlignmentND",
|
||||
'cd': "SW5E.AlignmentCD"
|
||||
};
|
||||
|
||||
|
||||
SW5E.weaponProficiencies = {
|
||||
"sim": "SW5E.WeaponSimpleProficiency",
|
||||
"bla": "SW5E.WeaponBlasterProficiency"
|
||||
};
|
||||
|
||||
SW5E.toolProficiencies = {
|
||||
"armor": "SW5E.ToolArmormech",
|
||||
"arms": "SW5E.ToolArmstech",
|
||||
"arti": "SW5E.ToolArtificer",
|
||||
"art": "SW5E.ToolArtist",
|
||||
"astro": "SW5E.ToolAstrotech",
|
||||
"bio": "SW5E.ToolBiotech",
|
||||
"con": "SW5E.ToolConstructor",
|
||||
"cyb": "SW5E.ToolCybertech",
|
||||
"jew": "SW5E.ToolJeweler",
|
||||
"sur": "SW5E.ToolSurveyor",
|
||||
"syn": "SW5E.ToolSynthweaver",
|
||||
"tin": "SW5E.ToolTinker",
|
||||
"ant": "SW5E.ToolAntitoxkit",
|
||||
"arc": "SW5E.ToolArchaeologistKit",
|
||||
"aud": "SW5E.ToolAudiotechKit",
|
||||
"bioa": "SW5E.ToolBioanalysisKit",
|
||||
"brew": "SW5E.ToolBrewerKit",
|
||||
"chef": "SW5E.ToolChefKit",
|
||||
"demo": "SW5E.ToolDemolitionKit",
|
||||
"disg": "SW5E.ToolDisguiseKit",
|
||||
"forg": "SW5E.ToolForgeryKit",
|
||||
"mech": "SW5E.ToolMechanicKit",
|
||||
"game": "SW5E.ToolGamingSet",
|
||||
"poi": "SW5E.ToolPoisonKit",
|
||||
"scav": "SW5E.ToolScavengingKit",
|
||||
"secur": "SW5E.ToolSecurityKit",
|
||||
"slic": "SW5E.ToolSlicerKit",
|
||||
"spice": "SW5E.ToolSpiceKit",
|
||||
"music": "SW5E.ToolMusicalInstrument",
|
||||
"vehicle": "SW5E.ToolVehicle"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This Object defines the various lengths of time which can occur in D&D5e
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.timePeriods = {
|
||||
"inst": "SW5E.TimeInst",
|
||||
"turn": "SW5E.TimeTurn",
|
||||
"round": "SW5E.TimeRound",
|
||||
"minute": "SW5E.TimeMinute",
|
||||
"hour": "SW5E.TimeHour",
|
||||
"day": "SW5E.TimeDay",
|
||||
"month": "SW5E.TimeMonth",
|
||||
"year": "SW5E.TimeYear",
|
||||
"perm": "SW5E.TimePerm",
|
||||
"spec": "SW5E.Special"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This describes the ways that an ability can be activated
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.abilityActivationTypes = {
|
||||
"none": "SW5E.None",
|
||||
"action": "SW5E.Action",
|
||||
"bonus": "SW5E.BonusAction",
|
||||
"reaction": "SW5E.Reaction",
|
||||
"minute": SW5E.timePeriods.minute,
|
||||
"hour": SW5E.timePeriods.hour,
|
||||
"day": SW5E.timePeriods.day,
|
||||
"special": SW5E.timePeriods.spec,
|
||||
"legendary": "SW5E.LegAct",
|
||||
"lair": "SW5E.LairAct"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Creature Sizes
|
||||
SW5E.actorSizes = {
|
||||
"tiny": "SW5E.SizeTiny",
|
||||
"sm": "SW5E.SizeSmall",
|
||||
"med": "SW5E.SizeMedium",
|
||||
"lg": "SW5E.SizeLarge",
|
||||
"huge": "SW5E.SizeHuge",
|
||||
"grg": "SW5E.SizeGargantuan"
|
||||
};
|
||||
|
||||
SW5E.tokenSizes = {
|
||||
"tiny": 1,
|
||||
"sm": 1,
|
||||
"med": 1,
|
||||
"lg": 2,
|
||||
"huge": 3,
|
||||
"grg": 4
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Classification types for item action types
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.itemActionTypes = {
|
||||
"mwak": "SW5E.ActionMWAK",
|
||||
"rwak": "SW5E.ActionRWAK",
|
||||
"mpak": "SW5E.ActionMPAK",
|
||||
"rpak": "SW5E.ActionRPAK",
|
||||
"save": "SW5E.ActionSave",
|
||||
"heal": "SW5E.ActionHeal",
|
||||
"abil": "SW5E.ActionAbil",
|
||||
"util": "SW5E.ActionUtil",
|
||||
"other": "SW5E.ActionOther"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
SW5E.itemCapacityTypes = {
|
||||
"items": "SW5E.ItemContainerCapacityItems",
|
||||
"weight": "SW5E.ItemContainerCapacityWeight"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Enumerate the lengths of time over which an item can have limited use ability
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.limitedUsePeriods = {
|
||||
"sr": "SW5E.ShortRest",
|
||||
"lr": "SW5E.LongRest",
|
||||
"day": "SW5E.Day",
|
||||
"charges": "SW5E.Charges"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The set of equipment types for armor, clothing, and other objects which can ber worn by the character
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.equipmentTypes = {
|
||||
"light": "SW5E.EquipmentLight",
|
||||
"medium": "SW5E.EquipmentMedium",
|
||||
"heavy": "SW5E.EquipmentHeavy",
|
||||
"bonus": "SW5E.EquipmentBonus",
|
||||
"natural": "SW5E.EquipmentNatural",
|
||||
"shield": "SW5E.EquipmentShield",
|
||||
"clothing": "SW5E.EquipmentClothing",
|
||||
"trinket": "SW5E.EquipmentTrinket"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The set of Armor Proficiencies which a character may have
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.armorProficiencies = {
|
||||
"lgt": SW5E.equipmentTypes.light,
|
||||
"med": SW5E.equipmentTypes.medium,
|
||||
"hvy": SW5E.equipmentTypes.heavy,
|
||||
"shl": "SW5E.EquipmentShieldProficiency"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Enumerate the valid consumable types which are recognized by the system
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.consumableTypes = {
|
||||
"adrenal": "SW5E.ConsumableAdrenal",
|
||||
"poison": "SW5E.ConsumablePoison",
|
||||
"explosive": "SW5E.ConsumableExplosive",
|
||||
"food": "SW5E.ConsumableFood",
|
||||
"medpac": "SW5E.ConsumableMedpac",
|
||||
"technology": "SW5E.ConsumableTechnology",
|
||||
"ammunition": "SW5E.ConsumableAmmunition",
|
||||
"trinket": "SW5E.ConsumableTrinket"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The valid currency denominations supported by the 5e system
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.currencies = {
|
||||
"CR": "SW5E.CurrencyCR",
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
// Damage Types
|
||||
SW5E.damageTypes = {
|
||||
"acid": "SW5E.DamageAcid",
|
||||
"cold": "SW5E.DamageCold",
|
||||
"energy": "SW5E.DamageEnergy",
|
||||
"fire": "SW5E.DamageFire",
|
||||
"force": "SW5E.DamageForce",
|
||||
"ion": "SW5E.DamageIon",
|
||||
"kinetic": "SW5E.DamageKinetic",
|
||||
"lightning": "SW5E.DamageLightning",
|
||||
"necrotic": "SW5E.DamageNecrotic",
|
||||
"poison": "SW5E.DamagePoison",
|
||||
"psychic": "SW5E.DamagePsychic",
|
||||
"Sonic": "SW5E.DamageSonic"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// armor Types
|
||||
SW5E.armorpropertiesTypes = {
|
||||
"Absorptive": "SW5E.ArmorProperAbsorptive",
|
||||
"Agile": "SW5E.ArmorProperAgile",
|
||||
"Anchor": "SW5E.ArmorProperAnchor",
|
||||
"Avoidant": "SW5E.ArmorProperAvoidant",
|
||||
"Barbed": "SW5E.ArmorProperBarbed",
|
||||
"Charging": "SW5E.ArmorProperCharging",
|
||||
"Concealing": "SW5E.ArmorProperConcealing",
|
||||
"Cumbersome": "SW5E.ArmorProperCumbersome",
|
||||
"Gauntleted": "SW5E.ArmorProperGauntleted",
|
||||
"Imbalanced": "SW5E.ArmorProperImbalanced",
|
||||
"Impermeable": "SW5E.ArmorProperImpermeable",
|
||||
"Insulated": "SW5E.ArmorProperInsulated",
|
||||
"Interlocking": "SW5E.ArmorProperInterlocking",
|
||||
"Lambent": "SW5E.ArmorProperLambent",
|
||||
"Lightweight": "SW5E.ArmorProperLightweight",
|
||||
"Magnetic": "SW5E.ArmorProperMagnetic",
|
||||
"Obscured": "SW5E.ArmorProperObscured",
|
||||
"Powered": "SW5E.ArmorProperPowered",
|
||||
"Reactive": "SW5E.ArmorProperReactive",
|
||||
"Regulated": "SW5E.ArmorProperRegulated",
|
||||
"Reinforced": "SW5E.ArmorProperReinforced",
|
||||
"Responsive": "SW5E.ArmorProperResponsive",
|
||||
"Rigid": "SW5E.ArmorProperRigid",
|
||||
"Silent": "SW5E.ArmorProperSilent",
|
||||
"Spiked": "SW5E.ArmorProperSpiked",
|
||||
"Steadfast": "SW5E.ArmorProperSteadfast",
|
||||
"Versatile": "SW5E.ArmorProperVersatile"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
SW5E.distanceUnits = {
|
||||
"none": "SW5E.None",
|
||||
"self": "SW5E.DistSelf",
|
||||
"touch": "SW5E.DistTouch",
|
||||
"ft": "SW5E.DistFt",
|
||||
"mi": "SW5E.DistMi",
|
||||
"spec": "SW5E.Special",
|
||||
"any": "SW5E.DistAny"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Configure aspects of encumbrance calculation so that it could be configured by modules
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.encumbrance = {
|
||||
currencyPerWeight: 50,
|
||||
strMultiplier: 15
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This Object defines the types of single or area targets which can be applied in D&D5e
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.targetTypes = {
|
||||
"none": "SW5E.None",
|
||||
"self": "SW5E.TargetSelf",
|
||||
"creature": "SW5E.TargetCreature",
|
||||
"droid": "SW5E.TargetDroid",
|
||||
"ally": "SW5E.TargetAlly",
|
||||
"enemy": "SW5E.TargetEnemy",
|
||||
"object": "SW5E.TargetObject",
|
||||
"space": "SW5E.TargetSpace",
|
||||
"radius": "SW5E.TargetRadius",
|
||||
"sphere": "SW5E.TargetSphere",
|
||||
"cylinder": "SW5E.TargetCylinder",
|
||||
"cone": "SW5E.TargetCone",
|
||||
"square": "SW5E.TargetSquare",
|
||||
"cube": "SW5E.TargetCube",
|
||||
"line": "SW5E.TargetLine",
|
||||
"wall": "SW5E.TargetWall",
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Map the subset of target types which produce a template area of effect
|
||||
* The keys are SW5E target types and the values are MeasuredTemplate shape types
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.areaTargetTypes = {
|
||||
cone: "cone",
|
||||
cube: "rect",
|
||||
cylinder: "circle",
|
||||
line: "ray",
|
||||
radius: "circle",
|
||||
sphere: "circle",
|
||||
square: "rect",
|
||||
wall: "ray"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Healing Types
|
||||
SW5E.healingTypes = {
|
||||
"healing": "SW5E.Healing",
|
||||
"temphp": "SW5E.HealingTemp"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Enumerate the denominations of hit dice which can apply to classes in the D&D5E system
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
SW5E.hitDieTypes = ["d6", "d8", "d10", "d12"];
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Character senses options
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.senses = {
|
||||
"bs": "SW5E.SenseBS",
|
||||
"dv": "SW5E.SenseDV",
|
||||
"ts": "SW5E.SenseTS",
|
||||
"tr": "SW5E.SenseTR"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The set of skill which can be trained in D&D5e
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.skills = {
|
||||
"acr": "SW5E.SkillAcr",
|
||||
"ani": "SW5E.SkillAni",
|
||||
"ath": "SW5E.SkillAth",
|
||||
"dec": "SW5E.SkillDec",
|
||||
"ins": "SW5E.SkillIns",
|
||||
"itm": "SW5E.SkillItm",
|
||||
"inv": "SW5E.SkillInv",
|
||||
"lor": "SW5E.SkillLor",
|
||||
"med": "SW5E.SkillMed",
|
||||
"nat": "SW5E.SkillNat",
|
||||
"prc": "SW5E.SkillPrc",
|
||||
"prf": "SW5E.SkillPrf",
|
||||
"per": "SW5E.SkillPer",
|
||||
"pil": "SW5E.SkillPil",
|
||||
"slt": "SW5E.SkillSlt",
|
||||
"ste": "SW5E.SkillSte",
|
||||
"sur": "SW5E.SkillSur",
|
||||
"tec": "SW5E.SkillTec",
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
SW5E.powerPreparationModes = {
|
||||
"always": "SW5E.PowerPrepAlways",
|
||||
"atwill": "SW5E.PowerPrepAtWill",
|
||||
"innate": "SW5E.PowerPrepInnate",
|
||||
"prepared": "SW5E.PowerPrepPrepared"
|
||||
};
|
||||
|
||||
SW5E.powerUpcastModes = ["always"];
|
||||
|
||||
|
||||
SW5E.powerProgression = {
|
||||
"none": "SW5E.PowerNone",
|
||||
"full": "SW5E.PowerProgFull",
|
||||
"artificer": "SW5E.PowerProgArt"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The available choices for how power damage scaling may be computed
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.powerScalingModes = {
|
||||
"none": "SW5E.PowerNone",
|
||||
"atwill": "SW5E.PowerAtWill",
|
||||
"level": "SW5E.PowerLevel"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Weapon Types
|
||||
SW5E.weaponTypes = {
|
||||
"simpleVW": "SW5E.WeaponSimpleVW",
|
||||
"simpleB": "SW5E.WeaponSimpleB",
|
||||
"simpleLW": "SW5E.WeaponSimpleLW",
|
||||
"martialVW": "SW5E.WeaponMartialVW",
|
||||
"martialB": "SW5E.WeaponMartialB",
|
||||
"martialLW": "SW5E.WeaponMartialLW",
|
||||
"natural": "SW5E.WeaponNatural",
|
||||
"improv": "SW5E.WeaponImprov",
|
||||
"ammo": "SW5E.WeaponAmmo"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Define the set of weapon property flags which can exist on a weapon
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.weaponProperties = {
|
||||
"amm": "SW5E.WeaponPropertiesAmm",
|
||||
"bur": "SW5E.WeaponPropertiesBur",
|
||||
"def": "SW5E.WeaponPropertiesDef",
|
||||
"dex": "SW5E.WeaponPropertiesDex",
|
||||
"drm": "SW5E.WeaponPropertiesDrm",
|
||||
"dgd": "SW5E.WeaponPropertiesDgd",
|
||||
"dis": "SW5E.WeaponPropertiesDis",
|
||||
"dpt": "SW5E.WeaponPropertiesDpt",
|
||||
"dou": "SW5E.WeaponPropertiesDou",
|
||||
"hvy": "SW5E.WeaponPropertiesHvy",
|
||||
"hid": "SW5E.WeaponPropertiesHid",
|
||||
"fin": "SW5E.WeaponPropertiesFin",
|
||||
"fix": "SW5E.WeaponPropertiesFix",
|
||||
"foc": "SW5E.WeaponPropertiesFoc",
|
||||
"ken": "SW5E.WeaponPropertiesKen",
|
||||
"lgt": "SW5E.WeaponPropertiesLgt",
|
||||
"lum": "SW5E.WeaponPropertiesLum",
|
||||
"pic": "SW5E.WeaponPropertiesPic",
|
||||
"rap": "SW5E.WeaponPropertiesRap",
|
||||
"rch": "SW5E.WeaponPropertiesRch",
|
||||
"rel": "SW5E.WeaponPropertiesRel",
|
||||
"ret": "SW5E.WeaponPropertiesRet",
|
||||
"shk": "SW5E.WeaponPropertiesShk",
|
||||
"spc": "SW5E.WeaponPropertiesSpc",
|
||||
"str": "SW5E.WeaponPropertiesStr",
|
||||
"thr": "SW5E.WeaponPropertiesThr",
|
||||
"two": "SW5E.WeaponPropertiesTwo",
|
||||
"ver": "SW5E.WeaponPropertiesVer",
|
||||
"vic": "SW5E.WeaponPropertiesVic"
|
||||
};
|
||||
|
||||
|
||||
// Power Components
|
||||
SW5E.powerComponents = {
|
||||
"V": "SW5E.ComponentVerbal",
|
||||
"S": "SW5E.ComponentSomatic",
|
||||
"M": "SW5E.ComponentMaterial"
|
||||
};
|
||||
|
||||
// Power Schools
|
||||
SW5E.powerSchools = {
|
||||
"lgt": "SW5E.SchoolLgt",
|
||||
"uni": "SW5E.SchoolUni",
|
||||
"drk": "SW5E.SchoolDrk",
|
||||
"tec": "SW5E.SchoolTec",
|
||||
"enh": "SW5E.SchoolEnh"
|
||||
};
|
||||
|
||||
|
||||
// Power Levels
|
||||
SW5E.powerLevels = {
|
||||
0: "SW5E.PowerLevel0",
|
||||
1: "SW5E.PowerLevel1",
|
||||
2: "SW5E.PowerLevel2",
|
||||
3: "SW5E.PowerLevel3",
|
||||
4: "SW5E.PowerLevel4",
|
||||
5: "SW5E.PowerLevel5",
|
||||
6: "SW5E.PowerLevel6",
|
||||
7: "SW5E.PowerLevel7",
|
||||
8: "SW5E.PowerLevel8",
|
||||
9: "SW5E.PowerLevel9"
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the standard slot progression by character level.
|
||||
* The entries of this array represent the power slot progression for a full power-caster.
|
||||
* @type {Array[]}
|
||||
*/
|
||||
SW5E.SPELL_SLOT_TABLE = [
|
||||
[2],
|
||||
[3],
|
||||
[4, 2],
|
||||
[4, 3],
|
||||
[4, 3, 2],
|
||||
[4, 3, 3],
|
||||
[4, 3, 3, 1],
|
||||
[4, 3, 3, 2],
|
||||
[4, 3, 3, 3, 1],
|
||||
[4, 3, 3, 3, 2],
|
||||
[4, 3, 3, 3, 2, 1],
|
||||
[4, 3, 3, 3, 2, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 1, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 2, 2, 1, 1]
|
||||
];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Polymorph options.
|
||||
SW5E.polymorphSettings = {
|
||||
keepPhysical: 'SW5E.PolymorphKeepPhysical',
|
||||
keepMental: 'SW5E.PolymorphKeepMental',
|
||||
keepSaves: 'SW5E.PolymorphKeepSaves',
|
||||
keepSkills: 'SW5E.PolymorphKeepSkills',
|
||||
mergeSaves: 'SW5E.PolymorphMergeSaves',
|
||||
mergeSkills: 'SW5E.PolymorphMergeSkills',
|
||||
keepClass: 'SW5E.PolymorphKeepClass',
|
||||
keepFeats: 'SW5E.PolymorphKeepFeats',
|
||||
keepPowers: 'SW5E.PolymorphKeepPowers',
|
||||
keepItems: 'SW5E.PolymorphKeepItems',
|
||||
keepBio: 'SW5E.PolymorphKeepBio',
|
||||
keepVision: 'SW5E.PolymorphKeepVision'
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Skill, ability, and tool proficiency levels
|
||||
* Each level provides a proficiency multiplier
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.proficiencyLevels = {
|
||||
0: "SW5E.NotProficient",
|
||||
1: "SW5E.Proficient",
|
||||
0.5: "SW5E.HalfProficient",
|
||||
2: "SW5E.Expertise"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
// Condition Types
|
||||
SW5E.conditionTypes = {
|
||||
"blinded": "SW5E.ConBlinded",
|
||||
"charmed": "SW5E.ConCharmed",
|
||||
"deafened": "SW5E.ConDeafened",
|
||||
"exhaustion": "SW5E.ConExhaustion",
|
||||
"frightened": "SW5E.ConFrightened",
|
||||
"grappled": "SW5E.ConGrappled",
|
||||
"incapacitated": "SW5E.ConIncapacitated",
|
||||
"invisible": "SW5E.ConInvisible",
|
||||
"paralyzed": "SW5E.ConParalyzed",
|
||||
"petrified": "SW5E.ConPetrified",
|
||||
"poisoned": "SW5E.ConPoisoned",
|
||||
"prone": "SW5E.ConProne",
|
||||
"restrained": "SW5E.ConRestrained",
|
||||
"shocked": "SW5E.ConShocked",
|
||||
"stunned": "SW5E.ConStunned",
|
||||
"unconscious": "SW5E.ConUnconscious"
|
||||
};
|
||||
|
||||
// Languages
|
||||
SW5E.languages = {
|
||||
"aleena": "SW5E.LanguagesAleena",
|
||||
"antarian": "SW5E.LanguagesAntarian",
|
||||
"anzellan": "SW5E.LanguagesAnzellan",
|
||||
"aqualish": "SW5E.LanguagesAqualish",
|
||||
"ardennian": "SW5E.LanguagesArdennian",
|
||||
"balosur": "SW5E.LanguagesBalosur",
|
||||
"barabel": "SW5E.LanguagesBarabel",
|
||||
"basic": "SW5E.LanguagesBasic",
|
||||
"besalisk": "SW5E.LanguagesBesalisk",
|
||||
"binary": "SW5E.LanguagesBinary",
|
||||
"bith": "SW5E.LanguagesBith",
|
||||
"bocce": "SW5E.LanguagesBocce",
|
||||
"bothese": "SW5E.LanguagesBothese",
|
||||
"catharese": "SW5E.LanguagesCartharese",
|
||||
"cerean": "SW5E.LanguagesCerean",
|
||||
"chadra-fan": "SW5E.LanguagesChadra-Fan",
|
||||
"chagri": "SW5E.LanguagesChagri",
|
||||
"cheunh": "SW5E.LanguagesCheunh",
|
||||
"chevin": "SW5E.LanguagesChevin",
|
||||
"chironan": "SW5E.LanguagesChironan",
|
||||
"clawdite": "SW5E.LanguagesClawdite",
|
||||
"codruese": "SW5E.LanguagesCodruese",
|
||||
"colicoid": "SW5E.LanguagesColicoid",
|
||||
"dashadi": "SW5E.LanguagesDashadi",
|
||||
"defel": "SW5E.LanguagesDefel",
|
||||
"devaronese": "SW5E.LanguagesDevaronese",
|
||||
"dosh": "SW5E.LanguagesDosh",
|
||||
"draethos": "SW5E.LanguagesDraethos",
|
||||
"durese": "SW5E.LanguagesDurese",
|
||||
"dug": "SW5E.LanguagesDug",
|
||||
"ewokese": "SW5E.LanguagesEwokese",
|
||||
"falleen": "SW5E.LanguagesFalleen",
|
||||
"felucianese": "SW5E.LanguagesFelucianese",
|
||||
"gamorrese": "SW5E.LanguagesGamorrese",
|
||||
"gand": "SW5E.LanguagesGand",
|
||||
"geonosian": "SW5E.LanguagesGeonosian",
|
||||
"givin": "SW5E.LanguagesGivin",
|
||||
"gran": "SW5E.LanguagesGran",
|
||||
"gungan": "SW5E.LanguagesGungan",
|
||||
"hapan": "SW5E.LanguagesHapan",
|
||||
"harchese": "SW5E.LanguagesHarchese",
|
||||
"herglese": "SW5E.LanguagesHerglese",
|
||||
"honoghran": "SW5E.LanguagesHonoghran",
|
||||
"huttese": "SW5E.LanguagesHuttese",
|
||||
"iktotchese": "SW5E.LanguagesIktotchese",
|
||||
"ithorese": "SW5E.LanguagesIthorese",
|
||||
"jawaese": "SW5E.LanguagesJawaese",
|
||||
"kaleesh": "SW5E.LanguagesKaleesh",
|
||||
"kaminoan": "SW5E.LanguagesKaminoan",
|
||||
"karkaran": "SW5E.LanguagesKarkaran",
|
||||
"keldor": "SW5E.LanguagesKelDor",
|
||||
"kharan": "SW5E.LanguagesKharan",
|
||||
"killik": "SW5E.LanguagesKillik",
|
||||
"klatooinian": "SW5E.LanguagesKlatooinian",
|
||||
"kubazian": "SW5E.LanguagesKubazian",
|
||||
"kushiban": "SW5E.LanguagesKushiban",
|
||||
"kyuzo": "SW5E.LanguagesKyuzo",
|
||||
"lannik": "SW5E.LanguagesLannik",
|
||||
"lasat": "SW5E.LanguagesLasat",
|
||||
"lowickese": "SW5E.LanguagesLowickese",
|
||||
"mandoa": "SW5E.LanguagesMandoa",
|
||||
"miralukese": "SW5E.LanguagesMiralukese",
|
||||
"mirialan": "SW5E.LanguagesMirialan",
|
||||
"moncal": "SW5E.LanguagesMonCal",
|
||||
"mustafarian": "SW5E.LanguagesMustafarian",
|
||||
"muun": "SW5E.LanguagesMuun",
|
||||
"nautila": "SW5E.LanguagesNautila",
|
||||
"ortolan": "SW5E.LanguagesOrtolan",
|
||||
"pakpak": "SW5E.LanguagesPakPak",
|
||||
"pyke": "SW5E.LanguagesPyke",
|
||||
"quarrenese": "SW5E.LanguagesQuarrenese",
|
||||
"rakata": "SW5E.LanguagesRakata",
|
||||
"rattataki": "SW5E.LanguagesRattataki",
|
||||
"rishii": "SW5E.LanguagesRishii",
|
||||
"rodese": "SW5E.LanguagesRodese",
|
||||
"selkatha": "SW5E.LanguagesSelkatha",
|
||||
"semblan": "SW5E.LanguagesSemblan",
|
||||
"shistavanen": "SW5E.LanguagesShistavanen",
|
||||
"shyriiwook": "SW5E.LanguagesShyriiwook",
|
||||
"sith": "SW5E.LanguagesSith",
|
||||
"squibbian": "SW5E.LanguagesSquibbian",
|
||||
"sriluurian": "SW5E.LanguagesSriluurian",
|
||||
"ssi-ruuvi": "SW5E.LanguagesSsi-ruuvi",
|
||||
"sullustese": "SW5E.LanguagesSullustese",
|
||||
"talzzi": "SW5E.LanguagesTalzzi",
|
||||
"thisspiasian": "SW5E.LanguagesThisspiasian",
|
||||
"togorese": "SW5E.LanguagesTogorese",
|
||||
"togruti": "SW5E.LanguagesTogruti",
|
||||
"toydarian": "SW5E.LanguagesToydarian",
|
||||
|
||||
"tusken": "SW5E.LanguagesTusken",
|
||||
"twi'leki": "SW5E.LanguagesTwileki",
|
||||
"ugnaught": "SW5E.LanguagesUgnaught",
|
||||
"umbaran": "SW5E.LanguagesUmbaran",
|
||||
"utapese": "SW5E.LanguagesUtapese",
|
||||
"verpine": "SW5E.LanguagesVerpine",
|
||||
"vong": "SW5E.LanguagesVong",
|
||||
"voss": "SW5E.LanguagesVoss",
|
||||
"yevethan": "SW5E.LanguagesYevethan",
|
||||
"zabraki": "SW5E.LanguagesZabraki",
|
||||
"zygerrian": "SW5E.LanguagesZygerrian"
|
||||
};
|
||||
|
||||
// Character Level XP Requirements
|
||||
SW5E.CHARACTER_EXP_LEVELS = [
|
||||
0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000,
|
||||
120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000]
|
||||
;
|
||||
|
||||
// Challenge Rating XP Levels
|
||||
SW5E.CR_EXP_LEVELS = [
|
||||
10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000,
|
||||
20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000
|
||||
];
|
||||
|
||||
// Configure Optional Character Flags
|
||||
SW5E.characterFlags = {
|
||||
"detailOriented": {
|
||||
name: "Detail Oriented",
|
||||
hint: "You have advantage on Intelligence (Investigation) checks within 5 feet.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"keenSenses": {
|
||||
name: "Keen Hearing and Smell",
|
||||
hint: "You have advantage on Wisdom (Perception) checks that involve hearing or smell.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"naturallyStealthy": {
|
||||
name: "Naturally Stealthy",
|
||||
hint: "You can attempt to hide even when you are obscured only by a creature that is your size or larger than you.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"nimbleEscape": {
|
||||
name: "Nimble Escape",
|
||||
hint: "You can take the Disengage or Hide action as a bonus action.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"powerfulBuild": {
|
||||
name: "Powerful Build",
|
||||
hint: "You count as one size larger when determining your carrying capacity and the weight you can push, drag, or lift.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"programmer": {
|
||||
name: "Programmer",
|
||||
hint: "Whenever you make an Intelligence (Technology) check related to computers, you are considered to have expertise in the Technology skill.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"techResistance": {
|
||||
name: "Tech Resistance",
|
||||
hint: "You have advantage on Dexterity and Intelligence saving throws against tech powers.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"unarmedCombatant": {
|
||||
name: "Unarmed Combatant",
|
||||
hint: "Your unarmed strikes deal 1d4 kinetic damage. You can use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"undersized": {
|
||||
name: "Undersized",
|
||||
hint: "You can’t use heavy shields, martial weapons with the two-handed property unless it also has the light property, and if a martial weapon has the versatile property, you can only wield it in two hands.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"initiativeAdv": {
|
||||
name: "Advantage on Initiative",
|
||||
hint: "Provided by feats or magical items.",
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"initiativeAlert": {
|
||||
name: "Alert Feat",
|
||||
hint: "Provides +5 to Initiative.",
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"jackOfAllTrades": {
|
||||
name: "Jack of All Trades",
|
||||
hint: "Half-Proficiency to Ability Checks in which you are not already Proficient.",
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"observantFeat": {
|
||||
name: "Observant Feat",
|
||||
hint: "Provides a +5 to passive Perception and Investigation.",
|
||||
skills: ['prc','inv'],
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"remarkableAthlete": {
|
||||
name: "Remarkable Athlete.",
|
||||
hint: "Half-Proficiency (rounded-up) to physical Ability Checks and Initiative.",
|
||||
abilities: ['str','dex','con'],
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"weaponCriticalThreshold": {
|
||||
name: "Critical Hit Threshold",
|
||||
hint: "Allow for expanded critical range; for example Improved or Superior Critical",
|
||||
section: "Feats",
|
||||
type: Number,
|
||||
placeholder: 20
|
||||
}
|
||||
};
|
359
module/dice.js
359
module/dice.js
|
@ -1,125 +1,156 @@
|
|||
export class Dice5e {
|
||||
|
||||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
*
|
||||
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
|
||||
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
|
||||
*
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Event|object} event The triggering event which initiated the roll
|
||||
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {string|null} template The HTML template used to render the roll dialog
|
||||
* @param {string|null} title The dice roll UI window title
|
||||
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string|null} flavor Flavor text to use in the posted chat message
|
||||
* @param {Boolean} fastForward Allow fast-forward advantage selection
|
||||
* @param {Function} onClose Callback for actions to take when the dialog form is closed
|
||||
* @param {Object} dialogOptions Modal dialog options
|
||||
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
|
||||
* @param {boolean} disadvantage Apply disadvantage to the roll (unless otherwise specified)
|
||||
* @param {number} critical The value of d20 result which represents a critical success
|
||||
* @param {number} fumble The value of d20 result which represents a critical failure
|
||||
* @param {number} targetValue Assign a target value against which the result of this roll should be compared
|
||||
* @param {boolean} elvenAccuracy Allow Elven Accuracy to modify this roll?
|
||||
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Event|object} event The triggering event which initiated the roll
|
||||
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {string|null} template The HTML template used to render the roll dialog
|
||||
* @param {string|null} title The dice roll UI window title
|
||||
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string|null} flavor Flavor text to use in the posted chat message
|
||||
* @param {Boolean} fastForward Allow fast-forward advantage selection
|
||||
* @param {Function} onClose Callback for actions to take when the dialog form is closed
|
||||
* @param {Object} dialogOptions Modal dialog options
|
||||
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
|
||||
* @param {boolean} disadvantage Apply disadvantage to the roll (unless otherwise specified)
|
||||
* @param {number} critical The value of d20 result which represents a critical success
|
||||
* @param {number} fumble The value of d20 result which represents a critical failure
|
||||
* @param {number} targetValue Assign a target value against which the result of this roll should be compared
|
||||
* @param {boolean} elvenAccuracy Allow Elven Accuracy to modify this roll?
|
||||
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
|
||||
* @param {boolean} reliableTalent Allow Reliable Talent to modify this roll?
|
||||
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
|
||||
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
static async d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
|
||||
flavor=null, fastForward=null, onClose, dialogOptions,
|
||||
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
|
||||
elvenAccuracy=false, halflingLucky=false}={}) {
|
||||
|
||||
// Handle input arguments
|
||||
flavor = flavor || title;
|
||||
speaker = speaker || ChatMessage.getSpeaker();
|
||||
export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
|
||||
flavor=null, fastForward=null, dialogOptions,
|
||||
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
|
||||
elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
|
||||
chatMessage=true, messageData={}}={}) {
|
||||
|
||||
// Prepare Message Data
|
||||
messageData.flavor = flavor || title;
|
||||
messageData.speaker = speaker || ChatMessage.getSpeaker();
|
||||
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
|
||||
parts = parts.concat(["@bonus"]);
|
||||
rollMode = rollMode || game.settings.get("core", "rollMode");
|
||||
let rolled = false;
|
||||
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, adv, form=null) {
|
||||
|
||||
// Determine the d20 roll and modifiers
|
||||
|
||||
// Handle fast-forward events
|
||||
let adv = 0;
|
||||
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
if (fastForward) {
|
||||
if ( advantage || event.altKey ) adv = 1;
|
||||
else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1;
|
||||
}
|
||||
|
||||
// Define the inner roll function
|
||||
const _roll = (parts, adv, form) => {
|
||||
|
||||
// Determine the d20 roll and modifiers
|
||||
let nd = 1;
|
||||
let mods = halflingLucky ? "r=1" : "";
|
||||
|
||||
// Handle advantage
|
||||
if ( adv === 1 ) {
|
||||
if (adv === 1) {
|
||||
nd = elvenAccuracy ? 3 : 2;
|
||||
flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
||||
mods += "kh";
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
// Handle disadvantage
|
||||
else if ( adv === -1 ) {
|
||||
else if (adv === -1) {
|
||||
nd = 2;
|
||||
flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
||||
mods += "kl";
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
|
||||
mods += "kl";
|
||||
}
|
||||
|
||||
// Include the d20 roll
|
||||
parts.unshift(`${nd}d20${mods}`);
|
||||
// Prepend the d20 roll
|
||||
let formula = `${nd}d20${mods}`;
|
||||
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`;
|
||||
parts.unshift(formula);
|
||||
|
||||
// Optionally include a situational bonus
|
||||
if ( form !== null ) data['bonus'] = form.bonus.value;
|
||||
if ( !data["bonus"] ) parts.pop();
|
||||
|
||||
if ( form ) {
|
||||
data['bonus'] = form.bonus.value;
|
||||
messageOptions.rollMode = form.rollMode.value;
|
||||
}
|
||||
if (!data["bonus"]) parts.pop();
|
||||
|
||||
// Optionally include an ability score selection (used for tool checks)
|
||||
const ability = form ? form.ability : null;
|
||||
if ( ability && ability.value ) {
|
||||
if (ability && ability.value) {
|
||||
data.ability = ability.value;
|
||||
const abl = data.abilities[data.ability];
|
||||
if ( abl ) {
|
||||
data.mod = abl.mod;
|
||||
flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
|
||||
}
|
||||
if (abl) {
|
||||
data.mod = abl.mod;
|
||||
messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the roll and flag critical thresholds on the d20
|
||||
let roll = new Roll(parts.join(" + "), data).roll();
|
||||
const d20 = roll.parts[0];
|
||||
d20.options.critical = critical;
|
||||
d20.options.fumble = fumble;
|
||||
if ( targetValue ) d20.options.target = targetValue;
|
||||
|
||||
// Convert the roll to a chat message and return the roll
|
||||
rollMode = form ? form.rollMode.value : rollMode;
|
||||
roll.toMessage({
|
||||
speaker: speaker,
|
||||
flavor: flavor
|
||||
}, { rollMode });
|
||||
rolled = true;
|
||||
return roll;
|
||||
};
|
||||
|
||||
// Determine whether the roll can be fast-forward
|
||||
if ( fastForward === null ) {
|
||||
fastForward = event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);
|
||||
// Execute the roll
|
||||
let roll = new Roll(parts.join(" + "), data);
|
||||
try {
|
||||
roll.roll();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optionally allow fast-forwarding to specify advantage or disadvantage
|
||||
if ( fastForward ) {
|
||||
if ( advantage || event.altKey ) return _roll(parts, 1);
|
||||
else if ( disadvantage || event.ctrlKey || event.metaKey ) return _roll(parts, -1);
|
||||
else return _roll(parts, 0);
|
||||
// Flag d20 options for any 20-sided dice in the roll
|
||||
for (let d of roll.dice) {
|
||||
if (d.faces === 20) {
|
||||
d.options.critical = critical;
|
||||
d.options.fumble = fumble;
|
||||
if (targetValue) d.options.target = targetValue;
|
||||
}
|
||||
}
|
||||
|
||||
// If reliable talent was applied, add it to the flavor text
|
||||
if (reliableTalent && roll.dice[0].total < 10) {
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
|
||||
}
|
||||
return roll;
|
||||
};
|
||||
|
||||
// Create the Roll instance
|
||||
const roll = fastForward ? _roll(parts, adv) :
|
||||
await _d20RollDialog({template, title, parts, data, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll});
|
||||
|
||||
// Create a Chat Message
|
||||
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Present a Dialog form which creates a d20 roll once submitted
|
||||
* @return {Promise<Roll>}
|
||||
* @private
|
||||
*/
|
||||
async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.rollModes,
|
||||
rollModes: CONFIG.Dice.rollModes,
|
||||
config: CONFIG.SW5E
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
// Create the Dialog window
|
||||
let roll;
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
|
@ -127,26 +158,24 @@ export class Dice5e {
|
|||
buttons: {
|
||||
advantage: {
|
||||
label: game.i18n.localize("SW5E.Advantage"),
|
||||
callback: html => roll = _roll(parts, 1, html[0].children[0])
|
||||
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize("SW5E.Normal"),
|
||||
callback: html => roll = _roll(parts, 0, html[0].children[0])
|
||||
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
|
||||
},
|
||||
disadvantage: {
|
||||
label: game.i18n.localize("SW5E.Disadvantage"),
|
||||
callback: html => roll = _roll(parts, -1, html[0].children[0])
|
||||
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
|
||||
}
|
||||
},
|
||||
default: "normal",
|
||||
close: html => {
|
||||
if (onClose) onClose(html, parts, data);
|
||||
resolve(rolled ? roll : false)
|
||||
}
|
||||
close: () => resolve(null)
|
||||
}, dialogOptions).render(true);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
@ -169,83 +198,103 @@ export class Dice5e {
|
|||
* @param {Boolean} fastForward Allow fast-forward advantage selection
|
||||
* @param {Function} onClose Callback for actions to take when the dialog form is closed
|
||||
* @param {Object} dialogOptions Modal dialog options
|
||||
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
|
||||
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
static async damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
|
||||
allowCritical=true, critical=false, fastForward=null, onClose, dialogOptions}) {
|
||||
|
||||
// Handle input arguments
|
||||
flavor = flavor || title;
|
||||
speaker = speaker || ChatMessage.getSpeaker();
|
||||
rollMode = game.settings.get("core", "rollMode");
|
||||
let rolled = false;
|
||||
export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
|
||||
allowCritical=true, critical=false, fastForward=null, dialogOptions, chatMessage=true, messageData={}}={}) {
|
||||
|
||||
// Prepare Message Data
|
||||
messageData.flavor = flavor || title;
|
||||
messageData.speaker = speaker || ChatMessage.getSpeaker();
|
||||
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
|
||||
parts = parts.concat(["@bonus"]);
|
||||
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, crit, form) {
|
||||
data['bonus'] = form ? form.bonus.value : 0;
|
||||
|
||||
// Optionally include a situational bonus
|
||||
if ( form ) {
|
||||
data['bonus'] = form.bonus.value;
|
||||
messageOptions.rollMode = form.rollMode.value;
|
||||
}
|
||||
if (!data["bonus"]) parts.pop();
|
||||
|
||||
// Create the damage roll
|
||||
let roll = new Roll(parts.join("+"), data);
|
||||
|
||||
// Modify the damage formula for critical hits
|
||||
if ( crit === true ) {
|
||||
let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0;
|
||||
let mult = 2;
|
||||
roll.alter(add, mult);
|
||||
flavor = `${flavor} (${game.i18n.localize("SW5E.Critical")})`;
|
||||
}
|
||||
|
||||
// Convert the roll to a chat message
|
||||
rollMode = form ? form.rollMode.value : rollMode;
|
||||
roll.toMessage({
|
||||
speaker: speaker,
|
||||
flavor: flavor
|
||||
}, { rollMode });
|
||||
rolled = true;
|
||||
return roll;
|
||||
};
|
||||
|
||||
// Determine whether the roll can be fast-forward
|
||||
if ( fastForward === null ) {
|
||||
fastForward = event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);
|
||||
// Modify the damage formula for critical hits
|
||||
if ( crit === true ) {
|
||||
let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0;
|
||||
let mult = 2;
|
||||
// TODO Backwards compatibility - REMOVE LATER
|
||||
if (isNewerVersion(game.data.version, "0.6.9")) roll.alter(mult, add);
|
||||
else roll.alter(add, mult);
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
|
||||
}
|
||||
|
||||
// Modify the roll and handle fast-forwarding
|
||||
if ( fastForward ) return _roll(parts, critical || event.altKey);
|
||||
else parts = parts.concat(["@bonus"]);
|
||||
// Execute the roll
|
||||
try {
|
||||
return roll.roll();
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.rollModes
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
// Create the Roll instance
|
||||
const roll = fastForward ? _roll(parts, critical || event.altKey) : await _damageRollDialog({
|
||||
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
|
||||
});
|
||||
|
||||
// Create a Chat Message
|
||||
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
|
||||
return roll;
|
||||
|
||||
// Create the Dialog window
|
||||
let roll;
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
critical: {
|
||||
condition: allowCritical,
|
||||
label: game.i18n.localize("SW5E.CriticalHit"),
|
||||
callback: html => roll = _roll(parts, true, html[0].children[0])
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
||||
callback: html => roll = _roll(parts, false, html[0].children[0])
|
||||
},
|
||||
},
|
||||
default: "normal",
|
||||
close: html => {
|
||||
if (onClose) onClose(html, parts, data);
|
||||
resolve(rolled ? roll : false);
|
||||
}
|
||||
}, dialogOptions).render(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Present a Dialog form which creates a damage roll once submitted
|
||||
* @return {Promise<Roll>}
|
||||
* @private
|
||||
*/
|
||||
async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) {
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.Dice.rollModes
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
// Create the Dialog window
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
critical: {
|
||||
condition: allowCritical,
|
||||
label: game.i18n.localize("SW5E.CriticalHit"),
|
||||
callback: html => resolve(roll(parts, true, html[0].querySelector("form")))
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
||||
callback: html => resolve(roll(parts, false, html[0].querySelector("form")))
|
||||
},
|
||||
},
|
||||
default: "normal",
|
||||
close: () => resolve(null)
|
||||
}, dialogOptions).render(true);
|
||||
});
|
||||
}
|
||||
|
|
60
module/macros.js
Normal file
60
module/macros.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
|
||||
/* -------------------------------------------- */
|
||||
/* Hotbar Macros */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a Macro from an Item drop.
|
||||
* Get an existing item macro if one exists, otherwise create a new one.
|
||||
* @param {Object} data The dropped data
|
||||
* @param {number} slot The hotbar slot to use
|
||||
* @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");
|
||||
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 ) {
|
||||
macro = await Macro.create({
|
||||
name: item.name,
|
||||
type: "script",
|
||||
img: item.img,
|
||||
command: command,
|
||||
flags: {"sw5e.itemMacro": true}
|
||||
});
|
||||
}
|
||||
game.user.assignHotbarMacro(macro, slot);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a Macro from an Item drop.
|
||||
* Get an existing item macro if one exists, otherwise create a new one.
|
||||
* @param {string} itemName
|
||||
* @return {Promise}
|
||||
*/
|
||||
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);
|
||||
|
||||
// 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 ) {
|
||||
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
|
||||
}
|
||||
const item = items[0];
|
||||
|
||||
// Trigger the item roll
|
||||
if ( item.data.type === "power" ) return actor.usePower(item);
|
||||
return item.roll();
|
||||
}
|
|
@ -136,6 +136,34 @@ export const migrateActorData = function(actor) {
|
|||
return updateData;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Scrub system flags
|
||||
const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => {
|
||||
obj[f] = null;
|
||||
return obj;
|
||||
}, {});
|
||||
if ( actorData.flags.sw5e ) {
|
||||
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
|
||||
}
|
||||
|
||||
// Return the scrubbed data
|
||||
return actorData;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
@ -222,3 +250,33 @@ const _migrateRemoveDeprecated = function(ent, updateData) {
|
|||
updateData[`data.${parts.join(".")}`] = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* A general tool to purge flags from all entities in a Compendium pack.
|
||||
* @param {Compendium} pack The compendium pack to clean
|
||||
* @private
|
||||
*/
|
||||
export async function purgeFlags(pack) {
|
||||
const cleanFlags = (flags) => {
|
||||
const flags5e = flags.sw5e || null;
|
||||
return flags5e ? {sw5e: flags5e} : {};
|
||||
};
|
||||
await pack.configure({locked: false});
|
||||
const content = await pack.getContent();
|
||||
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 => {
|
||||
i.flags = cleanFlags(i.flags);
|
||||
return i;
|
||||
})
|
||||
}
|
||||
await pack.updateEntity(update, {recursive: false});
|
||||
console.log(`Purged flags from ${entity.name}`);
|
||||
}
|
||||
await pack.configure({locked: true});
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { SW5E } from "../config.js";
|
|||
* A helper class for building MeasuredTemplates for 5e powers and abilities
|
||||
* @extends {MeasuredTemplate}
|
||||
*/
|
||||
export class AbilityTemplate extends MeasuredTemplate {
|
||||
export default class AbilityTemplate extends MeasuredTemplate {
|
||||
|
||||
/**
|
||||
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
|
||||
|
@ -37,8 +37,8 @@ export class AbilityTemplate extends MeasuredTemplate {
|
|||
templateData.width = target.value;
|
||||
templateData.direction = 45;
|
||||
break;
|
||||
case "ray": // 5e rays are most commonly 5ft wide
|
||||
templateData.width = 5;
|
||||
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
|
||||
templateData.width = target.width ?? canvas.dimensions.distance;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -52,9 +52,8 @@ export class AbilityTemplate extends MeasuredTemplate {
|
|||
|
||||
/**
|
||||
* Creates a preview of the power template
|
||||
* @param {Event} event The initiating click event
|
||||
*/
|
||||
drawPreview(event) {
|
||||
drawPreview() {
|
||||
const initialLayer = canvas.activeLayer;
|
||||
this.draw();
|
||||
this.layer.activate();
|
||||
|
|
|
@ -11,6 +11,23 @@ export const registerSystemSettings = function() {
|
|||
default: 0
|
||||
});
|
||||
|
||||
/**
|
||||
* Register resting variants
|
||||
*/
|
||||
game.settings.register("sw5e", "restVariant", {
|
||||
name: "SETTINGS.5eRestN",
|
||||
hint: "SETTINGS.5eRestL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: "normal",
|
||||
type: String,
|
||||
choices: {
|
||||
"normal": "SETTINGS.5eRestPHB",
|
||||
"gritty": "SETTINGS.5eRestGritty",
|
||||
"epic": "SETTINGS.5eRestEpic",
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Register diagonal movement rule setting
|
||||
*/
|
||||
|
@ -23,7 +40,8 @@ export const registerSystemSettings = function() {
|
|||
type: String,
|
||||
choices: {
|
||||
"555": "SETTINGS.5eDiagPHB",
|
||||
"5105": "SETTINGS.5eDiagDMG"
|
||||
"5105": "SETTINGS.5eDiagDMG",
|
||||
"EUCL": "SETTINGS.5eDiagEuclidean",
|
||||
},
|
||||
onChange: rule => canvas.grid.diagonalRule = rule
|
||||
});
|
||||
|
@ -31,21 +49,14 @@ export const registerSystemSettings = function() {
|
|||
/**
|
||||
* Register Initiative formula setting
|
||||
*/
|
||||
function _set5eInitiative(tiebreaker) {
|
||||
CONFIG.Combat.initiative.tiebreaker = tiebreaker;
|
||||
CONFIG.Combat.initiative.decimals = tiebreaker ? 2 : 0;
|
||||
if ( ui.combat && ui.combat._rendered ) ui.combat.render();
|
||||
}
|
||||
game.settings.register("sw5e", "initiativeDexTiebreaker", {
|
||||
name: "SETTINGS.5eInitTBN",
|
||||
hint: "SETTINGS.5eInitTBL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
onChange: enable => _set5eInitiative(enable)
|
||||
type: Boolean
|
||||
});
|
||||
_set5eInitiative(game.settings.get("sw5e", "initiativeDexTiebreaker"));
|
||||
|
||||
/**
|
||||
* Require Currency Carrying Weight
|
||||
|
|
|
@ -13,11 +13,13 @@ export const preloadHandlebarsTemplates = async function() {
|
|||
"systems/sw5e/templates/actors/parts/actor-inventory.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-features.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-powerbook.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-effects.html",
|
||||
|
||||
// Item Sheet Partials
|
||||
"systems/sw5e/templates/items/parts/item-action.html",
|
||||
"systems/sw5e/templates/items/parts/item-activation.html",
|
||||
"systems/sw5e/templates/items/parts/item-description.html"
|
||||
"systems/sw5e/templates/items/parts/item-description.html",
|
||||
"systems/sw5e/templates/items/parts/item-mountable.html"
|
||||
];
|
||||
|
||||
// Load the template parts
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue