forked from GitHub-Mirrors/foundry-sw5e
Update Core to 1.2
Update Core to 1.2, pulled from dev 12/10/2020
This commit is contained in:
parent
df456997eb
commit
e6bff40e1b
105 changed files with 4335 additions and 1274 deletions
833
module/actor/sheets/newSheet/base.js
Normal file
833
module/actor/sheets/newSheet/base.js
Normal file
|
@ -0,0 +1,833 @@
|
|||
import Item5e from "../../../item/entity.js";
|
||||
import TraitSelector from "../../../apps/trait-selector.js";
|
||||
import ActorSheetFlags from "../../../apps/actor-flags.js";
|
||||
import ActorMovementConfig from "../../../apps/movement-config.js";
|
||||
import ActorSensesConfig from "../../../apps/senses-config.js";
|
||||
import {SW5E} from '../../../config.js';
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
|
||||
|
||||
/**
|
||||
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
|
||||
* This sheet is an Abstract layer which is not used.
|
||||
* @extends {ActorSheet}
|
||||
*/
|
||||
export default class ActorSheet5e extends ActorSheet {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Track the set of item filters which are applied
|
||||
* @type {Set}
|
||||
*/
|
||||
this._filters = {
|
||||
inventory: new Set(),
|
||||
powerbook: new Set(),
|
||||
features: new Set(),
|
||||
effects: new Set()
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
scrollY: [
|
||||
".inventory .inventory-list",
|
||||
".features .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/newActor/expanded-limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
|
||||
// Basic data
|
||||
let isOwner = this.entity.owner;
|
||||
const data = {
|
||||
owner: isOwner,
|
||||
limited: this.entity.limited,
|
||||
options: this.options,
|
||||
editable: this.isEditable,
|
||||
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,
|
||||
};
|
||||
|
||||
// The Actor and its Items
|
||||
data.actor = duplicate(this.actor.data);
|
||||
data.items = this.actor.items.map(i => {
|
||||
i.data.labels = i.labels;
|
||||
return i.data;
|
||||
});
|
||||
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||||
data.data = data.actor.data;
|
||||
data.labels = this.actor.labels || {};
|
||||
data.filters = this._filters;
|
||||
|
||||
// Ability Scores
|
||||
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
|
||||
abl.icon = this._getProficiencyIcon(abl.proficient);
|
||||
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
|
||||
abl.label = CONFIG.SW5E.abilities[a];
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
|
||||
// Movement speeds
|
||||
data.movement = this._getMovementSpeed(data.actor);
|
||||
|
||||
// Senses
|
||||
data.senses = this._getSenses(data.actor);
|
||||
|
||||
// Update traits
|
||||
this._prepareTraits(data.actor.data.traits);
|
||||
|
||||
// Prepare owned items
|
||||
this._prepareItems(data);
|
||||
|
||||
// Prepare active effects
|
||||
data.effects = prepareActiveEffectCategories(this.entity.effects);
|
||||
|
||||
// Return data to the sheet
|
||||
return data
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare the display of movement speed data for the Actor
|
||||
* @param {object} actorData
|
||||
* @returns {{primary: string, special: string}}
|
||||
* @private
|
||||
*/
|
||||
_getMovementSpeed(actorData) {
|
||||
const movement = actorData.data.attributes.movement || {};
|
||||
const speeds = [
|
||||
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
|
||||
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
|
||||
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
|
||||
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
|
||||
].filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
|
||||
return {
|
||||
primary: `${movement.walk || 0} ${movement.units}`,
|
||||
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_getSenses(actorData) {
|
||||
const senses = actorData.data.attributes.senses || {};
|
||||
const tags = {};
|
||||
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
|
||||
const v = senses[k] ?? 0
|
||||
if ( v === 0 ) continue;
|
||||
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
|
||||
}
|
||||
if ( !!senses.special ) tags["special"] = senses.special;
|
||||
return tags;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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.damageResistanceTypes,
|
||||
"di": CONFIG.SW5E.damageResistanceTypes,
|
||||
"dv": CONFIG.SW5E.damageResistanceTypes,
|
||||
"ci": CONFIG.SW5E.conditionTypes,
|
||||
"languages": CONFIG.SW5E.languages,
|
||||
"armorProf": CONFIG.SW5E.armorProficiencies,
|
||||
"weaponProf": CONFIG.SW5E.weaponProficiencies,
|
||||
"toolProf": CONFIG.SW5E.toolProficiencies
|
||||
};
|
||||
for ( let [t, choices] of Object.entries(map) ) {
|
||||
const trait = traits[t];
|
||||
if ( !trait ) continue;
|
||||
let values = [];
|
||||
if ( trait.value ) {
|
||||
values = trait.value instanceof Array ? trait.value : [trait.value];
|
||||
}
|
||||
trait.selected = values.reduce((obj, t) => {
|
||||
obj[t] = choices[t];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
// Add custom entry
|
||||
if ( trait.custom ) {
|
||||
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
|
||||
}
|
||||
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Insert a power into the powerbook object when rendering the character sheet
|
||||
* @param {Object} data The Actor data being prepared
|
||||
* @param {Array} powers The power data being prepared
|
||||
* @private
|
||||
*/
|
||||
_preparePowerbook(data, powers) {
|
||||
const owner = this.actor.owner;
|
||||
const levels = data.data.powers;
|
||||
const powerbook = {};
|
||||
|
||||
// Define some mappings
|
||||
const sections = {
|
||||
"atwill": -20,
|
||||
"innate": -10,
|
||||
"pact": 0.5
|
||||
};
|
||||
|
||||
// Label power slot uses headers
|
||||
const useLabels = {
|
||||
"-20": "-",
|
||||
"-10": "-",
|
||||
"0": "∞"
|
||||
};
|
||||
|
||||
// Format a powerbook entry for a certain indexed level
|
||||
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
|
||||
powerbook[i] = {
|
||||
order: i,
|
||||
label: label,
|
||||
usesSlots: i > 0,
|
||||
canCreate: owner,
|
||||
canPrepare: (data.actor.type === "character") && (i >= 1),
|
||||
powers: [],
|
||||
uses: useLabels[i] || value || 0,
|
||||
slots: useLabels[i] || max || 0,
|
||||
override: override || 0,
|
||||
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode},
|
||||
prop: sl
|
||||
};
|
||||
};
|
||||
|
||||
// Determine the maximum power level which has a slot
|
||||
const maxLevel = Array.fromRange(10).reduce((max, i) => {
|
||||
if ( i === 0 ) return max;
|
||||
const level = levels[`power${i}`];
|
||||
if ( (level.max || level.override ) && ( i > max ) ) max = i;
|
||||
return max;
|
||||
}, 0);
|
||||
|
||||
// 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++) {
|
||||
const sl = `power${lvl}`;
|
||||
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pact magic users have cantrips and a pact magic section
|
||||
if ( levels.pact && levels.pact.max ) {
|
||||
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
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
|
||||
powers.forEach(power => {
|
||||
const mode = power.data.preparation.mode || "prepared";
|
||||
let s = power.data.level || 0;
|
||||
const sl = `power${s}`;
|
||||
|
||||
// Specialized powercasting modes (if they exist)
|
||||
if ( mode in sections ) {
|
||||
s = sections[mode];
|
||||
if ( !powerbook[s] ){
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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: levels[sl]});
|
||||
}
|
||||
|
||||
// Add the power to the relevant heading
|
||||
powerbook[s].powers.push(power);
|
||||
});
|
||||
|
||||
// Sort the powerbook by section level
|
||||
const sorted = Object.values(powerbook);
|
||||
sorted.sort((a, b) => a.order - b.order);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine whether an Owned Item will be shown based on the current set of filters
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
_filterItems(items, filters) {
|
||||
return items.filter(item => {
|
||||
const data = item.data;
|
||||
|
||||
// Action usage
|
||||
for ( let f of ["action", "bonus", "reaction"] ) {
|
||||
if ( filters.has(f) ) {
|
||||
if ((data.activation && (data.activation.type !== f))) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Power-specific filters
|
||||
if ( filters.has("ritual") ) {
|
||||
if (data.components.ritual !== true) return false;
|
||||
}
|
||||
if ( filters.has("concentration") ) {
|
||||
if (data.components.concentration !== true) return false;
|
||||
}
|
||||
if ( filters.has("prepared") ) {
|
||||
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
|
||||
if ( this.actor.data.type === "npc" ) return true;
|
||||
return data.preparation.prepared;
|
||||
}
|
||||
|
||||
// Equipment-specific filters
|
||||
if ( filters.has("equipped") ) {
|
||||
if ( data.equipped !== true ) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the font-awesome icon used to display a certain level of skill proficiency
|
||||
* @private
|
||||
*/
|
||||
_getProficiencyIcon(level) {
|
||||
const icons = {
|
||||
0: '<i class="far fa-circle"></i>',
|
||||
0.5: '<i class="fas fa-adjust"></i>',
|
||||
1: '<i class="fas fa-check"></i>',
|
||||
2: '<i class="fas fa-check-double"></i>'
|
||||
};
|
||||
return icons[level];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
|
||||
// Activate Item Filters
|
||||
const filterLists = html.find(".filter-list");
|
||||
filterLists.each(this._initializeFilterItemList.bind(this));
|
||||
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
|
||||
|
||||
// Item summaries
|
||||
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
|
||||
|
||||
// Editable Only Listeners
|
||||
if ( this.isEditable ) {
|
||||
|
||||
// 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));
|
||||
|
||||
// Toggle Skill Proficiency
|
||||
html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
|
||||
|
||||
// Trait Selector
|
||||
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
|
||||
|
||||
// Configure Special Flags
|
||||
html.find('.config-button').click(this._onConfigMenu.bind(this));
|
||||
|
||||
// Owned Item management
|
||||
html.find('.item-create').click(this._onItemCreate.bind(this));
|
||||
html.find('.item-edit').click(this._onItemEdit.bind(this));
|
||||
html.find('.item-delete').click(this._onItemDelete.bind(this));
|
||||
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
|
||||
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
|
||||
|
||||
// Active Effect management
|
||||
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
|
||||
}
|
||||
|
||||
// Owner Only Listeners
|
||||
if ( this.actor.owner ) {
|
||||
|
||||
// Ability Checks
|
||||
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
|
||||
|
||||
|
||||
// Roll Skill Checks
|
||||
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
|
||||
|
||||
// Item Rolling
|
||||
html.find('.item .item-image').click(event => this._onItemRoll(event));
|
||||
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
|
||||
}
|
||||
|
||||
// Otherwise remove rollable classes
|
||||
else {
|
||||
html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
|
||||
}
|
||||
|
||||
// Handle default listeners last so system listeners are triggered first
|
||||
super.activateListeners(html);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Iinitialize Item list filters by activating the set of filters which are currently applied
|
||||
* @private
|
||||
*/
|
||||
_initializeFilterItemList(i, ul) {
|
||||
const set = this._filters[ul.dataset.filter];
|
||||
const filters = ul.querySelectorAll(".filter-item");
|
||||
for ( let li of filters ) {
|
||||
if ( set.has(li.dataset.filter) ) li.classList.add("active");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
_onChangeInputDelta(event) {
|
||||
const input = event.target;
|
||||
const value = input.value;
|
||||
if ( ["+", "-"].includes(value[0]) ) {
|
||||
let delta = parseFloat(value);
|
||||
input.value = getProperty(this.actor.data, input.name) + delta;
|
||||
} else if ( value[0] === "=" ) {
|
||||
input.value = value.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onConfigMenu(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
switch ( button.dataset.action ) {
|
||||
case "movement":
|
||||
new ActorMovementConfig(this.object).render(true);
|
||||
break;
|
||||
case "flags":
|
||||
new ActorSheetFlags(this.object).render(true);
|
||||
break;
|
||||
case "senses":
|
||||
new ActorSensesConfig(this.object).render(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle cycling proficiency in a Skill
|
||||
* @param {Event} event A click or contextmenu event which triggered the handler
|
||||
* @private
|
||||
*/
|
||||
_onCycleSkillProficiency(event) {
|
||||
event.preventDefault();
|
||||
const field = $(event.currentTarget).siblings('input[type="hidden"]');
|
||||
|
||||
// Get the current level and the array of levels
|
||||
const level = parseFloat(field.val());
|
||||
const levels = [0, 1, 0.5, 2];
|
||||
let idx = levels.indexOf(level);
|
||||
|
||||
// Toggle next level - forward on click, backwards on right
|
||||
if ( event.type === "click" ) {
|
||||
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
|
||||
} else if ( event.type === "contextmenu" ) {
|
||||
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
|
||||
}
|
||||
|
||||
// Update the field value and save the form
|
||||
this._onSubmit(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
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;
|
||||
if (data.pack) {
|
||||
const pack = game.packs.find(p => p.collection === data.pack);
|
||||
sourceActor = await pack.getEntity(data.id);
|
||||
} else {
|
||||
sourceActor = game.actors.get(data.id);
|
||||
}
|
||||
if ( !sourceActor ) return;
|
||||
|
||||
// Define a function to record polymorph settings for future use
|
||||
const rememberOptions = html => {
|
||||
const options = {};
|
||||
html.find('input').each((i, el) => {
|
||||
options[el.name] = el.checked;
|
||||
});
|
||||
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
|
||||
game.settings.set('sw5e', 'polymorphSettings', settings);
|
||||
return settings;
|
||||
};
|
||||
|
||||
// Create and render the Dialog
|
||||
return new Dialog({
|
||||
title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
|
||||
content: {
|
||||
options: game.settings.get('sw5e', 'polymorphSettings'),
|
||||
i18n: SW5E.polymorphSettings,
|
||||
isToken: this.actor.isToken
|
||||
},
|
||||
default: 'accept',
|
||||
buttons: {
|
||||
accept: {
|
||||
icon: '<i class="fas fa-check"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
|
||||
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
|
||||
},
|
||||
wildshape: {
|
||||
icon: '<i class="fas fa-paw"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphWildShape'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
keepBio: true,
|
||||
keepClass: true,
|
||||
keepMental: true,
|
||||
mergeSaves: true,
|
||||
mergeSkills: true,
|
||||
transformTokens: rememberOptions(html).transformTokens
|
||||
})
|
||||
},
|
||||
polymorph: {
|
||||
icon: '<i class="fas fa-pastafarianism"></i>',
|
||||
label: game.i18n.localize('SW5E.Polymorph'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
transformTokens: rememberOptions(html).transformTokens
|
||||
})
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize('Cancel')
|
||||
}
|
||||
}
|
||||
}, {
|
||||
classes: ['dialog', 'sw5e'],
|
||||
width: 600,
|
||||
template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
|
||||
}).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
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle enabling editing for a power slot override value
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onPowerSlotOverride (event) {
|
||||
const span = event.currentTarget.parentElement;
|
||||
const level = span.dataset.level;
|
||||
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
|
||||
const input = document.createElement("INPUT");
|
||||
input.type = "text";
|
||||
input.name = `data.powers.${level}.override`;
|
||||
input.value = override;
|
||||
input.placeholder = span.dataset.slots;
|
||||
input.dataset.dtype = "Number";
|
||||
|
||||
// Replace the HTML
|
||||
const parent = span.parentElement;
|
||||
parent.removeChild(span);
|
||||
parent.appendChild(input);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Change the uses amount of an Owned Item within the Actor
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onUsesChange(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
|
||||
event.target.value = uses;
|
||||
return item.update({ 'data.uses.value': uses });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
|
||||
* @private
|
||||
*/
|
||||
_onItemRoll(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
return item.roll();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle attempting to recharge an item usage by rolling a recharge check
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onItemRecharge(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
return item.rollRecharge();
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
|
||||
* @private
|
||||
*/
|
||||
_onItemSummary(event) {
|
||||
event.preventDefault();
|
||||
let li = $(event.currentTarget).parents(".item"),
|
||||
item = this.actor.getOwnedItem(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.owner});
|
||||
|
||||
// Toggle summary
|
||||
if ( li.hasClass("expanded") ) {
|
||||
let summary = li.children(".item-summary");
|
||||
summary.slideUp(200, () => summary.remove());
|
||||
} else {
|
||||
let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
|
||||
let props = $(`<div class="item-properties"></div>`);
|
||||
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
|
||||
div.append(props);
|
||||
li.append(div.hide());
|
||||
div.slideDown(200);
|
||||
}
|
||||
li.toggleClass("expanded");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onItemCreate(event) {
|
||||
event.preventDefault();
|
||||
const header = event.currentTarget;
|
||||
const type = header.dataset.type;
|
||||
const itemData = {
|
||||
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
|
||||
type: type,
|
||||
data: duplicate(header.dataset)
|
||||
};
|
||||
delete itemData.data["type"];
|
||||
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle editing an existing Owned Item for the Actor
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onItemEdit(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget.closest(".item");
|
||||
const item = this.actor.getOwnedItem(li.dataset.itemId);
|
||||
item.sheet.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle deleting an existing Owned Item for the Actor
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget.closest(".item");
|
||||
this.actor.deleteOwnedItem(li.dataset.itemId);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling an Ability check, either a test or a saving throw
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onRollAbilityTest(event) {
|
||||
event.preventDefault();
|
||||
let ability = event.currentTarget.parentElement.dataset.ability;
|
||||
this.actor.rollAbility(ability, {event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling a Skill check
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onRollSkillCheck(event) {
|
||||
event.preventDefault();
|
||||
const skill = event.currentTarget.parentElement.dataset.skill;
|
||||
this.actor.rollSkill(skill, {event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle toggling Ability score proficiency level
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onToggleAbilityProficiency(event) {
|
||||
event.preventDefault();
|
||||
const field = event.currentTarget.previousElementSibling;
|
||||
this.actor.update({[field.name]: 1 - parseInt(field.value)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle toggling of filters to display a different set of owned items
|
||||
* @param {Event} event The click event which triggered the toggle
|
||||
* @private
|
||||
*/
|
||||
_onToggleFilter(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget;
|
||||
const set = this._filters[li.parentElement.dataset.filter];
|
||||
const filter = li.dataset.filter;
|
||||
if ( set.has(filter) ) set.delete(filter);
|
||||
else set.add(filter);
|
||||
this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onTraitSelector(event) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
const label = a.parentElement.querySelector("label");
|
||||
const choices = CONFIG.SW5E[a.dataset.options];
|
||||
const options = { name: a.dataset.target, title: label.innerText, choices };
|
||||
new TraitSelector(this.actor, options).render(true)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getHeaderButtons() {
|
||||
let buttons = super._getHeaderButtons();
|
||||
|
||||
// Add button to revert polymorph
|
||||
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
|
||||
buttons.unshift({
|
||||
label: 'SW5E.PolymorphRestoreTransformation',
|
||||
class: "restore-transformation",
|
||||
icon: "fas fa-backward",
|
||||
onclick: ev => this.actor.revertOriginalForm()
|
||||
});
|
||||
return buttons;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
|
@ -79,9 +79,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
||||
};
|
||||
|
||||
|
||||
// Partition items by category
|
||||
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
|
||||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
|
@ -104,10 +104,12 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
else if ( item.type === "archetype" ) arr[5].push(item);
|
||||
else if ( item.type === "classfeature" ) arr[6].push(item);
|
||||
else if ( item.type === "background" ) arr[7].push(item);
|
||||
else if ( item.type === "lightsaberform" ) arr[8].push(item);
|
||||
else if ( item.type === "fightingstyle" ) arr[8].push(item);
|
||||
else if ( item.type === "fightingmastery" ) arr[9].push(item);
|
||||
else if ( item.type === "lightsaberform" ) arr[10].push(item);
|
||||
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
|
||||
return arr;
|
||||
}, [[], [], [], [], [], [], [], [], []]);
|
||||
}, [[], [], [], [], [], [], [], [], [], [], []]);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
|
@ -131,11 +133,13 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
// Organize Features
|
||||
const features = {
|
||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
||||
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true },
|
||||
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
||||
lightsaberform: { label: "SW5E.ItemTypeLightsaberForm", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
|
||||
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
|
||||
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
||||
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
|
||||
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
|
||||
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
|
||||
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
||||
};
|
||||
|
@ -145,11 +149,13 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
}
|
||||
classes.sort((a, b) => b.levels - a.levels);
|
||||
features.classes.items = classes;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.lightsaberform.items = lightsaberforms;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.fightingstyles.items = fightingstyles;
|
||||
features.fightingmasteries.items = fightingmasteries;
|
||||
features.lightsaberforms.items = lightsaberforms;
|
||||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
|
@ -204,8 +210,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
||||
|
||||
// Death saving throws
|
||||
html.find('.death-save').click(this._onDeathSave.bind(this));
|
||||
// Rollable sheet actions
|
||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||
|
||||
// Send Languages to Chat onClick
|
||||
html.find('[data-options="share-languages"]').click(event => {
|
||||
|
@ -271,13 +277,19 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling a death saving throw for the Character
|
||||
* Handle mouse click events for character sheet actions
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onDeathSave(event) {
|
||||
_onSheetAction(event) {
|
||||
event.preventDefault();
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
const button = event.currentTarget;
|
||||
switch( button.dataset.action ) {
|
||||
case "rollDeathSave":
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
case "rollInitiative":
|
||||
return this.actor.rollInitiative({createCombatants: true});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -324,57 +336,26 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse click events to convert currency to the highest possible denomination
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onConvertCurrency(event) {
|
||||
event.preventDefault();
|
||||
return Dialog.confirm({
|
||||
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
|
||||
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
|
||||
yes: () => this.actor.convertCurrency()
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
// Upgrade the number of class levels a character has and add features
|
||||
// Increment the number of class levels a character instead of creating a new item
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
const classWasAlreadyPresent = !!cls;
|
||||
|
||||
// Add new features for class level
|
||||
if ( !classWasAlreadyPresent ) {
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
}
|
||||
|
||||
// If the actor already has the class, increment the level instead of creating a new item
|
||||
// then add new features as long as level increases
|
||||
if ( classWasAlreadyPresent ) {
|
||||
const lvl = cls.data.data.levels;
|
||||
const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
|
||||
if ( !(lvl === newLvl) ) {
|
||||
cls.update({"data.levels": newLvl});
|
||||
itemData.data.levels = newLvl;
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
if ( !!cls ) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
if ( next > priorLevel ) {
|
||||
itemData.levels = next;
|
||||
return cls.update({"data.levels": next});
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
|
||||
async function addFavorites(app, html, data) {
|
||||
// Thisfunction is adapted for the SwaltSheet from the Favorites Item
|
||||
// Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord).
|
||||
|
@ -516,7 +497,7 @@ async function addFavorites(app, html, data) {
|
|||
|
||||
if (app.options.editable) {
|
||||
favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev));
|
||||
let handler = ev => app._onDragItemStart(ev);
|
||||
let handler = ev => app._onDragStart(ev);
|
||||
favtabHtml.find('.item').each((i, li) => {
|
||||
if (li.classList.contains("inventory-header")) return;
|
||||
li.setAttribute("draggable", true);
|
||||
|
@ -566,7 +547,14 @@ async function addFavorites(app, html, data) {
|
|||
});
|
||||
}
|
||||
tabContainer.append(favtabHtml);
|
||||
|
||||
// if(app.options.editable) {
|
||||
// let handler = ev => app._onDragItemStart(ev);
|
||||
// tabContainer.find('.item').each((i, li) => {
|
||||
// if (li.classList.contains("inventory-header")) return;
|
||||
// li.setAttribute("draggable", true);
|
||||
// li.addEventListener("dragstart", handler, false);
|
||||
// });
|
||||
//}
|
||||
// try {
|
||||
// if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div");
|
||||
// }
|
||||
|
|
138
module/actor/sheets/newSheet/npc.js
Normal file
138
module/actor/sheets/newSheet/npc.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for NPC type characters in the SW5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @extends {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
||||
|
||||
/** @override */
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
|
||||
}
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 600,
|
||||
width: 800,
|
||||
tabs: [{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
||||
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
|
||||
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [powers, other] = data.items.reduce((arr, item) => {
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
if ( item.type === "power" ) arr[0].push(item);
|
||||
else arr[1].push(item);
|
||||
return arr;
|
||||
}, [[], []]);
|
||||
|
||||
// Apply item filters
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
|
||||
// Organize Powerbook
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
|
||||
// Organize Features
|
||||
for ( let item of other ) {
|
||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
||||
else if ( item.type === "feat" ) {
|
||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
}
|
||||
else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.powerbook = powerbook;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
|
||||
// Challenge Rating
|
||||
const cr = parseFloat(data.data.details.cr || 0);
|
||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Object Updates */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
|
||||
// Format NPC Challenge Rating
|
||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||
let crv = "data.details.cr";
|
||||
let cr = formData[crv];
|
||||
cr = crs[cr] || parseFloat(cr);
|
||||
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
|
||||
// Parent ActorSheet update steps
|
||||
super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling NPC health values using the provided formula
|
||||
* @param {Event} event The original click event
|
||||
* @private
|
||||
*/
|
||||
_onRollHealthFormula(event) {
|
||||
event.preventDefault();
|
||||
const formula = this.actor.data.data.attributes.hp.formula;
|
||||
if ( !formula ) return;
|
||||
const hp = new Roll(formula).roll().total;
|
||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
||||
}
|
||||
}
|
||||
|
385
module/actor/sheets/newSheet/vehicle.js
Normal file
385
module/actor/sheets/newSheet/vehicle.js
Normal file
|
@ -0,0 +1,385 @@
|
|||
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 max = actorData.data.attributes.capacity.cargo;
|
||||
const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
|
||||
return {value: totalWeight.toNearest(0.1), max, pct};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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 : '—';
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getMovementSpeed(actorData) {
|
||||
return {primary: "", special: ""};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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});
|
||||
}
|
||||
};
|
|
@ -1,9 +1,10 @@
|
|||
import Item5e from "../../item/entity.js";
|
||||
import TraitSelector from "../../apps/trait-selector.js";
|
||||
import ActorSheetFlags from "../../apps/actor-flags.js";
|
||||
import MovementConfig from "../../apps/movement-config.js";
|
||||
import {SW5E} from '../../config.js';
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../effects.js";
|
||||
import Item5e from "../../../item/entity.js";
|
||||
import TraitSelector from "../../../apps/trait-selector.js";
|
||||
import ActorSheetFlags from "../../../apps/actor-flags.js";
|
||||
import ActorMovementConfig from "../../../apps/movement-config.js";
|
||||
import ActorSensesConfig from "../../../apps/senses-config.js";
|
||||
import {SW5E} from '../../../config.js';
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
|
||||
|
||||
/**
|
||||
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
|
||||
|
@ -99,6 +100,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
// Movement speeds
|
||||
data.movement = this._getMovementSpeed(data.actor);
|
||||
|
||||
// Senses
|
||||
data.senses = this._getSenses(data.actor);
|
||||
|
||||
// Update traits
|
||||
this._prepareTraits(data.actor.data.traits);
|
||||
|
||||
|
@ -121,7 +125,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @private
|
||||
*/
|
||||
_getMovementSpeed(actorData) {
|
||||
const movement = actorData.data.attributes.movement;
|
||||
const movement = actorData.data.attributes.movement || {};
|
||||
const speeds = [
|
||||
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
|
||||
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
|
||||
|
@ -136,6 +140,20 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_getSenses(actorData) {
|
||||
const senses = actorData.data.attributes.senses || {};
|
||||
const tags = {};
|
||||
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
|
||||
const v = senses[k] ?? 0
|
||||
if ( v === 0 ) continue;
|
||||
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
|
||||
}
|
||||
if ( !!senses.special ) tags["special"] = senses.special;
|
||||
return tags;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -373,8 +391,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
|
||||
|
||||
// Configure Special Flags
|
||||
html.find('.configure-movement').click(this._onMovementConfig.bind(this));
|
||||
html.find('.configure-flags').click(this._onConfigureFlags.bind(this));
|
||||
html.find('.config-button').click(this._onConfigMenu.bind(this));
|
||||
|
||||
// Owned Item management
|
||||
html.find('.item-create').click(this._onItemCreate.bind(this));
|
||||
|
@ -448,11 +465,24 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle click events for the Traits tab button to configure special Character Flags
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onConfigureFlags(event) {
|
||||
_onConfigMenu(event) {
|
||||
event.preventDefault();
|
||||
new ActorSheetFlags(this.actor).render(true);
|
||||
const button = event.currentTarget;
|
||||
switch ( button.dataset.action ) {
|
||||
case "movement":
|
||||
new ActorMovementConfig(this.object).render(true);
|
||||
break;
|
||||
case "flags":
|
||||
new ActorSheetFlags(this.object).render(true);
|
||||
break;
|
||||
case "senses":
|
||||
new ActorSensesConfig(this.object).render(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -529,6 +559,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
icon: '<i class="fas fa-paw"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphWildShape'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
keepBio: true,
|
||||
keepClass: true,
|
||||
keepMental: true,
|
||||
mergeSaves: true,
|
||||
mergeSkills: true,
|
||||
|
@ -619,14 +651,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
|
||||
// Roll powers through the actor
|
||||
if ( item.data.type === "power" ) {
|
||||
return this.actor.usePower(item, {configureDialog: !event.shiftKey});
|
||||
}
|
||||
|
||||
// Otherwise roll the Item directly
|
||||
else return item.roll();
|
||||
return item.roll();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -687,7 +712,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
data: duplicate(header.dataset)
|
||||
};
|
||||
delete itemData.data["type"];
|
||||
return this.actor.createOwnedItem(itemData);
|
||||
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -791,18 +816,6 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onMovementConfig(event) {
|
||||
event.preventDefault();
|
||||
new MovementConfig(this.object).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getHeaderButtons() {
|
||||
let buttons = super._getHeaderButtons();
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
|
@ -75,6 +75,18 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.attunement = {
|
||||
1: {
|
||||
icon: "fa-sun",
|
||||
cls: "not-attuned",
|
||||
title: "SW5E.AttunementRequired"
|
||||
},
|
||||
2: {
|
||||
icon: "fa-sun",
|
||||
cls: "attuned",
|
||||
title: "SW5E.AttunementAttuned"
|
||||
}
|
||||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
|
@ -122,7 +134,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
// Organize Features
|
||||
const features = {
|
||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
||||
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true },
|
||||
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
|
||||
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
||||
|
@ -203,7 +215,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling a death saving throw for the Character
|
||||
* Handle mouse click events for character sheet actions
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
|
@ -263,37 +275,21 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
let addLevel = false;
|
||||
|
||||
// Upgrade the number of class levels a character has and add features
|
||||
// Increment the number of class levels a character instead of creating a new item
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
const hasClass = !!cls;
|
||||
|
||||
// Increment levels instead of creating a new item
|
||||
if ( hasClass ) {
|
||||
if ( !!cls ) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
if ( next > priorLevel ) {
|
||||
itemData.levels = next;
|
||||
await cls.update({"data.levels": next});
|
||||
addLevel = true;
|
||||
return cls.update({"data.levels": next});
|
||||
}
|
||||
}
|
||||
|
||||
// Add class features
|
||||
if ( !hasClass || addLevel ) {
|
||||
const features = await Actor5e.getClassFeatures({
|
||||
className: itemData.name,
|
||||
archetypeName: itemData.data.archetype,
|
||||
level: itemData.levels,
|
||||
priorLevel: priorLevel
|
||||
});
|
||||
await this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
}
|
||||
}
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
if ( !addLevel ) super._onDropItemCreate(itemData);
|
||||
super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for NPC type characters in the SW5E system.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for Vehicle type actors.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue