forked from GitHub-Mirrors/foundry-sw5e
commit
2212e3c7d8
43 changed files with 13465 additions and 12707 deletions
14
.prettierrc
Normal file
14
.prettierrc
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -1,2 +1,3 @@
|
||||||
{
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
}
|
}
|
10
gulpfile.js
10
gulpfile.js
|
@ -8,19 +8,19 @@ const less = require("gulp-less");
|
||||||
const SW5E_LESS = ["less/**/*.less"];
|
const SW5E_LESS = ["less/**/*.less"];
|
||||||
|
|
||||||
function compileLESS() {
|
function compileLESS() {
|
||||||
return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./"));
|
return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileGlobalLess() {
|
function compileGlobalLess() {
|
||||||
return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./"));
|
return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileLightLess() {
|
function compileLightLess() {
|
||||||
return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./"));
|
return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileDarkLess() {
|
function compileDarkLess() {
|
||||||
return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./"));
|
return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess);
|
const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess);
|
||||||
|
@ -30,7 +30,7 @@ const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compil
|
||||||
/* ----------------------------------------- */
|
/* ----------------------------------------- */
|
||||||
|
|
||||||
function watchUpdates() {
|
function watchUpdates() {
|
||||||
gulp.watch(SW5E_LESS, css);
|
gulp.watch(SW5E_LESS, css);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------- */
|
/* ----------------------------------------- */
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -6,143 +6,154 @@ import ActorSheet5e from "./base.js";
|
||||||
* @extends {ActorSheet5e}
|
* @extends {ActorSheet5e}
|
||||||
*/
|
*/
|
||||||
export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
||||||
|
/** @override */
|
||||||
/** @override */
|
get template() {
|
||||||
get template() {
|
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
|
||||||
return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
|
}
|
||||||
}
|
/** @override */
|
||||||
/** @override */
|
static get defaultOptions() {
|
||||||
static get defaultOptions() {
|
return mergeObject(super.defaultOptions, {
|
||||||
return mergeObject(super.defaultOptions, {
|
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
width: 800,
|
||||||
width: 800,
|
tabs: [
|
||||||
tabs: [{
|
{
|
||||||
navSelector: ".root-tabs",
|
navSelector: ".root-tabs",
|
||||||
contentSelector: ".sheet-body",
|
contentSelector: ".sheet-body",
|
||||||
initial: "attributes"
|
initial: "attributes"
|
||||||
}],
|
}
|
||||||
});
|
]
|
||||||
}
|
});
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
static unsupportedItemTypes = new Set(["class"]);
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
|
|
||||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
|
||||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
|
||||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
|
||||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
|
||||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
|
||||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
|
||||||
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
|
|
||||||
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
|
|
||||||
else arr[2].push(item);
|
|
||||||
return arr;
|
|
||||||
}, [[], [], []]);
|
|
||||||
|
|
||||||
// Apply item filters
|
|
||||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
|
||||||
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
|
||||||
other = this._filterItems(other, this._filters.features);
|
|
||||||
|
|
||||||
// Organize Powerbook
|
|
||||||
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
|
||||||
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
|
||||||
|
|
||||||
// Organize Features
|
|
||||||
for ( let item of other ) {
|
|
||||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
|
||||||
else if ( item.type === "feat" ) {
|
|
||||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
|
||||||
else features.passive.items.push(item);
|
|
||||||
}
|
|
||||||
else features.equipment.items.push(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign and return
|
/* -------------------------------------------- */
|
||||||
data.features = Object.values(features);
|
|
||||||
data.forcePowerbook = forcePowerbook;
|
|
||||||
data.techPowerbook = techPowerbook;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/** @override */
|
||||||
|
static unsupportedItemTypes = new Set(["class"]);
|
||||||
|
|
||||||
/** @inheritdoc */
|
/* -------------------------------------------- */
|
||||||
getData(options) {
|
|
||||||
const data = super.getData(options);
|
|
||||||
|
|
||||||
// Challenge Rating
|
/**
|
||||||
const cr = parseFloat(data.data.details.cr || 0);
|
* Organize Owned Items for rendering the NPC sheet
|
||||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
* @private
|
||||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
*/
|
||||||
|
_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"}}
|
||||||
|
};
|
||||||
|
|
||||||
// Creature Type
|
// Start by classifying items into groups for rendering
|
||||||
data.labels["type"] = this.actor.labels.creatureType;
|
let [forcepowers, techpowers, other] = data.items.reduce(
|
||||||
return data;
|
(arr, item) => {
|
||||||
}
|
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||||
|
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||||
|
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||||
|
item.isOnCooldown =
|
||||||
|
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||||
|
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||||
|
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||||
|
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
|
||||||
|
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
|
||||||
|
else arr[2].push(item);
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
|
[[], [], []]
|
||||||
|
);
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Apply item filters
|
||||||
/* Object Updates */
|
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||||
/* -------------------------------------------- */
|
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
||||||
|
other = this._filterItems(other, this._filters.features);
|
||||||
|
|
||||||
/** @override */
|
// Organize Powerbook
|
||||||
async _updateObject(event, formData) {
|
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
||||||
|
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||||
|
|
||||||
// Format NPC Challenge Rating
|
// Organize Features
|
||||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
for (let item of other) {
|
||||||
let crv = "data.details.cr";
|
if (item.type === "weapon") features.weapons.items.push(item);
|
||||||
let cr = formData[crv];
|
else if (item.type === "feat") {
|
||||||
cr = crs[cr] || parseFloat(cr);
|
if (item.data.activation.type) features.actions.items.push(item);
|
||||||
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
else features.passive.items.push(item);
|
||||||
|
} else features.equipment.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
// Parent ActorSheet update steps
|
// Assign and return
|
||||||
return super._updateObject(event, formData);
|
data.features = Object.values(features);
|
||||||
}
|
data.forcePowerbook = forcePowerbook;
|
||||||
|
data.techPowerbook = techPowerbook;
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Event Listeners and Handlers */
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
/** @inheritdoc */
|
||||||
activateListeners(html) {
|
getData(options) {
|
||||||
super.activateListeners(html);
|
const data = super.getData(options);
|
||||||
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// 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;
|
||||||
|
|
||||||
/**
|
// Creature Type
|
||||||
* Handle rolling NPC health values using the provided formula
|
data.labels["type"] = this.actor.labels.creatureType;
|
||||||
* @param {Event} event The original click event
|
return data;
|
||||||
* @private
|
}
|
||||||
*/
|
|
||||||
_onRollHPFormula(event) {
|
/* -------------------------------------------- */
|
||||||
event.preventDefault();
|
/* Object Updates */
|
||||||
const formula = this.actor.data.data.attributes.hp.formula;
|
/* -------------------------------------------- */
|
||||||
if ( !formula ) return;
|
|
||||||
const hp = new Roll(formula).roll().total;
|
/** @override */
|
||||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
async _updateObject(event, formData) {
|
||||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
// 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
|
||||||
|
return super._updateObject(event, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* Event Listeners and Handlers */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
activateListeners(html) {
|
||||||
|
super.activateListeners(html);
|
||||||
|
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle rolling NPC health values using the provided formula
|
||||||
|
* @param {Event} event The original click event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onRollHPFormula(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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,150 +6,164 @@ import ActorSheet5e from "./base.js";
|
||||||
* @extends {ActorSheet5e}
|
* @extends {ActorSheet5e}
|
||||||
*/
|
*/
|
||||||
export default class ActorSheet5eStarship extends ActorSheet5e {
|
export default class ActorSheet5eStarship extends ActorSheet5e {
|
||||||
|
/** @override */
|
||||||
/** @override */
|
get template() {
|
||||||
get template() {
|
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
return `systems/sw5e/templates/actors/newActor/starship.html`;
|
||||||
return `systems/sw5e/templates/actors/newActor/starship.html`;
|
}
|
||||||
}
|
/** @override */
|
||||||
/** @override */
|
static get defaultOptions() {
|
||||||
static get defaultOptions() {
|
return mergeObject(super.defaultOptions, {
|
||||||
return mergeObject(super.defaultOptions, {
|
classes: ["sw5e", "sheet", "actor", "starship"],
|
||||||
classes: ["sw5e", "sheet", "actor", "starship"],
|
width: 800,
|
||||||
width: 800,
|
tabs: [
|
||||||
tabs: [{
|
{
|
||||||
navSelector: ".root-tabs",
|
navSelector: ".root-tabs",
|
||||||
contentSelector: ".sheet-body",
|
contentSelector: ".sheet-body",
|
||||||
initial: "attributes"
|
initial: "attributes"
|
||||||
}],
|
}
|
||||||
});
|
]
|
||||||
}
|
});
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Organize Owned Items for rendering the starship sheet
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_prepareItems(data) {
|
|
||||||
|
|
||||||
// Categorize Items as Features and Powers
|
|
||||||
const features = {
|
|
||||||
weapons: { label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), items: [], hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
|
||||||
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
|
|
||||||
equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
|
|
||||||
starshipfeatures: { label: game.i18n.localize("SW5E.StarshipfeaturePl"), items: [], hasActions: true, dataset: {type: "starshipfeature"} },
|
|
||||||
starshipmods: { label: game.i18n.localize("SW5E.StarshipmodPl"), items: [], hasActions: false, dataset: {type: "starshipmod"} }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start by classifying items into groups for rendering
|
|
||||||
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
|
|
||||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
|
||||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
|
||||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
|
||||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
|
||||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
|
||||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
|
||||||
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
|
|
||||||
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
|
|
||||||
else arr[2].push(item);
|
|
||||||
return arr;
|
|
||||||
}, [[], [], []]);
|
|
||||||
|
|
||||||
// Apply item filters
|
|
||||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
|
||||||
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
|
||||||
other = this._filterItems(other, this._filters.features);
|
|
||||||
|
|
||||||
// Organize Powerbook
|
|
||||||
// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
|
||||||
// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
|
||||||
|
|
||||||
// Organize Features
|
|
||||||
for ( let item of other ) {
|
|
||||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
|
||||||
else if ( item.type === "feat" ) {
|
|
||||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
|
||||||
else features.passive.items.push(item);
|
|
||||||
}
|
|
||||||
else if ( item.type === "starshipfeature" ) {
|
|
||||||
features.starshipfeatures.items.push(item);
|
|
||||||
}
|
|
||||||
else if ( item.type === "starshipmod" ) {
|
|
||||||
features.starshipmods.items.push(item);
|
|
||||||
}
|
|
||||||
else features.equipment.items.push(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign and return
|
/* -------------------------------------------- */
|
||||||
data.features = Object.values(features);
|
|
||||||
// data.forcePowerbook = forcePowerbook;
|
|
||||||
// data.techPowerbook = techPowerbook;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organize Owned Items for rendering the starship sheet
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_prepareItems(data) {
|
||||||
|
// Categorize Items as Features and Powers
|
||||||
|
const features = {
|
||||||
|
weapons: {
|
||||||
|
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
|
||||||
|
items: [],
|
||||||
|
hasActions: true,
|
||||||
|
dataset: {"type": "weapon", "weapon-type": "natural"}
|
||||||
|
},
|
||||||
|
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
|
||||||
|
equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
|
||||||
|
starshipfeatures: {
|
||||||
|
label: game.i18n.localize("SW5E.StarshipfeaturePl"),
|
||||||
|
items: [],
|
||||||
|
hasActions: true,
|
||||||
|
dataset: {type: "starshipfeature"}
|
||||||
|
},
|
||||||
|
starshipmods: {
|
||||||
|
label: game.i18n.localize("SW5E.StarshipmodPl"),
|
||||||
|
items: [],
|
||||||
|
hasActions: false,
|
||||||
|
dataset: {type: "starshipmod"}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Start by classifying items into groups for rendering
|
||||||
|
let [forcepowers, techpowers, other] = data.items.reduce(
|
||||||
|
(arr, item) => {
|
||||||
|
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||||
|
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||||
|
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||||
|
item.isOnCooldown =
|
||||||
|
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||||
|
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||||
|
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||||
|
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
|
||||||
|
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
|
||||||
|
else arr[2].push(item);
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
|
[[], [], []]
|
||||||
|
);
|
||||||
|
|
||||||
/** @override */
|
// Apply item filters
|
||||||
getData(options) {
|
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||||
const data = super.getData(options);
|
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
||||||
|
other = this._filterItems(other, this._filters.features);
|
||||||
|
|
||||||
// Add Size info
|
// Organize Powerbook
|
||||||
data.isTiny = data.actor.data.traits.size === "tiny";
|
// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
||||||
data.isSmall = data.actor.data.traits.size === "sm";
|
// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||||
data.isMedium = data.actor.data.traits.size === "med";
|
|
||||||
data.isLarge = data.actor.data.traits.size === "lg";
|
|
||||||
data.isHuge = data.actor.data.traits.size === "huge";
|
|
||||||
data.isGargantuan = data.actor.data.traits.size === "grg";
|
|
||||||
|
|
||||||
// Challenge Rating
|
// Organize Features
|
||||||
const cr = parseFloat(data.data.details.cr || 0);
|
for (let item of other) {
|
||||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
if (item.type === "weapon") features.weapons.items.push(item);
|
||||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
else if (item.type === "feat") {
|
||||||
return data;
|
if (item.data.activation.type) features.actions.items.push(item);
|
||||||
}
|
else features.passive.items.push(item);
|
||||||
|
} else if (item.type === "starshipfeature") {
|
||||||
|
features.starshipfeatures.items.push(item);
|
||||||
|
} else if (item.type === "starshipmod") {
|
||||||
|
features.starshipmods.items.push(item);
|
||||||
|
} else features.equipment.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Assign and return
|
||||||
/* Object Updates */
|
data.features = Object.values(features);
|
||||||
/* -------------------------------------------- */
|
// data.forcePowerbook = forcePowerbook;
|
||||||
|
// data.techPowerbook = techPowerbook;
|
||||||
|
}
|
||||||
|
|
||||||
/** @override */
|
/* -------------------------------------------- */
|
||||||
async _updateObject(event, formData) {
|
|
||||||
|
|
||||||
// Format NPC Challenge Rating
|
/** @override */
|
||||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
getData(options) {
|
||||||
let crv = "data.details.cr";
|
const data = super.getData(options);
|
||||||
let cr = formData[crv];
|
|
||||||
cr = crs[cr] || parseFloat(cr);
|
|
||||||
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
|
||||||
|
|
||||||
// Parent ActorSheet update steps
|
// Add Size info
|
||||||
return super._updateObject(event, formData);
|
data.isTiny = data.actor.data.traits.size === "tiny";
|
||||||
}
|
data.isSmall = data.actor.data.traits.size === "sm";
|
||||||
|
data.isMedium = data.actor.data.traits.size === "med";
|
||||||
|
data.isLarge = data.actor.data.traits.size === "lg";
|
||||||
|
data.isHuge = data.actor.data.traits.size === "huge";
|
||||||
|
data.isGargantuan = data.actor.data.traits.size === "grg";
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Challenge Rating
|
||||||
/* Event Listeners and Handlers */
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/** @override */
|
/* -------------------------------------------- */
|
||||||
activateListeners(html) {
|
/* Object Updates */
|
||||||
super.activateListeners(html);
|
/* -------------------------------------------- */
|
||||||
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/** @override */
|
||||||
|
async _updateObject(event, formData) {
|
||||||
|
// Format NPC Challenge Rating
|
||||||
|
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||||
|
let crv = "data.details.cr";
|
||||||
|
let cr = formData[crv];
|
||||||
|
cr = crs[cr] || parseFloat(cr);
|
||||||
|
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||||
|
|
||||||
/**
|
// Parent ActorSheet update steps
|
||||||
* Handle rolling NPC health values using the provided formula
|
return super._updateObject(event, formData);
|
||||||
* @param {Event} event The original click event
|
}
|
||||||
* @private
|
|
||||||
*/
|
/* -------------------------------------------- */
|
||||||
_onRollHPFormula(event) {
|
/* Event Listeners and Handlers */
|
||||||
event.preventDefault();
|
/* -------------------------------------------- */
|
||||||
const formula = this.actor.data.data.attributes.hp.formula;
|
|
||||||
if ( !formula ) return;
|
/** @override */
|
||||||
const hp = new Roll(formula).roll().total;
|
activateListeners(html) {
|
||||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
super.activateListeners(html);
|
||||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle rolling NPC health values using the provided formula
|
||||||
|
* @param {Event} event The original click event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onRollHPFormula(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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,411 +6,427 @@ import ActorSheet5e from "./base.js";
|
||||||
* @type {ActorSheet5e}
|
* @type {ActorSheet5e}
|
||||||
*/
|
*/
|
||||||
export default class ActorSheet5eVehicle extends ActorSheet5e {
|
export default class ActorSheet5eVehicle extends ActorSheet5e {
|
||||||
/**
|
/**
|
||||||
* Define default rendering options for the Vehicle sheet.
|
* Define default rendering options for the Vehicle sheet.
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
static get defaultOptions() {
|
static get defaultOptions() {
|
||||||
return mergeObject(super.defaultOptions, {
|
return mergeObject(super.defaultOptions, {
|
||||||
classes: ["sw5e", "sheet", "actor", "vehicle"],
|
classes: ["sw5e", "sheet", "actor", "vehicle"],
|
||||||
width: 605,
|
width: 605,
|
||||||
height: 680
|
height: 680
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
static unsupportedItemTypes = new Set(["class"]);
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
_getMovementSpeed(actorData, largestPrimary=true) {
|
|
||||||
return super._getMovementSpeed(actorData, largestPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 */
|
||||||
|
static unsupportedItemTypes = new Set(["class"]);
|
||||||
|
|
||||||
/**
|
/* -------------------------------------------- */
|
||||||
* 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'),
|
* Creates a new cargo entry for a vehicle Actor.
|
||||||
css: 'item-qty',
|
*/
|
||||||
property: 'data.quantity'
|
static get newCargo() {
|
||||||
}, {
|
return {
|
||||||
label: game.i18n.localize('SW5E.AC'),
|
name: "",
|
||||||
css: 'item-ac',
|
quantity: 1
|
||||||
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'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Classify items owned by the vehicle and compute total cargo weight
|
|
||||||
let totalWeight = 0;
|
|
||||||
for (const item of data.items) {
|
|
||||||
this._prepareCrewedItem(item);
|
|
||||||
|
|
||||||
// Handle cargo explicitly
|
|
||||||
const isCargo = item.flags.sw5e?.vehicleCargo === true;
|
|
||||||
if ( isCargo ) {
|
|
||||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
|
||||||
cargo.cargo.items.push(item);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle non-cargo item types
|
|
||||||
switch ( item.type ) {
|
|
||||||
case "weapon":
|
|
||||||
features.weapons.items.push(item);
|
|
||||||
break;
|
|
||||||
case "equipment":
|
|
||||||
features.equipment.items.push(item);
|
|
||||||
break;
|
|
||||||
case "feat":
|
|
||||||
if ( !item.data.activation.type || (item.data.activation.type === "none") ) features.passive.items.push(item);
|
|
||||||
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
|
|
||||||
else features.actions.items.push(item);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
|
||||||
cargo.cargo.items.push(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the rendering context data
|
/* -------------------------------------------- */
|
||||||
data.features = Object.values(features);
|
|
||||||
data.cargo = Object.values(cargo);
|
|
||||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
/* Event Listeners and Handlers */
|
* 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;
|
||||||
|
|
||||||
/** @override */
|
// Vehicle weights are an order of magnitude greater.
|
||||||
activateListeners(html) {
|
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
|
||||||
super.activateListeners(html);
|
|
||||||
if (!this.isEditable) return;
|
|
||||||
|
|
||||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
// Compute overall encumbrance
|
||||||
html.find('.item-hp input')
|
const max = actorData.data.attributes.capacity.cargo;
|
||||||
.click(evt => evt.target.select())
|
const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
|
||||||
.change(this._onHPChange.bind(this));
|
return {value: totalWeight.toNearest(0.1), max, pct};
|
||||||
|
|
||||||
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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
|
||||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super._onItemDelete(event);
|
/* -------------------------------------------- */
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/** @override */
|
||||||
|
_getMovementSpeed(actorData, largestPrimary = true) {
|
||||||
|
return super._getMovementSpeed(actorData, largestPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
/** @override */
|
/* -------------------------------------------- */
|
||||||
async _onDropItemCreate(itemData) {
|
|
||||||
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
|
|
||||||
const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
|
|
||||||
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
|
|
||||||
return super._onDropItemCreate(itemData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
|
* 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
|
||||||
* Special handling for editing HP to clamp it within appropriate range.
|
if (item.type === "feat" && item.data.activation.type === "crew") {
|
||||||
* @param event {Event}
|
item.crew = item.data.activation.cost;
|
||||||
* @returns {Promise<Item>}
|
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
|
||||||
* @private
|
if (item.data.cover === 0.5) item.cover = "½";
|
||||||
*/
|
else if (item.data.cover === 0.75) item.cover = "¾";
|
||||||
_onHPChange(event) {
|
else if (item.data.cover === null) item.cover = "—";
|
||||||
event.preventDefault();
|
if (item.crew < 1 || item.crew === null) item.crew = "—";
|
||||||
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});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Prepare vehicle weapons
|
||||||
|
if (item.type === "equipment" || item.type === "weapon") {
|
||||||
|
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/* -------------------------------------------- */
|
||||||
* Handle toggling an item's crewed status.
|
|
||||||
* @param event {Event}
|
/**
|
||||||
* @returns {Promise<Item>}
|
* Organize Owned Items for rendering the Vehicle sheet.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_onToggleItem(event) {
|
_prepareItems(data) {
|
||||||
event.preventDefault();
|
const cargoColumns = [
|
||||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
{
|
||||||
const item = this.actor.items.get(itemID);
|
label: game.i18n.localize("SW5E.Quantity"),
|
||||||
const crewed = !!item.data.data.crewed;
|
css: "item-qty",
|
||||||
return item.update({'data.crewed': !crewed});
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Classify items owned by the vehicle and compute total cargo weight
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (const item of data.items) {
|
||||||
|
this._prepareCrewedItem(item);
|
||||||
|
|
||||||
|
// Handle cargo explicitly
|
||||||
|
const isCargo = item.flags.sw5e?.vehicleCargo === true;
|
||||||
|
if (isCargo) {
|
||||||
|
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||||
|
cargo.cargo.items.push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-cargo item types
|
||||||
|
switch (item.type) {
|
||||||
|
case "weapon":
|
||||||
|
features.weapons.items.push(item);
|
||||||
|
break;
|
||||||
|
case "equipment":
|
||||||
|
features.equipment.items.push(item);
|
||||||
|
break;
|
||||||
|
case "feat":
|
||||||
|
if (!item.data.activation.type || item.data.activation.type === "none")
|
||||||
|
features.passive.items.push(item);
|
||||||
|
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
|
||||||
|
else features.actions.items.push(item);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||||
|
cargo.cargo.items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the rendering context data
|
||||||
|
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.isEditable) 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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||||
|
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||||
|
}
|
||||||
|
|
||||||
|
return super._onItemDelete(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
async _onDropItemCreate(itemData) {
|
||||||
|
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
|
||||||
|
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
|
||||||
|
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
|
||||||
|
return super._onDropItemCreate(itemData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,295 +7,362 @@ import Actor5e from "../../entity.js";
|
||||||
* @type {ActorSheet5e}
|
* @type {ActorSheet5e}
|
||||||
*/
|
*/
|
||||||
export default class ActorSheet5eCharacter extends ActorSheet5e {
|
export default class ActorSheet5eCharacter extends ActorSheet5e {
|
||||||
|
/**
|
||||||
|
* Define default rendering options for the NPC sheet
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
static get defaultOptions() {
|
||||||
|
return mergeObject(super.defaultOptions, {
|
||||||
|
classes: ["sw5e", "sheet", "actor", "character"],
|
||||||
|
width: 720,
|
||||||
|
height: 736
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/* -------------------------------------------- */
|
||||||
* Define default rendering options for the NPC sheet
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
static get defaultOptions() {
|
|
||||||
return mergeObject(super.defaultOptions, {
|
|
||||||
classes: ["sw5e", "sheet", "actor", "character"],
|
|
||||||
width: 720,
|
|
||||||
height: 736
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
|
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||||
|
*/
|
||||||
|
getData() {
|
||||||
|
const sheetData = super.getData();
|
||||||
|
|
||||||
/**
|
// Temporary HP
|
||||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
let hp = sheetData.data.attributes.hp;
|
||||||
*/
|
if (hp.temp === 0) delete hp.temp;
|
||||||
getData() {
|
if (hp.tempmax === 0) delete hp.tempmax;
|
||||||
const sheetData = super.getData();
|
|
||||||
|
|
||||||
// Temporary HP
|
// Resources
|
||||||
let hp = sheetData.data.attributes.hp;
|
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||||
if (hp.temp === 0) delete hp.temp;
|
const res = sheetData.data.resources[r] || {};
|
||||||
if (hp.tempmax === 0) delete hp.tempmax;
|
res.name = r;
|
||||||
|
res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase());
|
||||||
|
if (res && res.value === 0) delete res.value;
|
||||||
|
if (res && res.max === 0) delete res.max;
|
||||||
|
return arr.concat([res]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Resources
|
// Experience Tracking
|
||||||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||||
const res = sheetData.data.resources[r] || {};
|
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
|
||||||
res.name = r;
|
sheetData["multiclassLabels"] = this.actor.itemTypes.class
|
||||||
res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase());
|
.map((c) => {
|
||||||
if (res && res.value === 0) delete res.value;
|
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
|
||||||
if (res && res.max === 0) delete res.max;
|
})
|
||||||
return arr.concat([res]);
|
.join(", ");
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Experience Tracking
|
// Return data for rendering
|
||||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
return sheetData;
|
||||||
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
|
}
|
||||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
|
|
||||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
|
|
||||||
}).join(', ');
|
|
||||||
|
|
||||||
// Return data for rendering
|
/* -------------------------------------------- */
|
||||||
return sheetData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
|
* Organize and classify Owned Items for Character sheets
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_prepareItems(data) {
|
||||||
|
// Categorize items as inventory, powerbook, features, and classes
|
||||||
|
const inventory = {
|
||||||
|
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
|
||||||
|
equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}},
|
||||||
|
consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}},
|
||||||
|
tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}},
|
||||||
|
backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}},
|
||||||
|
loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
// Partition items by category
|
||||||
* Organize and classify Owned Items for Character sheets
|
let [
|
||||||
* @private
|
items,
|
||||||
*/
|
powers,
|
||||||
_prepareItems(data) {
|
feats,
|
||||||
|
classes,
|
||||||
|
species,
|
||||||
|
archetypes,
|
||||||
|
classfeatures,
|
||||||
|
backgrounds,
|
||||||
|
fightingstyles,
|
||||||
|
fightingmasteries,
|
||||||
|
lightsaberforms
|
||||||
|
] = data.items.reduce(
|
||||||
|
(arr, item) => {
|
||||||
|
// Item details
|
||||||
|
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||||
|
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||||
|
item.attunement = {
|
||||||
|
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||||
|
icon: "fa-sun",
|
||||||
|
cls: "not-attuned",
|
||||||
|
title: "SW5E.AttunementRequired"
|
||||||
|
},
|
||||||
|
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
||||||
|
icon: "fa-sun",
|
||||||
|
cls: "attuned",
|
||||||
|
title: "SW5E.AttunementAttuned"
|
||||||
|
}
|
||||||
|
}[item.data.attunement];
|
||||||
|
|
||||||
// Categorize items as inventory, powerbook, features, and classes
|
// Item usage
|
||||||
const inventory = {
|
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||||
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
|
item.isOnCooldown =
|
||||||
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} },
|
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||||
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} },
|
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||||
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} },
|
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
|
||||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Partition items by category
|
// Item toggle state
|
||||||
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
this._prepareItemToggleState(item);
|
||||||
|
|
||||||
// Item details
|
// Primary Class
|
||||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
if (item.type === "class")
|
||||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
|
||||||
item.attunement = {
|
|
||||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
// Classify items into types
|
||||||
icon: "fa-sun",
|
if (item.type === "power") arr[1].push(item);
|
||||||
cls: "not-attuned",
|
else if (item.type === "feat") arr[2].push(item);
|
||||||
title: "SW5E.AttunementRequired"
|
else if (item.type === "class") arr[3].push(item);
|
||||||
},
|
else if (item.type === "species") arr[4].push(item);
|
||||||
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
else if (item.type === "archetype") arr[5].push(item);
|
||||||
icon: "fa-sun",
|
else if (item.type === "classfeature") arr[6].push(item);
|
||||||
cls: "attuned",
|
else if (item.type === "background") arr[7].push(item);
|
||||||
title: "SW5E.AttunementAttuned"
|
else if (item.type === "fightingstyle") arr[8].push(item);
|
||||||
|
else if (item.type === "fightingmastery") arr[9].push(item);
|
||||||
|
else if (item.type === "lightsaberform") arr[10].push(item);
|
||||||
|
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
|
[[], [], [], [], [], [], [], [], [], [], []]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply active item filters
|
||||||
|
items = this._filterItems(items, this._filters.inventory);
|
||||||
|
powers = this._filterItems(powers, this._filters.powerbook);
|
||||||
|
feats = this._filterItems(feats, this._filters.features);
|
||||||
|
|
||||||
|
// Organize items
|
||||||
|
for (let i of items) {
|
||||||
|
i.data.quantity = i.data.quantity || 0;
|
||||||
|
i.data.weight = i.data.weight || 0;
|
||||||
|
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||||
|
inventory[i.type].items.push(i);
|
||||||
}
|
}
|
||||||
}[item.data.attunement];
|
|
||||||
|
|
||||||
// Item usage
|
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
const powerbook = this._preparePowerbook(data, powers);
|
||||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
const nPrepared = powers.filter((s) => {
|
||||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared;
|
||||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
}).length;
|
||||||
|
|
||||||
// Item toggle state
|
// Organize Features
|
||||||
this._prepareItemToggleState(item);
|
const features = {
|
||||||
|
classes: {
|
||||||
// Primary Class
|
label: "SW5E.ItemTypeClassPl",
|
||||||
if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass );
|
items: [],
|
||||||
|
hasActions: false,
|
||||||
// Classify items into types
|
dataset: {type: "class"},
|
||||||
if ( item.type === "power" ) arr[1].push(item);
|
isClass: true
|
||||||
else if ( item.type === "feat" ) arr[2].push(item);
|
},
|
||||||
else if ( item.type === "class" ) arr[3].push(item);
|
classfeatures: {
|
||||||
else if ( item.type === "species" ) arr[4].push(item);
|
label: "SW5E.ItemTypeClassFeats",
|
||||||
else if ( item.type === "archetype" ) arr[5].push(item);
|
items: [],
|
||||||
else if ( item.type === "classfeature" ) arr[6].push(item);
|
hasActions: true,
|
||||||
else if ( item.type === "background" ) arr[7].push(item);
|
dataset: {type: "classfeature"},
|
||||||
else if ( item.type === "fightingstyle" ) arr[8].push(item);
|
isClassfeature: true
|
||||||
else if ( item.type === "fightingmastery" ) arr[9].push(item);
|
},
|
||||||
else if ( item.type === "lightsaberform" ) arr[10].push(item);
|
archetype: {
|
||||||
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
|
label: "SW5E.ItemTypeArchetype",
|
||||||
return arr;
|
items: [],
|
||||||
}, [[], [], [], [], [], [], [], [], [], [], []]);
|
hasActions: false,
|
||||||
|
dataset: {type: "archetype"},
|
||||||
// Apply active item filters
|
isArchetype: true
|
||||||
items = this._filterItems(items, this._filters.inventory);
|
},
|
||||||
powers = this._filterItems(powers, this._filters.powerbook);
|
species: {
|
||||||
feats = this._filterItems(feats, this._filters.features);
|
label: "SW5E.ItemTypeSpecies",
|
||||||
|
items: [],
|
||||||
// Organize items
|
hasActions: false,
|
||||||
for ( let i of items ) {
|
dataset: {type: "species"},
|
||||||
i.data.quantity = i.data.quantity || 0;
|
isSpecies: true
|
||||||
i.data.weight = i.data.weight || 0;
|
},
|
||||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
background: {
|
||||||
inventory[i.type].items.push(i);
|
label: "SW5E.ItemTypeBackground",
|
||||||
}
|
items: [],
|
||||||
|
hasActions: false,
|
||||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
dataset: {type: "background"},
|
||||||
const powerbook = this._preparePowerbook(data, powers);
|
isBackground: true
|
||||||
const nPrepared = powers.filter(s => {
|
},
|
||||||
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
|
fightingstyles: {
|
||||||
}).length;
|
label: "SW5E.ItemTypeFightingStylePl",
|
||||||
|
items: [],
|
||||||
// Organize Features
|
hasActions: false,
|
||||||
const features = {
|
dataset: {type: "fightingstyle"},
|
||||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
isFightingstyle: 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 },
|
fightingmasteries: {
|
||||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
label: "SW5E.ItemTypeFightingMasteryPl",
|
||||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
items: [],
|
||||||
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
|
hasActions: false,
|
||||||
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
|
dataset: {type: "fightingmastery"},
|
||||||
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
|
isFightingmastery: true
|
||||||
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
},
|
||||||
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
lightsaberforms: {
|
||||||
};
|
label: "SW5E.ItemTypeLightsaberFormPl",
|
||||||
for ( let f of feats ) {
|
items: [],
|
||||||
if ( f.data.activation.type ) features.active.items.push(f);
|
hasActions: false,
|
||||||
else features.passive.items.push(f);
|
dataset: {type: "lightsaberform"},
|
||||||
}
|
isLightsaberform: true
|
||||||
classes.sort((a, b) => b.data.levels - a.data.levels);
|
},
|
||||||
features.classes.items = classes;
|
active: {
|
||||||
features.classfeatures.items = classfeatures;
|
label: "SW5E.FeatureActive",
|
||||||
features.archetype.items = archetypes;
|
items: [],
|
||||||
features.species.items = species;
|
hasActions: true,
|
||||||
features.background.items = backgrounds;
|
dataset: {"type": "feat", "activation.type": "action"}
|
||||||
features.fightingstyles.items = fightingstyles;
|
},
|
||||||
features.fightingmasteries.items = fightingmasteries;
|
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
|
||||||
features.lightsaberforms.items = lightsaberforms;
|
};
|
||||||
|
for (let f of feats) {
|
||||||
// Assign and return
|
if (f.data.activation.type) features.active.items.push(f);
|
||||||
data.inventory = Object.values(inventory);
|
else features.passive.items.push(f);
|
||||||
data.powerbook = powerbook;
|
|
||||||
data.preparedPowers = nPrepared;
|
|
||||||
data.features = Object.values(features);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper method to establish the displayed preparation state for an item
|
|
||||||
* @param {Item} item
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_prepareItemToggleState(item) {
|
|
||||||
if (item.type === "power") {
|
|
||||||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
|
||||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
|
||||||
item.toggleClass = isPrepared ? "active" : "";
|
|
||||||
if ( isAlways ) item.toggleClass = "fixed";
|
|
||||||
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
|
||||||
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
|
||||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const isActive = getProperty(item.data, "equipped");
|
|
||||||
item.toggleClass = isActive ? "active" : "";
|
|
||||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
/* Event Listeners and Handlers
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activate event listeners using the prepared sheet HTML
|
|
||||||
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
|
|
||||||
*/
|
|
||||||
activateListeners(html) {
|
|
||||||
super.activateListeners(html);
|
|
||||||
if ( !this.isEditable ) return;
|
|
||||||
|
|
||||||
// Item State Toggling
|
|
||||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
|
||||||
|
|
||||||
// Short and Long Rest
|
|
||||||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
|
||||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
|
||||||
|
|
||||||
// Rollable sheet actions
|
|
||||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle mouse click events for character sheet actions
|
|
||||||
* @param {MouseEvent} event The originating click event
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_onSheetAction(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const button = event.currentTarget;
|
|
||||||
switch( button.dataset.action ) {
|
|
||||||
case "rollDeathSave":
|
|
||||||
return this.actor.rollDeathSave({event: event});
|
|
||||||
case "rollInitiative":
|
|
||||||
return this.actor.rollInitiative({createCombatants: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle toggling the state of an Owned Item within the Actor
|
|
||||||
* @param {Event} event The triggering click event
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_onToggleItem(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
|
||||||
const item = this.actor.items.get(itemId);
|
|
||||||
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
|
||||||
return item.update({[attr]: !getProperty(item.data, attr)});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a short rest, calling the relevant function on the Actor instance
|
|
||||||
* @param {Event} event The triggering click event
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
async _onShortRest(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
await this._onSubmit(event);
|
|
||||||
return this.actor.shortRest();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a long rest, calling the relevant function on the Actor instance
|
|
||||||
* @param {Event} event The triggering click event
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
async _onLongRest(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
await this._onSubmit(event);
|
|
||||||
return this.actor.longRest();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
async _onDropItemCreate(itemData) {
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
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});
|
|
||||||
}
|
}
|
||||||
}
|
classes.sort((a, b) => b.data.levels - a.data.levels);
|
||||||
|
features.classes.items = classes;
|
||||||
|
features.classfeatures.items = classfeatures;
|
||||||
|
features.archetype.items = archetypes;
|
||||||
|
features.species.items = species;
|
||||||
|
features.background.items = backgrounds;
|
||||||
|
features.fightingstyles.items = fightingstyles;
|
||||||
|
features.fightingmasteries.items = fightingmasteries;
|
||||||
|
features.lightsaberforms.items = lightsaberforms;
|
||||||
|
|
||||||
|
// Assign and return
|
||||||
|
data.inventory = Object.values(inventory);
|
||||||
|
data.powerbook = powerbook;
|
||||||
|
data.preparedPowers = nPrepared;
|
||||||
|
data.features = Object.values(features);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default drop handling if levels were not added
|
/* -------------------------------------------- */
|
||||||
return super._onDropItemCreate(itemData);
|
|
||||||
}
|
/**
|
||||||
|
* A helper method to establish the displayed preparation state for an item
|
||||||
|
* @param {Item} item
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_prepareItemToggleState(item) {
|
||||||
|
if (item.type === "power") {
|
||||||
|
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||||
|
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||||
|
item.toggleClass = isPrepared ? "active" : "";
|
||||||
|
if (isAlways) item.toggleClass = "fixed";
|
||||||
|
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||||
|
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||||
|
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||||
|
} else {
|
||||||
|
const isActive = getProperty(item.data, "equipped");
|
||||||
|
item.toggleClass = isActive ? "active" : "";
|
||||||
|
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* Event Listeners and Handlers
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate event listeners using the prepared sheet HTML
|
||||||
|
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
|
||||||
|
*/
|
||||||
|
activateListeners(html) {
|
||||||
|
super.activateListeners(html);
|
||||||
|
if (!this.isEditable) return;
|
||||||
|
|
||||||
|
// Item State Toggling
|
||||||
|
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||||
|
|
||||||
|
// Short and Long Rest
|
||||||
|
html.find(".short-rest").click(this._onShortRest.bind(this));
|
||||||
|
html.find(".long-rest").click(this._onLongRest.bind(this));
|
||||||
|
|
||||||
|
// Rollable sheet actions
|
||||||
|
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle mouse click events for character sheet actions
|
||||||
|
* @param {MouseEvent} event The originating click event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onSheetAction(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const button = event.currentTarget;
|
||||||
|
switch (button.dataset.action) {
|
||||||
|
case "rollDeathSave":
|
||||||
|
return this.actor.rollDeathSave({event: event});
|
||||||
|
case "rollInitiative":
|
||||||
|
return this.actor.rollInitiative({createCombatants: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle toggling the state of an Owned Item within the Actor
|
||||||
|
* @param {Event} event The triggering click event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onToggleItem(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||||
|
const item = this.actor.items.get(itemId);
|
||||||
|
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
||||||
|
return item.update({[attr]: !getProperty(item.data, attr)});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a short rest, calling the relevant function on the Actor instance
|
||||||
|
* @param {Event} event The triggering click event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _onShortRest(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
await this._onSubmit(event);
|
||||||
|
return this.actor.shortRest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a long rest, calling the relevant function on the Actor instance
|
||||||
|
* @param {Event} event The triggering click event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _onLongRest(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
await this._onSubmit(event);
|
||||||
|
return this.actor.longRest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
async _onDropItemCreate(itemData) {
|
||||||
|
// 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;
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default drop handling if levels were not added
|
||||||
|
return super._onDropItemCreate(itemData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,130 +6,139 @@ import ActorSheet5e from "./base.js";
|
||||||
* @extends {ActorSheet5e}
|
* @extends {ActorSheet5e}
|
||||||
*/
|
*/
|
||||||
export default class ActorSheet5eNPC extends ActorSheet5e {
|
export default class ActorSheet5eNPC extends ActorSheet5e {
|
||||||
|
/** @override */
|
||||||
/** @override */
|
static get defaultOptions() {
|
||||||
static get defaultOptions() {
|
return mergeObject(super.defaultOptions, {
|
||||||
return mergeObject(super.defaultOptions, {
|
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
width: 600,
|
||||||
width: 600,
|
height: 680
|
||||||
height: 680
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
static unsupportedItemTypes = new Set(["class"]);
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 || CONST.DEFAULT_TOKEN;
|
|
||||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
|
||||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
|
||||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
|
||||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
|
||||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
|
||||||
if ( item.type === "power" ) arr[0].push(item);
|
|
||||||
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 */
|
||||||
|
static unsupportedItemTypes = new Set(["class"]);
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @inheritdoc */
|
/**
|
||||||
getData(options) {
|
* Organize Owned Items for rendering the NPC sheet
|
||||||
const data = super.getData(options);
|
* @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"}}
|
||||||
|
};
|
||||||
|
|
||||||
// Challenge Rating
|
// Start by classifying items into groups for rendering
|
||||||
const cr = parseFloat(data.data.details.cr || 0);
|
let [powers, other] = data.items.reduce(
|
||||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
(arr, item) => {
|
||||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||||
|
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||||
|
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||||
|
item.isOnCooldown =
|
||||||
|
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||||
|
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||||
|
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||||
|
if (item.type === "power") arr[0].push(item);
|
||||||
|
else arr[1].push(item);
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
|
[[], []]
|
||||||
|
);
|
||||||
|
|
||||||
// Creature Type
|
// Apply item filters
|
||||||
data.labels["type"] = this.actor.labels.creatureType;
|
powers = this._filterItems(powers, this._filters.powerbook);
|
||||||
return data;
|
other = this._filterItems(other, this._filters.features);
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Organize Powerbook
|
||||||
/* Object Updates */
|
const powerbook = this._preparePowerbook(data, powers);
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
// Organize Features
|
||||||
async _updateObject(event, formData) {
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Format NPC Challenge Rating
|
// Assign and return
|
||||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
data.features = Object.values(features);
|
||||||
let crv = "data.details.cr";
|
data.powerbook = powerbook;
|
||||||
let cr = formData[crv];
|
}
|
||||||
cr = crs[cr] || parseFloat(cr);
|
|
||||||
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
|
||||||
|
|
||||||
// Parent ActorSheet update steps
|
/* -------------------------------------------- */
|
||||||
return super._updateObject(event, formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/** @inheritdoc */
|
||||||
/* Event Listeners and Handlers */
|
getData(options) {
|
||||||
/* -------------------------------------------- */
|
const data = super.getData(options);
|
||||||
|
|
||||||
/** @override */
|
// Challenge Rating
|
||||||
activateListeners(html) {
|
const cr = parseFloat(data.data.details.cr || 0);
|
||||||
super.activateListeners(html);
|
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
||||||
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Creature Type
|
||||||
|
data.labels["type"] = this.actor.labels.creatureType;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/* -------------------------------------------- */
|
||||||
* Handle rolling NPC health values using the provided formula
|
/* Object Updates */
|
||||||
* @param {Event} event The original click event
|
/* -------------------------------------------- */
|
||||||
* @private
|
|
||||||
*/
|
/** @override */
|
||||||
_onRollHPFormula(event) {
|
async _updateObject(event, formData) {
|
||||||
event.preventDefault();
|
// Format NPC Challenge Rating
|
||||||
const formula = this.actor.data.data.attributes.hp.formula;
|
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||||
if ( !formula ) return;
|
let crv = "data.details.cr";
|
||||||
const hp = new Roll(formula).roll().total;
|
let cr = formData[crv];
|
||||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
cr = crs[cr] || parseFloat(cr);
|
||||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||||
}
|
|
||||||
|
// Parent ActorSheet update steps
|
||||||
|
return super._updateObject(event, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* Event Listeners and Handlers */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
activateListeners(html) {
|
||||||
|
super.activateListeners(html);
|
||||||
|
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle rolling NPC health values using the provided formula
|
||||||
|
* @param {Event} event The original click event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onRollHPFormula(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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,411 +6,427 @@ import ActorSheet5e from "./base.js";
|
||||||
* @type {ActorSheet5e}
|
* @type {ActorSheet5e}
|
||||||
*/
|
*/
|
||||||
export default class ActorSheet5eVehicle extends ActorSheet5e {
|
export default class ActorSheet5eVehicle extends ActorSheet5e {
|
||||||
/**
|
/**
|
||||||
* Define default rendering options for the Vehicle sheet.
|
* Define default rendering options for the Vehicle sheet.
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
static get defaultOptions() {
|
static get defaultOptions() {
|
||||||
return mergeObject(super.defaultOptions, {
|
return mergeObject(super.defaultOptions, {
|
||||||
classes: ["sw5e", "sheet", "actor", "vehicle"],
|
classes: ["sw5e", "sheet", "actor", "vehicle"],
|
||||||
width: 605,
|
width: 605,
|
||||||
height: 680
|
height: 680
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
static unsupportedItemTypes = new Set(["class"]);
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
_getMovementSpeed(actorData, largestPrimary=true) {
|
|
||||||
return super._getMovementSpeed(actorData, largestPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 */
|
||||||
|
static unsupportedItemTypes = new Set(["class"]);
|
||||||
|
|
||||||
/**
|
/* -------------------------------------------- */
|
||||||
* 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'),
|
* Creates a new cargo entry for a vehicle Actor.
|
||||||
css: 'item-qty',
|
*/
|
||||||
property: 'data.quantity'
|
static get newCargo() {
|
||||||
}, {
|
return {
|
||||||
label: game.i18n.localize('SW5E.AC'),
|
name: "",
|
||||||
css: 'item-ac',
|
quantity: 1
|
||||||
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'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Classify items owned by the vehicle and compute total cargo weight
|
|
||||||
let totalWeight = 0;
|
|
||||||
for (const item of data.items) {
|
|
||||||
this._prepareCrewedItem(item);
|
|
||||||
|
|
||||||
// Handle cargo explicitly
|
|
||||||
const isCargo = item.flags.sw5e?.vehicleCargo === true;
|
|
||||||
if ( isCargo ) {
|
|
||||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
|
||||||
cargo.cargo.items.push(item);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle non-cargo item types
|
|
||||||
switch ( item.type ) {
|
|
||||||
case "weapon":
|
|
||||||
features.weapons.items.push(item);
|
|
||||||
break;
|
|
||||||
case "equipment":
|
|
||||||
features.equipment.items.push(item);
|
|
||||||
break;
|
|
||||||
case "feat":
|
|
||||||
if (!item.data.activation.type || (item.data.activation.type === "none")) features.passive.items.push(item);
|
|
||||||
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
|
|
||||||
else features.actions.items.push(item);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
|
||||||
cargo.cargo.items.push(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the rendering context data
|
/* -------------------------------------------- */
|
||||||
data.features = Object.values(features);
|
|
||||||
data.cargo = Object.values(cargo);
|
|
||||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
/* Event Listeners and Handlers */
|
* 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;
|
||||||
|
|
||||||
/** @override */
|
// Vehicle weights are an order of magnitude greater.
|
||||||
activateListeners(html) {
|
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
|
||||||
super.activateListeners(html);
|
|
||||||
if (!this.isEditable) return;
|
|
||||||
|
|
||||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
// Compute overall encumbrance
|
||||||
html.find('.item-hp input')
|
const max = actorData.data.attributes.capacity.cargo;
|
||||||
.click(evt => evt.target.select())
|
const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
|
||||||
.change(this._onHPChange.bind(this));
|
return {value: totalWeight.toNearest(0.1), max, pct};
|
||||||
|
|
||||||
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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
|
||||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super._onItemDelete(event);
|
/* -------------------------------------------- */
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/** @override */
|
||||||
|
_getMovementSpeed(actorData, largestPrimary = true) {
|
||||||
|
return super._getMovementSpeed(actorData, largestPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
/** @override */
|
/* -------------------------------------------- */
|
||||||
async _onDropItemCreate(itemData) {
|
|
||||||
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
|
|
||||||
const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
|
|
||||||
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
|
|
||||||
return super._onDropItemCreate(itemData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
|
* 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
|
||||||
* Special handling for editing HP to clamp it within appropriate range.
|
if (item.type === "feat" && item.data.activation.type === "crew") {
|
||||||
* @param event {Event}
|
item.crew = item.data.activation.cost;
|
||||||
* @returns {Promise<Item>}
|
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
|
||||||
* @private
|
if (item.data.cover === 0.5) item.cover = "½";
|
||||||
*/
|
else if (item.data.cover === 0.75) item.cover = "¾";
|
||||||
_onHPChange(event) {
|
else if (item.data.cover === null) item.cover = "—";
|
||||||
event.preventDefault();
|
if (item.crew < 1 || item.crew === null) item.crew = "—";
|
||||||
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});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Prepare vehicle weapons
|
||||||
|
if (item.type === "equipment" || item.type === "weapon") {
|
||||||
|
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/* -------------------------------------------- */
|
||||||
* Handle toggling an item's crewed status.
|
|
||||||
* @param event {Event}
|
/**
|
||||||
* @returns {Promise<Item>}
|
* Organize Owned Items for rendering the Vehicle sheet.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_onToggleItem(event) {
|
_prepareItems(data) {
|
||||||
event.preventDefault();
|
const cargoColumns = [
|
||||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
{
|
||||||
const item = this.actor.items.get(itemID);
|
label: game.i18n.localize("SW5E.Quantity"),
|
||||||
const crewed = !!item.data.data.crewed;
|
css: "item-qty",
|
||||||
return item.update({'data.crewed': !crewed});
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Classify items owned by the vehicle and compute total cargo weight
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (const item of data.items) {
|
||||||
|
this._prepareCrewedItem(item);
|
||||||
|
|
||||||
|
// Handle cargo explicitly
|
||||||
|
const isCargo = item.flags.sw5e?.vehicleCargo === true;
|
||||||
|
if (isCargo) {
|
||||||
|
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||||
|
cargo.cargo.items.push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-cargo item types
|
||||||
|
switch (item.type) {
|
||||||
|
case "weapon":
|
||||||
|
features.weapons.items.push(item);
|
||||||
|
break;
|
||||||
|
case "equipment":
|
||||||
|
features.equipment.items.push(item);
|
||||||
|
break;
|
||||||
|
case "feat":
|
||||||
|
if (!item.data.activation.type || item.data.activation.type === "none")
|
||||||
|
features.passive.items.push(item);
|
||||||
|
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
|
||||||
|
else features.actions.items.push(item);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||||
|
cargo.cargo.items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the rendering context data
|
||||||
|
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.isEditable) 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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||||
|
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||||
|
}
|
||||||
|
|
||||||
|
return super._onItemDelete(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
async _onDropItemCreate(itemData) {
|
||||||
|
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
|
||||||
|
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
|
||||||
|
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
|
||||||
|
return super._onDropItemCreate(itemData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,220 +3,225 @@
|
||||||
* @type {Dialog}
|
* @type {Dialog}
|
||||||
*/
|
*/
|
||||||
export default class AbilityUseDialog extends Dialog {
|
export default class AbilityUseDialog extends Dialog {
|
||||||
constructor(item, dialogData={}, options={}) {
|
constructor(item, dialogData = {}, options = {}) {
|
||||||
super(dialogData, options);
|
super(dialogData, options);
|
||||||
this.options.classes = ["sw5e", "dialog"];
|
this.options.classes = ["sw5e", "dialog"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a reference to the Item entity being used
|
||||||
|
* @type {Item5e}
|
||||||
|
*/
|
||||||
|
this.item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* Rendering */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a reference to the Item entity being used
|
* A constructor function which displays the Power Cast Dialog app for a given Actor and Item.
|
||||||
* @type {Item5e}
|
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
|
||||||
|
* @param {Item5e} item
|
||||||
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
this.item = item;
|
static async create(item) {
|
||||||
}
|
if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item");
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Prepare data
|
||||||
/* Rendering */
|
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;
|
||||||
|
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
|
||||||
|
|
||||||
/**
|
// Prepare dialog form data
|
||||||
* A constructor function which displays the Power Cast Dialog app for a given Actor and Item.
|
const data = {
|
||||||
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
|
item: item.data,
|
||||||
* @param {Item5e} item
|
title: game.i18n.format("SW5E.AbilityUseHint", {
|
||||||
* @return {Promise}
|
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||||
*/
|
name: item.name
|
||||||
static async create(item) {
|
}),
|
||||||
if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
|
note: this._getAbilityUseNote(item.data, uses, recharge),
|
||||||
|
consumePowerSlot: false,
|
||||||
|
consumeRecharge: recharges,
|
||||||
|
consumeResource: !!itemData.consume.target,
|
||||||
|
consumeUses: uses.per && uses.max > 0,
|
||||||
|
canUse: recharges ? recharge.charged : sufficientUses,
|
||||||
|
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
if (item.data.type === "power") this._getPowerData(actorData, itemData, data);
|
||||||
|
|
||||||
// Prepare data
|
// Render the ability usage template
|
||||||
const actorData = item.actor.data.data;
|
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
|
||||||
const itemData = item.data.data;
|
|
||||||
const uses = itemData.uses || {};
|
|
||||||
const quantity = itemData.quantity || 0;
|
|
||||||
const recharge = itemData.recharge || {};
|
|
||||||
const recharges = !!recharge.value;
|
|
||||||
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
|
|
||||||
|
|
||||||
// Prepare dialog form data
|
// Create the Dialog and return data as a Promise
|
||||||
const data = {
|
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
|
||||||
item: item.data,
|
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
|
||||||
title: game.i18n.format("SW5E.AbilityUseHint", {type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), name: item.name}),
|
return new Promise((resolve) => {
|
||||||
note: this._getAbilityUseNote(item.data, uses, recharge),
|
const dlg = new this(item, {
|
||||||
consumePowerSlot: false,
|
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
|
||||||
consumeRecharge: recharges,
|
content: html,
|
||||||
consumeResource: !!itemData.consume.target,
|
buttons: {
|
||||||
consumeUses: uses.per && (uses.max > 0),
|
use: {
|
||||||
canUse: recharges ? recharge.charged : sufficientUses,
|
icon: `<i class="fas ${icon}"></i>`,
|
||||||
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
label: label,
|
||||||
errors: []
|
callback: (html) => {
|
||||||
};
|
const fd = new FormDataExtended(html[0].querySelector("form"));
|
||||||
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
|
resolve(fd.toObject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
default: "use",
|
||||||
|
close: () => resolve(null)
|
||||||
|
});
|
||||||
|
dlg.render(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Render the ability usage template
|
/* -------------------------------------------- */
|
||||||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
|
/* Helpers */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
// Create the Dialog and return data as a Promise
|
/**
|
||||||
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
|
* Get dialog data related to limited power slots
|
||||||
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
|
* @private
|
||||||
return new Promise((resolve) => {
|
*/
|
||||||
const dlg = new this(item, {
|
static _getPowerData(actorData, itemData, data) {
|
||||||
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
|
// Determine whether the power may be up-cast
|
||||||
content: html,
|
const lvl = itemData.level;
|
||||||
buttons: {
|
const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||||
use: {
|
|
||||||
icon: `<i class="fas ${icon}"></i>`,
|
// If can't upcast, return early and don't bother calculating available power slots
|
||||||
label: label,
|
if (!consumePowerSlot) {
|
||||||
callback: html => {
|
mergeObject(data, {isPower: true, consumePowerSlot});
|
||||||
const fd = new FormDataExtended(html[0].querySelector("form"));
|
return;
|
||||||
resolve(fd.toObject());
|
}
|
||||||
|
|
||||||
|
// Determine the levels which are feasible
|
||||||
|
let lmax = 0;
|
||||||
|
let points;
|
||||||
|
let powerType;
|
||||||
|
switch (itemData.school) {
|
||||||
|
case "lgt":
|
||||||
|
case "uni":
|
||||||
|
case "drk": {
|
||||||
|
powerType = "force";
|
||||||
|
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tec": {
|
||||||
|
powerType = "tech";
|
||||||
|
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
default: "use",
|
|
||||||
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 consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
|
||||||
|
|
||||||
// If can't upcast, return early and don't bother calculating available power slots
|
|
||||||
if (!consumePowerSlot) {
|
|
||||||
mergeObject(data, { isPower: true, consumePowerSlot });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the levels which are feasible
|
|
||||||
let lmax = 0;
|
|
||||||
let points;
|
|
||||||
let powerType;
|
|
||||||
switch (itemData.school){
|
|
||||||
case "lgt":
|
|
||||||
case "uni":
|
|
||||||
case "drk": {
|
|
||||||
powerType = "force"
|
|
||||||
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "tec": {
|
|
||||||
powerType = "tech"
|
|
||||||
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eliminate point usage for innate casters
|
|
||||||
if (actorData.attributes.powercasting === 'innate') points = 999;
|
|
||||||
|
|
||||||
|
|
||||||
let powerLevels
|
|
||||||
if (powerType === "force"){
|
|
||||||
powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
|
||||||
if ( i < lvl ) return arr;
|
|
||||||
const label = CONFIG.SW5E.powerLevels[i];
|
|
||||||
const l = actorData.powers["power"+i] || {fmax: 0, foverride: null};
|
|
||||||
let max = parseInt(l.foverride || l.fmax || 0);
|
|
||||||
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
|
|
||||||
if ( max > 0 ) lmax = i;
|
|
||||||
if ((max > 0) && (slots > 0) && (points > i)){
|
|
||||||
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);
|
// eliminate point usage for innate casters
|
||||||
}else if (powerType === "tech"){
|
if (actorData.attributes.powercasting === "innate") points = 999;
|
||||||
powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
|
||||||
if ( i < lvl ) return arr;
|
let powerLevels;
|
||||||
const label = CONFIG.SW5E.powerLevels[i];
|
if (powerType === "force") {
|
||||||
const l = actorData.powers["power"+i] || {tmax: 0, toverride: null};
|
powerLevels = Array.fromRange(10)
|
||||||
let max = parseInt(l.override || l.tmax || 0);
|
.reduce((arr, i) => {
|
||||||
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
|
if (i < lvl) return arr;
|
||||||
if ( max > 0 ) lmax = i;
|
const label = CONFIG.SW5E.powerLevels[i];
|
||||||
if ((max > 0) && (slots > 0) && (points > i)){
|
const l = actorData.powers["power" + i] || {fmax: 0, foverride: null};
|
||||||
arr.push({
|
let max = parseInt(l.foverride || l.fmax || 0);
|
||||||
level: i,
|
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
|
||||||
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
|
if (max > 0) lmax = i;
|
||||||
canCast: max > 0,
|
if (max > 0 && slots > 0 && points > i) {
|
||||||
hasSlots: slots > 0
|
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);
|
||||||
|
} else if (powerType === "tech") {
|
||||||
|
powerLevels = Array.fromRange(10)
|
||||||
|
.reduce((arr, i) => {
|
||||||
|
if (i < lvl) return arr;
|
||||||
|
const label = CONFIG.SW5E.powerLevels[i];
|
||||||
|
const l = actorData.powers["power" + i] || {tmax: 0, toverride: null};
|
||||||
|
let max = parseInt(l.override || l.tmax || 0);
|
||||||
|
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
|
||||||
|
if (max > 0) lmax = i;
|
||||||
|
if (max > 0 && slots > 0 && points > i) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
return arr;
|
|
||||||
}, []).filter(sl => sl.level <= lmax);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const canCast = powerLevels.some(l => l.hasSlots);
|
const canCast = powerLevels.some((l) => l.hasSlots);
|
||||||
if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", {
|
if (!canCast)
|
||||||
level: CONFIG.SW5E.powerLevels[lvl],
|
data.errors.push(
|
||||||
name: data.item.name
|
game.i18n.format("SW5E.PowerCastNoSlots", {
|
||||||
}));
|
level: CONFIG.SW5E.powerLevels[lvl],
|
||||||
|
name: data.item.name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Merge power casting data
|
// Merge power casting data
|
||||||
return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
|
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does not use any resource
|
/* -------------------------------------------- */
|
||||||
if ( !uses.per || !uses.max ) return "";
|
|
||||||
|
|
||||||
// Consumables
|
/**
|
||||||
if ( item.type === "consumable" ) {
|
* Get the ability usage note that is displayed
|
||||||
let str = "SW5E.AbilityUseNormalHint";
|
* @private
|
||||||
if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint";
|
*/
|
||||||
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
|
static _getAbilityUseNote(item, uses, recharge) {
|
||||||
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
|
// Zero quantity
|
||||||
return game.i18n.format(str, {
|
const quantity = item.data.quantity;
|
||||||
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
|
if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
|
||||||
value: uses.value,
|
|
||||||
quantity: item.data.quantity,
|
|
||||||
max: uses.max,
|
|
||||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other Items
|
// Abilities which use Recharge
|
||||||
else {
|
if (!!recharge.value) {
|
||||||
return game.i18n.format("SW5E.AbilityUseNormalHint", {
|
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
|
||||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
|
||||||
value: uses.value,
|
});
|
||||||
max: uses.max,
|
}
|
||||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
|
||||||
});
|
// 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: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
|
||||||
|
value: uses.value,
|
||||||
|
quantity: item.data.quantity,
|
||||||
|
max: uses.max,
|
||||||
|
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other Items
|
||||||
|
else {
|
||||||
|
return game.i18n.format("SW5E.AbilityUseNormalHint", {
|
||||||
|
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||||
|
value: uses.value,
|
||||||
|
max: uses.max,
|
||||||
|
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,135 +3,137 @@
|
||||||
* @implements {DocumentSheet}
|
* @implements {DocumentSheet}
|
||||||
*/
|
*/
|
||||||
export default class ActorSheetFlags extends DocumentSheet {
|
export default class ActorSheetFlags extends DocumentSheet {
|
||||||
static get defaultOptions() {
|
static get defaultOptions() {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
id: "actor-flags",
|
id: "actor-flags",
|
||||||
classes: ["sw5e"],
|
classes: ["sw5e"],
|
||||||
template: "systems/sw5e/templates/apps/actor-flags.html",
|
template: "systems/sw5e/templates/apps/actor-flags.html",
|
||||||
width: 500,
|
width: 500,
|
||||||
closeOnSubmit: true
|
closeOnSubmit: true
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
get title() {
|
|
||||||
return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
getData() {
|
|
||||||
const data = {};
|
|
||||||
data.actor = this.object;
|
|
||||||
data.classes = this._getClasses();
|
|
||||||
data.flags = this._getFlags();
|
|
||||||
data.bonuses = this._getBonuses();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare an object of sorted classes.
|
|
||||||
* @return {object}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_getClasses() {
|
|
||||||
const classes = this.object.items.filter(i => i.type === "class");
|
|
||||||
return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => {
|
|
||||||
obj[i.id] = i.name;
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare an object of flags data which groups flags by section
|
|
||||||
* Add some additional data for rendering
|
|
||||||
* @return {object}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_getFlags() {
|
|
||||||
const flags = {};
|
|
||||||
const baseData = this.document.toJSON();
|
|
||||||
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
|
|
||||||
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
|
|
||||||
let flag = foundry.utils.deepClone(v);
|
|
||||||
flag.type = v.type.name;
|
|
||||||
flag.isCheckbox = v.type === Boolean;
|
|
||||||
flag.isSelect = v.hasOwnProperty('choices');
|
|
||||||
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
|
|
||||||
flags[v.section][`flags.sw5e.${k}`] = flag;
|
|
||||||
}
|
}
|
||||||
return flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/** @override */
|
||||||
* Get the bonuses fields and their localization strings
|
get title() {
|
||||||
* @return {Array<object>}
|
return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_getBonuses() {
|
|
||||||
const bonuses = [
|
|
||||||
{name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"},
|
|
||||||
{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.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"},
|
|
||||||
{name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"},
|
|
||||||
{name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"},
|
|
||||||
{name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"},
|
|
||||||
{name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
|
|
||||||
{name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
|
|
||||||
];
|
|
||||||
for ( let b of bonuses ) {
|
|
||||||
b.value = getProperty(this.object._data, b.name) || "";
|
|
||||||
}
|
}
|
||||||
return bonuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
async _updateObject(event, formData) {
|
getData() {
|
||||||
const actor = this.object;
|
const data = {};
|
||||||
let updateData = expandObject(formData);
|
data.actor = this.object;
|
||||||
|
data.classes = this._getClasses();
|
||||||
|
data.flags = this._getFlags();
|
||||||
|
data.bonuses = this._getBonuses();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
// Unset any flags which are "false"
|
/* -------------------------------------------- */
|
||||||
let unset = false;
|
|
||||||
const flags = updateData.flags.sw5e;
|
/**
|
||||||
//clone flags to dnd5e for module compatability
|
* Prepare an object of sorted classes.
|
||||||
updateData.flags.dnd5e = updateData.flags.sw5e
|
* @return {object}
|
||||||
for ( let [k, v] of Object.entries(flags) ) {
|
* @private
|
||||||
if ( [undefined, null, "", false, 0].includes(v) ) {
|
*/
|
||||||
delete flags[k];
|
_getClasses() {
|
||||||
if ( hasProperty(actor._data.flags, `sw5e.${k}`) ) {
|
const classes = this.object.items.filter((i) => i.type === "class");
|
||||||
unset = true;
|
return classes
|
||||||
flags[`-=${k}`] = null;
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.reduce((obj, i) => {
|
||||||
|
obj[i.id] = i.name;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare an object of flags data which groups flags by section
|
||||||
|
* Add some additional data for rendering
|
||||||
|
* @return {object}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getFlags() {
|
||||||
|
const flags = {};
|
||||||
|
const baseData = this.document.toJSON();
|
||||||
|
for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) {
|
||||||
|
if (!flags.hasOwnProperty(v.section)) flags[v.section] = {};
|
||||||
|
let flag = foundry.utils.deepClone(v);
|
||||||
|
flag.type = v.type.name;
|
||||||
|
flag.isCheckbox = v.type === Boolean;
|
||||||
|
flag.isSelect = v.hasOwnProperty("choices");
|
||||||
|
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
|
||||||
|
flags[v.section][`flags.sw5e.${k}`] = flag;
|
||||||
}
|
}
|
||||||
}
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
* Get the bonuses fields and their localization strings
|
||||||
}
|
* @return {Array<object>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getBonuses() {
|
||||||
|
const bonuses = [
|
||||||
|
{name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"},
|
||||||
|
{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.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"},
|
||||||
|
{name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"},
|
||||||
|
{name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"},
|
||||||
|
{name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"},
|
||||||
|
{name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
|
||||||
|
{name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
|
||||||
|
];
|
||||||
|
for (let b of bonuses) {
|
||||||
|
b.value = getProperty(this.object._data, b.name) || "";
|
||||||
|
}
|
||||||
|
return bonuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff the data against any applied overrides and apply
|
/* -------------------------------------------- */
|
||||||
await actor.update(updateData, {diff: false});
|
|
||||||
}
|
/** @override */
|
||||||
|
async _updateObject(event, formData) {
|
||||||
|
const actor = this.object;
|
||||||
|
let updateData = expandObject(formData);
|
||||||
|
|
||||||
|
// Unset any flags which are "false"
|
||||||
|
let unset = false;
|
||||||
|
const flags = updateData.flags.sw5e;
|
||||||
|
//clone flags to dnd5e for module compatability
|
||||||
|
updateData.flags.dnd5e = updateData.flags.sw5e;
|
||||||
|
for (let [k, v] of Object.entries(flags)) {
|
||||||
|
if ([undefined, null, "", false, 0].includes(v)) {
|
||||||
|
delete flags[k];
|
||||||
|
if (hasProperty(actor._data.flags, `sw5e.${k}`)) {
|
||||||
|
unset = true;
|
||||||
|
flags[`-=${k}`] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
await actor.update(updateData, {diff: false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import Actor5e from "../actor/entity.js";
|
||||||
* @extends {FormApplication}
|
* @extends {FormApplication}
|
||||||
*/
|
*/
|
||||||
export default class ActorTypeConfig extends FormApplication {
|
export default class ActorTypeConfig extends FormApplication {
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
static get defaultOptions() {
|
static get defaultOptions() {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
|
@ -32,23 +31,23 @@ export default class ActorTypeConfig extends FormApplication {
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
getData(options) {
|
getData(options) {
|
||||||
|
|
||||||
// Get current value or new default
|
// Get current value or new default
|
||||||
let attr = foundry.utils.getProperty(this.object.data.data, 'details.type');
|
let attr = foundry.utils.getProperty(this.object.data.data, "details.type");
|
||||||
if ( foundry.utils.getType(attr) !== "Object" ) attr = {
|
if (foundry.utils.getType(attr) !== "Object")
|
||||||
value: (attr in CONFIG.SW5E.creatureTypes) ? attr : "humanoid",
|
attr = {
|
||||||
subtype: "",
|
value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid",
|
||||||
swarm: "",
|
subtype: "",
|
||||||
custom: ""
|
swarm: "",
|
||||||
};
|
custom: ""
|
||||||
|
};
|
||||||
|
|
||||||
// Populate choices
|
// Populate choices
|
||||||
const types = {};
|
const types = {};
|
||||||
for ( let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes) ) {
|
for (let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes)) {
|
||||||
types[k] = {
|
types[k] = {
|
||||||
label: game.i18n.localize(v),
|
label: game.i18n.localize(v),
|
||||||
chosen: attr.value === k
|
chosen: attr.value === k
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return data for rendering
|
// Return data for rendering
|
||||||
|
@ -61,12 +60,14 @@ export default class ActorTypeConfig extends FormApplication {
|
||||||
},
|
},
|
||||||
subtype: attr.subtype,
|
subtype: attr.subtype,
|
||||||
swarm: attr.swarm,
|
swarm: attr.swarm,
|
||||||
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)).reverse().reduce((obj, e) => {
|
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes))
|
||||||
obj[e[0]] = e[1];
|
.reverse()
|
||||||
return obj;
|
.reduce((obj, e) => {
|
||||||
}, {}),
|
obj[e[0]] = e[1];
|
||||||
|
return obj;
|
||||||
|
}, {}),
|
||||||
preview: Actor5e.formatCreatureType(attr) || "–"
|
preview: Actor5e.formatCreatureType(attr) || "–"
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -74,7 +75,7 @@ export default class ActorTypeConfig extends FormApplication {
|
||||||
/** @override */
|
/** @override */
|
||||||
async _updateObject(event, formData) {
|
async _updateObject(event, formData) {
|
||||||
const typeObject = foundry.utils.expandObject(formData);
|
const typeObject = foundry.utils.expandObject(formData);
|
||||||
return this.object.update({ 'data.details.type': typeObject });
|
return this.object.update({"data.details.type": typeObject});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
* @implements {DocumentSheet}
|
* @implements {DocumentSheet}
|
||||||
*/
|
*/
|
||||||
export default class ActorHitDiceConfig extends DocumentSheet {
|
export default class ActorHitDiceConfig extends DocumentSheet {
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
static get defaultOptions() {
|
static get defaultOptions() {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
|
@ -26,20 +25,22 @@ export default class ActorHitDiceConfig extends DocumentSheet {
|
||||||
/** @override */
|
/** @override */
|
||||||
getData(options) {
|
getData(options) {
|
||||||
return {
|
return {
|
||||||
classes: this.object.items.reduce((classes, item) => {
|
classes: this.object.items
|
||||||
if (item.data.type === "class") {
|
.reduce((classes, item) => {
|
||||||
// Add the appropriate data only if this item is a "class"
|
if (item.data.type === "class") {
|
||||||
classes.push({
|
// Add the appropriate data only if this item is a "class"
|
||||||
classItemId: item.data._id,
|
classes.push({
|
||||||
name: item.data.name,
|
classItemId: item.data._id,
|
||||||
diceDenom: item.data.data.hitDice,
|
name: item.data.name,
|
||||||
currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
|
diceDenom: item.data.data.hitDice,
|
||||||
maxHitDice: item.data.data.levels,
|
currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
|
||||||
canRoll: (item.data.data.levels - item.data.data.hitDiceUsed) > 0
|
maxHitDice: item.data.data.levels,
|
||||||
});
|
canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0
|
||||||
}
|
});
|
||||||
return classes;
|
}
|
||||||
}, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
|
return classes;
|
||||||
|
}, [])
|
||||||
|
.sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ export default class ActorHitDiceConfig extends DocumentSheet {
|
||||||
super.activateListeners(html);
|
super.activateListeners(html);
|
||||||
|
|
||||||
// Hook up -/+ buttons to adjust the current value in the form
|
// Hook up -/+ buttons to adjust the current value in the form
|
||||||
html.find("button.increment,button.decrement").click(event => {
|
html.find("button.increment,button.decrement").click((event) => {
|
||||||
const button = event.currentTarget;
|
const button = event.currentTarget;
|
||||||
const current = button.parentElement.querySelector(".current");
|
const current = button.parentElement.querySelector(".current");
|
||||||
const max = button.parentElement.querySelector(".max");
|
const max = button.parentElement.querySelector(".max");
|
||||||
|
@ -67,8 +68,8 @@ export default class ActorHitDiceConfig extends DocumentSheet {
|
||||||
async _updateObject(event, formData) {
|
async _updateObject(event, formData) {
|
||||||
const actorItems = this.object.items;
|
const actorItems = this.object.items;
|
||||||
const classUpdates = Object.entries(formData).map(([id, hd]) => ({
|
const classUpdates = Object.entries(formData).map(([id, hd]) => ({
|
||||||
_id: id,
|
"_id": id,
|
||||||
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd,
|
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd
|
||||||
}));
|
}));
|
||||||
return this.object.updateEmbeddedDocuments("Item", classUpdates);
|
return this.object.updateEmbeddedDocuments("Item", classUpdates);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,65 +3,65 @@
|
||||||
* @extends {Dialog}
|
* @extends {Dialog}
|
||||||
*/
|
*/
|
||||||
export default class LongRestDialog extends Dialog {
|
export default class LongRestDialog extends Dialog {
|
||||||
constructor(actor, dialogData = {}, options = {}) {
|
constructor(actor, dialogData = {}, options = {}) {
|
||||||
super(dialogData, options);
|
super(dialogData, options);
|
||||||
this.actor = actor;
|
this.actor = actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
static get defaultOptions() {
|
static get defaultOptions() {
|
||||||
return mergeObject(super.defaultOptions, {
|
return mergeObject(super.defaultOptions, {
|
||||||
template: "systems/sw5e/templates/apps/long-rest.html",
|
template: "systems/sw5e/templates/apps/long-rest.html",
|
||||||
classes: ["sw5e", "dialog"]
|
classes: ["sw5e", "dialog"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
getData() {
|
getData() {
|
||||||
const data = super.getData();
|
const data = super.getData();
|
||||||
const variant = game.settings.get("sw5e", "restVariant");
|
const variant = game.settings.get("sw5e", "restVariant");
|
||||||
data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
|
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)
|
data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
|
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
|
||||||
* workflow has been resolved.
|
* workflow has been resolved.
|
||||||
* @param {Actor5e} actor
|
* @param {Actor5e} actor
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
static async longRestDialog({ actor } = {}) {
|
static async longRestDialog({actor} = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const dlg = new this(actor, {
|
const dlg = new this(actor, {
|
||||||
title: game.i18n.localize("SW5E.LongRest"),
|
title: game.i18n.localize("SW5E.LongRest"),
|
||||||
buttons: {
|
buttons: {
|
||||||
rest: {
|
rest: {
|
||||||
icon: '<i class="fas fa-bed"></i>',
|
icon: '<i class="fas fa-bed"></i>',
|
||||||
label: game.i18n.localize("SW5E.Rest"),
|
label: game.i18n.localize("SW5E.Rest"),
|
||||||
callback: html => {
|
callback: (html) => {
|
||||||
let newDay = true;
|
let newDay = true;
|
||||||
if (game.settings.get("sw5e", "restVariant") !== "gritty")
|
if (game.settings.get("sw5e", "restVariant") !== "gritty")
|
||||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||||
resolve(newDay);
|
resolve(newDay);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
icon: '<i class="fas fa-times"></i>',
|
icon: '<i class="fas fa-times"></i>',
|
||||||
label: game.i18n.localize("Cancel"),
|
label: game.i18n.localize("Cancel"),
|
||||||
callback: reject
|
callback: reject
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
default: 'rest',
|
default: "rest",
|
||||||
close: reject
|
close: reject
|
||||||
});
|
});
|
||||||
dlg.render(true);
|
dlg.render(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,37 +3,36 @@
|
||||||
* @extends {DocumentSheet}
|
* @extends {DocumentSheet}
|
||||||
*/
|
*/
|
||||||
export default class ActorMovementConfig extends DocumentSheet {
|
export default class ActorMovementConfig extends DocumentSheet {
|
||||||
|
/** @override */
|
||||||
/** @override */
|
static get defaultOptions() {
|
||||||
static get defaultOptions() {
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
classes: ["sw5e"],
|
||||||
classes: ["sw5e"],
|
template: "systems/sw5e/templates/apps/movement-config.html",
|
||||||
template: "systems/sw5e/templates/apps/movement-config.html",
|
width: 300,
|
||||||
width: 300,
|
height: "auto"
|
||||||
height: "auto"
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
/** @override */
|
||||||
/** @override */
|
get title() {
|
||||||
get title() {
|
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
|
||||||
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
|
}
|
||||||
}
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
/** @override */
|
||||||
/** @override */
|
getData(options) {
|
||||||
getData(options) {
|
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
|
||||||
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
|
const data = {
|
||||||
const data = {
|
movement: foundry.utils.deepClone(sourceMovement),
|
||||||
movement: foundry.utils.deepClone(sourceMovement),
|
units: CONFIG.SW5E.movementUnits
|
||||||
units: CONFIG.SW5E.movementUnits
|
};
|
||||||
};
|
for (let [k, v] of Object.entries(data.movement)) {
|
||||||
for ( let [k, v] of Object.entries(data.movement) ) {
|
if (["units", "hover"].includes(k)) continue;
|
||||||
if ( ["units", "hover"].includes(k) ) continue;
|
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
|
||||||
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
|
}
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* @type {Dialog}
|
* @type {Dialog}
|
||||||
*/
|
*/
|
||||||
export default class SelectItemsPrompt extends Dialog {
|
export default class SelectItemsPrompt extends Dialog {
|
||||||
constructor(items, dialogData={}, options={}) {
|
constructor(items, dialogData = {}, options = {}) {
|
||||||
super(dialogData, options);
|
super(dialogData, options);
|
||||||
this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"];
|
this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"];
|
||||||
|
|
||||||
|
@ -18,11 +18,11 @@ export default class SelectItemsPrompt extends Dialog {
|
||||||
super.activateListeners(html);
|
super.activateListeners(html);
|
||||||
|
|
||||||
// render the item's sheet if its image is clicked
|
// render the item's sheet if its image is clicked
|
||||||
html.on('click', '.item-image', (event) => {
|
html.on("click", ".item-image", (event) => {
|
||||||
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
|
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
|
||||||
|
|
||||||
item?.sheet.render(true);
|
item?.sheet.render(true);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,29 +33,27 @@ export default class SelectItemsPrompt extends Dialog {
|
||||||
* @param {string} options.hint - Localized hint to display at the top of the prompt
|
* @param {string} options.hint - Localized hint to display at the top of the prompt
|
||||||
* @return {Promise<string[]>} - list of item ids which the user has selected
|
* @return {Promise<string[]>} - list of item ids which the user has selected
|
||||||
*/
|
*/
|
||||||
static async create(items, {
|
static async create(items, {hint}) {
|
||||||
hint
|
|
||||||
}) {
|
|
||||||
// Render the ability usage template
|
// Render the ability usage template
|
||||||
const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint});
|
const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint});
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const dlg = new this(items, {
|
const dlg = new this(items, {
|
||||||
title: game.i18n.localize('SW5E.SelectItemsPromptTitle'),
|
title: game.i18n.localize("SW5E.SelectItemsPromptTitle"),
|
||||||
content: html,
|
content: html,
|
||||||
buttons: {
|
buttons: {
|
||||||
apply: {
|
apply: {
|
||||||
icon: `<i class="fas fa-user-plus"></i>`,
|
icon: `<i class="fas fa-user-plus"></i>`,
|
||||||
label: game.i18n.localize('SW5E.Apply'),
|
label: game.i18n.localize("SW5E.Apply"),
|
||||||
callback: html => {
|
callback: (html) => {
|
||||||
const fd = new FormDataExtended(html[0].querySelector("form")).toObject();
|
const fd = new FormDataExtended(html[0].querySelector("form")).toObject();
|
||||||
const selectedIds = Object.keys(fd).filter(itemId => fd[itemId]);
|
const selectedIds = Object.keys(fd).filter((itemId) => fd[itemId]);
|
||||||
resolve(selectedIds);
|
resolve(selectedIds);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
icon: '<i class="fas fa-forward"></i>',
|
icon: '<i class="fas fa-forward"></i>',
|
||||||
label: game.i18n.localize('SW5E.Skip'),
|
label: game.i18n.localize("SW5E.Skip"),
|
||||||
callback: () => resolve([])
|
callback: () => resolve([])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,41 +3,41 @@
|
||||||
* @extends {DocumentSheet}
|
* @extends {DocumentSheet}
|
||||||
*/
|
*/
|
||||||
export default class ActorSensesConfig extends DocumentSheet {
|
export default class ActorSensesConfig extends DocumentSheet {
|
||||||
|
/** @inheritdoc */
|
||||||
/** @inheritdoc */
|
static get defaultOptions() {
|
||||||
static get defaultOptions() {
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
classes: ["sw5e"],
|
||||||
classes: ["sw5e"],
|
template: "systems/sw5e/templates/apps/senses-config.html",
|
||||||
template: "systems/sw5e/templates/apps/senses-config.html",
|
width: 300,
|
||||||
width: 300,
|
height: "auto"
|
||||||
height: "auto"
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
/** @inheritdoc */
|
||||||
/** @inheritdoc */
|
get title() {
|
||||||
get title() {
|
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
|
||||||
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
|
}
|
||||||
}
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
/** @inheritdoc */
|
||||||
/** @inheritdoc */
|
getData(options) {
|
||||||
getData(options) {
|
const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {};
|
||||||
const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {};
|
const data = {
|
||||||
const data = {
|
senses: {},
|
||||||
senses: {},
|
special: senses.special ?? "",
|
||||||
special: senses.special ?? "",
|
units: senses.units,
|
||||||
units: senses.units, movementUnits: CONFIG.SW5E.movementUnits
|
movementUnits: CONFIG.SW5E.movementUnits
|
||||||
};
|
};
|
||||||
for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) {
|
for (let [name, label] of Object.entries(CONFIG.SW5E.senses)) {
|
||||||
const v = senses[name];
|
const v = senses[name];
|
||||||
data.senses[name] = {
|
data.senses[name] = {
|
||||||
label: game.i18n.localize(label),
|
label: game.i18n.localize(label),
|
||||||
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
|
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,129 +5,130 @@ import LongRestDialog from "./long-rest.js";
|
||||||
* @extends {Dialog}
|
* @extends {Dialog}
|
||||||
*/
|
*/
|
||||||
export default class ShortRestDialog extends Dialog {
|
export default class ShortRestDialog extends Dialog {
|
||||||
constructor(actor, dialogData={}, options={}) {
|
constructor(actor, dialogData = {}, options = {}) {
|
||||||
super(dialogData, options);
|
super(dialogData, options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a reference to the Actor entity which is resting
|
* Store a reference to the Actor entity which is resting
|
||||||
* @type {Actor}
|
* @type {Actor}
|
||||||
*/
|
*/
|
||||||
this.actor = actor;
|
this.actor = actor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track the most recently used HD denomination for re-rendering the form
|
* Track the most recently used HD denomination for re-rendering the form
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
this._denom = null;
|
this._denom = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
static get defaultOptions() {
|
static get defaultOptions() {
|
||||||
return mergeObject(super.defaultOptions, {
|
return mergeObject(super.defaultOptions, {
|
||||||
template: "systems/sw5e/templates/apps/short-rest.html",
|
template: "systems/sw5e/templates/apps/short-rest.html",
|
||||||
classes: ["sw5e", "dialog"]
|
classes: ["sw5e", "dialog"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
getData() {
|
getData() {
|
||||||
const data = super.getData();
|
const data = super.getData();
|
||||||
|
|
||||||
// Determine Hit Dice
|
// Determine Hit Dice
|
||||||
data.availableHD = this.actor.data.items.reduce((hd, item) => {
|
data.availableHD = this.actor.data.items.reduce((hd, item) => {
|
||||||
if ( item.type === "class" ) {
|
if (item.type === "class") {
|
||||||
const d = item.data.data;
|
const d = item.data.data;
|
||||||
const denom = d.hitDice || "d6";
|
const denom = d.hitDice || "d6";
|
||||||
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
|
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
|
||||||
hd[denom] = denom in hd ? hd[denom] + available : available;
|
hd[denom] = denom in hd ? hd[denom] + available : available;
|
||||||
}
|
|
||||||
return hd;
|
|
||||||
}, {});
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
activateListeners(html) {
|
|
||||||
super.activateListeners(html);
|
|
||||||
let btn = html.find("#roll-hd");
|
|
||||||
btn.click(this._onRollHitDie.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle rolling a Hit Die as part of a Short Rest action
|
|
||||||
* @param {Event} event The triggering click event
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
async _onRollHitDie(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const btn = event.currentTarget;
|
|
||||||
this._denom = btn.form.hd.value;
|
|
||||||
await this.actor.rollHitDie(this._denom);
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
|
|
||||||
* been resolved.
|
|
||||||
* @param {Actor5e} actor
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
static async shortRestDialog({actor}={}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const dlg = new this(actor, {
|
|
||||||
title: game.i18n.localize("SW5E.ShortRest"),
|
|
||||||
buttons: {
|
|
||||||
rest: {
|
|
||||||
icon: '<i class="fas fa-bed"></i>',
|
|
||||||
label: game.i18n.localize("SW5E.Rest"),
|
|
||||||
callback: html => {
|
|
||||||
let newDay = false;
|
|
||||||
if (game.settings.get("sw5e", "restVariant") === "gritty")
|
|
||||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
|
||||||
resolve(newDay);
|
|
||||||
}
|
}
|
||||||
},
|
return hd;
|
||||||
cancel: {
|
}, {});
|
||||||
icon: '<i class="fas fa-times"></i>',
|
data.canRoll = this.actor.data.data.attributes.hd > 0;
|
||||||
label: game.i18n.localize("Cancel"),
|
data.denomination = this._denom;
|
||||||
callback: reject
|
|
||||||
}
|
|
||||||
},
|
|
||||||
close: reject
|
|
||||||
});
|
|
||||||
dlg.render(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/* -------------------------------------------- */
|
||||||
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
|
|
||||||
* workflow has been resolved.
|
/** @override */
|
||||||
* @deprecated
|
activateListeners(html) {
|
||||||
* @param {Actor5e} actor
|
super.activateListeners(html);
|
||||||
* @return {Promise}
|
let btn = html.find("#roll-hd");
|
||||||
*/
|
btn.click(this._onRollHitDie.bind(this));
|
||||||
static async longRestDialog({actor}={}) {
|
}
|
||||||
console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
|
|
||||||
return LongRestDialog.longRestDialog(...arguments);
|
/* -------------------------------------------- */
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Handle rolling a Hit Die as part of a Short Rest action
|
||||||
|
* @param {Event} event The triggering click event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _onRollHitDie(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const btn = event.currentTarget;
|
||||||
|
this._denom = btn.form.hd.value;
|
||||||
|
await this.actor.rollHitDie(this._denom);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has
|
||||||
|
* been resolved.
|
||||||
|
* @param {Actor5e} actor
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static async shortRestDialog({actor} = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const dlg = new this(actor, {
|
||||||
|
title: game.i18n.localize("SW5E.ShortRest"),
|
||||||
|
buttons: {
|
||||||
|
rest: {
|
||||||
|
icon: '<i class="fas fa-bed"></i>',
|
||||||
|
label: game.i18n.localize("SW5E.Rest"),
|
||||||
|
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: game.i18n.localize("Cancel"),
|
||||||
|
callback: reject
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: reject
|
||||||
|
});
|
||||||
|
dlg.render(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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} = {}) {
|
||||||
|
console.warn(
|
||||||
|
"WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
|
||||||
|
);
|
||||||
|
return LongRestDialog.longRestDialog(...arguments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,86 +3,85 @@
|
||||||
* @extends {DocumentSheet}
|
* @extends {DocumentSheet}
|
||||||
*/
|
*/
|
||||||
export default class TraitSelector extends DocumentSheet {
|
export default class TraitSelector extends DocumentSheet {
|
||||||
|
/** @inheritdoc */
|
||||||
/** @inheritdoc */
|
static get defaultOptions() {
|
||||||
static get defaultOptions() {
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
id: "trait-selector",
|
||||||
id: "trait-selector",
|
classes: ["sw5e", "trait-selector", "subconfig"],
|
||||||
classes: ["sw5e", "trait-selector", "subconfig"],
|
title: "Actor Trait Selection",
|
||||||
title: "Actor Trait Selection",
|
template: "systems/sw5e/templates/apps/trait-selector.html",
|
||||||
template: "systems/sw5e/templates/apps/trait-selector.html",
|
width: 320,
|
||||||
width: 320,
|
height: "auto",
|
||||||
height: "auto",
|
choices: {},
|
||||||
choices: {},
|
allowCustom: true,
|
||||||
allowCustom: true,
|
minimum: 0,
|
||||||
minimum: 0,
|
maximum: null,
|
||||||
maximum: null,
|
valueKey: "value",
|
||||||
valueKey: "value",
|
customKey: "custom"
|
||||||
customKey: "custom"
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a reference to the target attribute
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
get attribute() {
|
|
||||||
return this.options.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
getData() {
|
|
||||||
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
|
|
||||||
const o = this.options;
|
|
||||||
const value = (o.valueKey) ? attr[o.valueKey] ?? [] : attr;
|
|
||||||
const custom = (o.customKey) ? attr[o.customKey] ?? "" : "";
|
|
||||||
|
|
||||||
// Populate choices
|
|
||||||
const choices = Object.entries(o.choices).reduce((obj, e) => {
|
|
||||||
let [k, v] = e;
|
|
||||||
obj[k] = { label: v, chosen: attr ? value.includes(k) : false };
|
|
||||||
return obj;
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
// Return data
|
|
||||||
return {
|
|
||||||
allowCustom: o.allowCustom,
|
|
||||||
choices: choices,
|
|
||||||
custom: custom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
async _updateObject(event, formData) {
|
|
||||||
const o = this.options;
|
|
||||||
|
|
||||||
// Obtain choices
|
|
||||||
const chosen = [];
|
|
||||||
for ( let [k, v] of Object.entries(formData) ) {
|
|
||||||
if ( (k !== "custom") && v ) chosen.push(k);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object including custom data
|
/* -------------------------------------------- */
|
||||||
const updateData = {};
|
|
||||||
if ( o.valueKey ) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
|
|
||||||
else updateData[this.attribute] = chosen;
|
|
||||||
if ( o.allowCustom ) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
|
|
||||||
|
|
||||||
// Validate the number chosen
|
/**
|
||||||
if ( o.minimum && (chosen.length < o.minimum) ) {
|
* Return a reference to the target attribute
|
||||||
return ui.notifications.error(`You must choose at least ${o.minimum} options`);
|
* @type {string}
|
||||||
}
|
*/
|
||||||
if ( o.maximum && (chosen.length > o.maximum) ) {
|
get attribute() {
|
||||||
return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
|
return this.options.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the object
|
/* -------------------------------------------- */
|
||||||
this.object.update(updateData);
|
|
||||||
}
|
/** @override */
|
||||||
|
getData() {
|
||||||
|
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
|
||||||
|
const o = this.options;
|
||||||
|
const value = o.valueKey ? attr[o.valueKey] ?? [] : attr;
|
||||||
|
const custom = o.customKey ? attr[o.customKey] ?? "" : "";
|
||||||
|
|
||||||
|
// Populate choices
|
||||||
|
const choices = Object.entries(o.choices).reduce((obj, e) => {
|
||||||
|
let [k, v] = e;
|
||||||
|
obj[k] = {label: v, chosen: attr ? value.includes(k) : false};
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Return data
|
||||||
|
return {
|
||||||
|
allowCustom: o.allowCustom,
|
||||||
|
choices: choices,
|
||||||
|
custom: custom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
async _updateObject(event, formData) {
|
||||||
|
const o = this.options;
|
||||||
|
|
||||||
|
// Obtain choices
|
||||||
|
const chosen = [];
|
||||||
|
for (let [k, v] of Object.entries(formData)) {
|
||||||
|
if (k !== "custom" && v) chosen.push(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object including custom data
|
||||||
|
const updateData = {};
|
||||||
|
if (o.valueKey) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
|
||||||
|
else updateData[this.attribute] = chosen;
|
||||||
|
if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
|
||||||
|
|
||||||
|
// Validate the number chosen
|
||||||
|
if (o.minimum && chosen.length < o.minimum) {
|
||||||
|
return ui.notifications.error(`You must choose at least ${o.minimum} options`);
|
||||||
|
}
|
||||||
|
if (o.maximum && chosen.length > o.maximum) {
|
||||||
|
return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the object
|
||||||
|
this.object.update(updateData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
/** @override */
|
/** @override */
|
||||||
export const measureDistances = function(segments, options={}) {
|
export const measureDistances = function (segments, options = {}) {
|
||||||
if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
|
if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options);
|
||||||
|
|
||||||
// Track the total number of diagonals
|
// Track the total number of diagonals
|
||||||
let nDiagonal = 0;
|
let nDiagonal = 0;
|
||||||
const rule = this.parent.diagonalRule;
|
const rule = this.parent.diagonalRule;
|
||||||
const d = canvas.dimensions;
|
const d = canvas.dimensions;
|
||||||
|
|
||||||
// Iterate over measured segments
|
// Iterate over measured segments
|
||||||
return segments.map(s => {
|
return segments.map((s) => {
|
||||||
let r = s.ray;
|
let r = s.ray;
|
||||||
|
|
||||||
// Determine the total distance traveled
|
// Determine the total distance traveled
|
||||||
let nx = Math.abs(Math.ceil(r.dx / d.size));
|
let nx = Math.abs(Math.ceil(r.dx / d.size));
|
||||||
let ny = Math.abs(Math.ceil(r.dy / d.size));
|
let ny = Math.abs(Math.ceil(r.dy / d.size));
|
||||||
|
|
||||||
// Determine the number of straight and diagonal moves
|
// Determine the number of straight and diagonal moves
|
||||||
let nd = Math.min(nx, ny);
|
let nd = Math.min(nx, ny);
|
||||||
let ns = Math.abs(ny - nx);
|
let ns = Math.abs(ny - nx);
|
||||||
nDiagonal += nd;
|
nDiagonal += nd;
|
||||||
|
|
||||||
// Alternative DMG Movement
|
// Alternative DMG Movement
|
||||||
if (rule === "5105") {
|
if (rule === "5105") {
|
||||||
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
|
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
|
||||||
let spaces = (nd10 * 2) + (nd - nd10) + ns;
|
let spaces = nd10 * 2 + (nd - nd10) + ns;
|
||||||
return spaces * canvas.dimensions.distance;
|
return spaces * canvas.dimensions.distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Euclidean Measurement
|
// Euclidean Measurement
|
||||||
else if (rule === "EUCL") {
|
else if (rule === "EUCL") {
|
||||||
return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
|
return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard PHB Movement
|
// Standard PHB Movement
|
||||||
else return (ns + nd) * canvas.scene.data.gridDistance;
|
else return (ns + nd) * canvas.scene.data.gridDistance;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,51 +1,51 @@
|
||||||
export default class CharacterImporter {
|
export default class CharacterImporter {
|
||||||
// transform JSON from sw5e.com to Foundry friendly format
|
// transform JSON from sw5e.com to Foundry friendly format
|
||||||
// and insert new actor
|
// and insert new actor
|
||||||
static async transform(rawCharacter) {
|
static async transform(rawCharacter) {
|
||||||
const sourceCharacter = JSON.parse(rawCharacter); //source character
|
const sourceCharacter = JSON.parse(rawCharacter); //source character
|
||||||
|
|
||||||
const details = {
|
const details = {
|
||||||
species: sourceCharacter.attribs.find((e) => e.name == "race").current,
|
species: sourceCharacter.attribs.find((e) => e.name == "race").current,
|
||||||
background: sourceCharacter.attribs.find((e) => e.name == "background").current,
|
background: sourceCharacter.attribs.find((e) => e.name == "background").current,
|
||||||
alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
|
alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
|
||||||
};
|
};
|
||||||
|
|
||||||
const hp = {
|
const hp = {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "hp").current,
|
value: sourceCharacter.attribs.find((e) => e.name == "hp").current,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: sourceCharacter.attribs.find((e) => e.name == "hp").current,
|
max: sourceCharacter.attribs.find((e) => e.name == "hp").current,
|
||||||
temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current
|
temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current
|
||||||
};
|
};
|
||||||
|
|
||||||
const abilities = {
|
const abilities = {
|
||||||
str: {
|
str: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "strength").current,
|
value: sourceCharacter.attribs.find((e) => e.name == "strength").current,
|
||||||
proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0
|
proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0
|
||||||
},
|
},
|
||||||
dex: {
|
dex: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current,
|
value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current,
|
||||||
proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0
|
proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0
|
||||||
},
|
},
|
||||||
con: {
|
con: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "constitution").current,
|
value: sourceCharacter.attribs.find((e) => e.name == "constitution").current,
|
||||||
proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0
|
proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0
|
||||||
},
|
},
|
||||||
int: {
|
int: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current,
|
value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current,
|
||||||
proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0
|
proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0
|
||||||
},
|
},
|
||||||
wis: {
|
wis: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current,
|
value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current,
|
||||||
proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0
|
proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0
|
||||||
},
|
},
|
||||||
cha: {
|
cha: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "charisma").current,
|
value: sourceCharacter.attribs.find((e) => e.name == "charisma").current,
|
||||||
proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0
|
proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ----------------------------------------------------------------- */
|
/* ----------------------------------------------------------------- */
|
||||||
/* character.data.skills.<skill_name>.value is all that matters
|
/* character.data.skills.<skill_name>.value is all that matters
|
||||||
/* values can be 0, 0.5, 1 or 2
|
/* values can be 0, 0.5, 1 or 2
|
||||||
/* 0 = regular
|
/* 0 = regular
|
||||||
/* 0.5 = half-proficient
|
/* 0.5 = half-proficient
|
||||||
|
@ -53,272 +53,274 @@ export default class CharacterImporter {
|
||||||
/* 2 = expertise
|
/* 2 = expertise
|
||||||
/* foundry takes care of calculating the rest
|
/* foundry takes care of calculating the rest
|
||||||
/* ----------------------------------------------------------------- */
|
/* ----------------------------------------------------------------- */
|
||||||
const skills = {
|
const skills = {
|
||||||
acr: {
|
acr: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
|
||||||
},
|
},
|
||||||
ani: {
|
ani: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
|
||||||
},
|
},
|
||||||
ath: {
|
ath: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
|
||||||
},
|
},
|
||||||
dec: {
|
dec: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
|
||||||
},
|
},
|
||||||
ins: {
|
ins: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
|
||||||
},
|
},
|
||||||
inv: {
|
inv: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
|
||||||
},
|
},
|
||||||
itm: {
|
itm: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
|
||||||
},
|
},
|
||||||
lor: {
|
lor: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
|
||||||
},
|
},
|
||||||
med: {
|
med: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
|
||||||
},
|
},
|
||||||
nat: {
|
nat: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
|
||||||
},
|
},
|
||||||
per: {
|
per: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
|
||||||
},
|
},
|
||||||
pil: {
|
pil: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
|
||||||
},
|
},
|
||||||
prc: {
|
prc: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
|
||||||
},
|
},
|
||||||
prf: {
|
prf: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
|
||||||
},
|
},
|
||||||
slt: {
|
slt: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
|
||||||
},
|
},
|
||||||
ste: {
|
ste: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
|
||||||
},
|
},
|
||||||
sur: {
|
sur: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
|
||||||
},
|
},
|
||||||
tec: {
|
tec: {
|
||||||
value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
|
value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const targetCharacter = {
|
|
||||||
name: sourceCharacter.name,
|
|
||||||
type: "character",
|
|
||||||
data: {
|
|
||||||
abilities: abilities,
|
|
||||||
details: details,
|
|
||||||
skills: skills,
|
|
||||||
attributes: {
|
|
||||||
hp: hp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let actor = await Actor.create(targetCharacter);
|
|
||||||
CharacterImporter.addProfessions(sourceCharacter, actor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse all classes and add them to already created actor.
|
|
||||||
// "class" is a reserved word, therefore I use profession where I can.
|
|
||||||
static async addProfessions(sourceCharacter, actor) {
|
|
||||||
let result = [];
|
|
||||||
|
|
||||||
// parse all class and multiclassX items
|
|
||||||
// couldn't get Array.filter to work here for some reason
|
|
||||||
// result = array of objects. each object is a separate class
|
|
||||||
sourceCharacter.attribs.forEach((e) => {
|
|
||||||
if (CharacterImporter.classOrMulticlass(e.name)) {
|
|
||||||
var t = {
|
|
||||||
profession: CharacterImporter.capitalize(e.current),
|
|
||||||
type: CharacterImporter.baseOrMulti(e.name),
|
|
||||||
level: CharacterImporter.getLevel(e, sourceCharacter)
|
|
||||||
};
|
};
|
||||||
result.push(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// pull classes directly from system compendium and add them to current actor
|
const targetCharacter = {
|
||||||
const professionsPack = await game.packs.get("sw5e.classes").getDocuments();
|
name: sourceCharacter.name,
|
||||||
result.forEach((prof) => {
|
type: "character",
|
||||||
let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
|
data: {
|
||||||
assignedProfession.data.data.levels = prof.level;
|
abilities: abilities,
|
||||||
actor.createEmbeddedDocuments("Item", [assignedProfession.data], { displaySheet: false });
|
details: details,
|
||||||
});
|
skills: skills,
|
||||||
|
attributes: {
|
||||||
|
hp: hp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
|
let actor = await Actor.create(targetCharacter);
|
||||||
|
CharacterImporter.addProfessions(sourceCharacter, actor);
|
||||||
this.addPowers(
|
|
||||||
sourceCharacter.attribs.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1).map((e) => e.current),
|
|
||||||
actor
|
|
||||||
);
|
|
||||||
|
|
||||||
const discoveredItems = sourceCharacter.attribs.filter(
|
|
||||||
(e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
|
|
||||||
);
|
|
||||||
const items = discoveredItems.map((item) => {
|
|
||||||
const id = item.name.match(/-\w{19}/g);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: item.current,
|
|
||||||
quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addItems(items, actor);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async addClasses(profession, level, actor) {
|
|
||||||
let classes = await game.packs.get("sw5e.classes").getDocuments();
|
|
||||||
let assignedClass = classes.find((c) => c.name === profession);
|
|
||||||
assignedClass.data.data.levels = level;
|
|
||||||
await actor.createEmbeddedDocuments("Item", [assignedClass.data], { displaySheet: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
static classOrMulticlass(name) {
|
|
||||||
return name === "class" || (name.includes("multiclass") && name.length <= 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
static baseOrMulti(name) {
|
|
||||||
if (name === "class") {
|
|
||||||
return "base_class";
|
|
||||||
} else {
|
|
||||||
return "multi_class";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static getLevel(item, sourceCharacter) {
|
// Parse all classes and add them to already created actor.
|
||||||
if (item.name === "class") {
|
// "class" is a reserved word, therefore I use profession where I can.
|
||||||
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
|
static async addProfessions(sourceCharacter, actor) {
|
||||||
return parseInt(result);
|
let result = [];
|
||||||
} else {
|
|
||||||
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
|
// parse all class and multiclassX items
|
||||||
return parseInt(result);
|
// couldn't get Array.filter to work here for some reason
|
||||||
|
// result = array of objects. each object is a separate class
|
||||||
|
sourceCharacter.attribs.forEach((e) => {
|
||||||
|
if (CharacterImporter.classOrMulticlass(e.name)) {
|
||||||
|
var t = {
|
||||||
|
profession: CharacterImporter.capitalize(e.current),
|
||||||
|
type: CharacterImporter.baseOrMulti(e.name),
|
||||||
|
level: CharacterImporter.getLevel(e, sourceCharacter)
|
||||||
|
};
|
||||||
|
result.push(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// pull classes directly from system compendium and add them to current actor
|
||||||
|
const professionsPack = await game.packs.get("sw5e.classes").getDocuments();
|
||||||
|
result.forEach((prof) => {
|
||||||
|
let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
|
||||||
|
assignedProfession.data.data.levels = prof.level;
|
||||||
|
actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
|
||||||
|
|
||||||
|
this.addPowers(
|
||||||
|
sourceCharacter.attribs
|
||||||
|
.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
|
||||||
|
.map((e) => e.current),
|
||||||
|
actor
|
||||||
|
);
|
||||||
|
|
||||||
|
const discoveredItems = sourceCharacter.attribs.filter(
|
||||||
|
(e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
|
||||||
|
);
|
||||||
|
const items = discoveredItems.map((item) => {
|
||||||
|
const id = item.name.match(/-\w{19}/g);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: item.current,
|
||||||
|
quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addItems(items, actor);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static capitalize(str) {
|
static async addClasses(profession, level, actor) {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
let classes = await game.packs.get("sw5e.classes").getDocuments();
|
||||||
}
|
let assignedClass = classes.find((c) => c.name === profession);
|
||||||
|
assignedClass.data.data.levels = level;
|
||||||
static async addSpecies(race, actor) {
|
await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false});
|
||||||
const species = await game.packs.get("sw5e.species").getDocuments();
|
|
||||||
const assignedSpecies = species.find((c) => c.name === race);
|
|
||||||
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
|
|
||||||
const actorData = { data: { abilities: { ...actor.data.data.abilities } } };
|
|
||||||
|
|
||||||
activeEffects.map((effect) => {
|
|
||||||
switch (effect.key) {
|
|
||||||
case "data.abilities.str.value":
|
|
||||||
actorData.data.abilities.str.value -= effect.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "data.abilities.dex.value":
|
|
||||||
actorData.data.abilities.dex.value -= effect.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "data.abilities.con.value":
|
|
||||||
actorData.data.abilities.con.value -= effect.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "data.abilities.int.value":
|
|
||||||
actorData.data.abilities.int.value -= effect.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "data.abilities.wis.value":
|
|
||||||
actorData.data.abilities.wis.value -= effect.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "data.abilities.cha.value":
|
|
||||||
actorData.data.abilities.cha.value -= effect.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
actor.update(actorData);
|
|
||||||
|
|
||||||
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], { displaySheet: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
static async addPowers(powers, actor) {
|
|
||||||
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
|
|
||||||
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
|
|
||||||
|
|
||||||
for (const power of powers) {
|
|
||||||
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
|
|
||||||
|
|
||||||
if (createdPower) {
|
|
||||||
await actor.createEmbeddedDocuments("Item", [createdPower.data], { displaySheet: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static async addItems(items, actor) {
|
static classOrMulticlass(name) {
|
||||||
const weapons = await game.packs.get("sw5e.weapons").getDocuments();
|
return name === "class" || (name.includes("multiclass") && name.length <= 12);
|
||||||
const armors = await game.packs.get("sw5e.armor").getDocuments();
|
}
|
||||||
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
|
|
||||||
|
|
||||||
for (const item of items) {
|
static baseOrMulti(name) {
|
||||||
const createdItem =
|
if (name === "class") {
|
||||||
weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
return "base_class";
|
||||||
armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
} else {
|
||||||
adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
|
return "multi_class";
|
||||||
|
|
||||||
if (createdItem) {
|
|
||||||
if (item.quantity != 1) {
|
|
||||||
createdItem.data.data.quantity = item.quantity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await actor.createEmbeddedDocuments("Item", [createdItem.data], { displaySheet: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static addImportButton(html) {
|
static getLevel(item, sourceCharacter) {
|
||||||
const actionButtons = html.find(".header-actions");
|
if (item.name === "class") {
|
||||||
actionButtons[0].insertAdjacentHTML(
|
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
|
||||||
"afterend",
|
return parseInt(result);
|
||||||
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>`
|
} else {
|
||||||
);
|
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
|
||||||
|
return parseInt(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let characterImportButton = $(".cs-import-button");
|
static capitalize(str) {
|
||||||
characterImportButton.click(() => {
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
let content = `<h1>Saved Character JSON Import</h1>
|
}
|
||||||
|
|
||||||
|
static async addSpecies(race, actor) {
|
||||||
|
const species = await game.packs.get("sw5e.species").getDocuments();
|
||||||
|
const assignedSpecies = species.find((c) => c.name === race);
|
||||||
|
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
|
||||||
|
const actorData = {data: {abilities: {...actor.data.data.abilities}}};
|
||||||
|
|
||||||
|
activeEffects.map((effect) => {
|
||||||
|
switch (effect.key) {
|
||||||
|
case "data.abilities.str.value":
|
||||||
|
actorData.data.abilities.str.value -= effect.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "data.abilities.dex.value":
|
||||||
|
actorData.data.abilities.dex.value -= effect.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "data.abilities.con.value":
|
||||||
|
actorData.data.abilities.con.value -= effect.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "data.abilities.int.value":
|
||||||
|
actorData.data.abilities.int.value -= effect.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "data.abilities.wis.value":
|
||||||
|
actorData.data.abilities.wis.value -= effect.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "data.abilities.cha.value":
|
||||||
|
actorData.data.abilities.cha.value -= effect.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
actor.update(actorData);
|
||||||
|
|
||||||
|
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addPowers(powers, actor) {
|
||||||
|
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
|
||||||
|
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
|
||||||
|
|
||||||
|
for (const power of powers) {
|
||||||
|
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
|
||||||
|
|
||||||
|
if (createdPower) {
|
||||||
|
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addItems(items, actor) {
|
||||||
|
const weapons = await game.packs.get("sw5e.weapons").getDocuments();
|
||||||
|
const armors = await game.packs.get("sw5e.armor").getDocuments();
|
||||||
|
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const createdItem =
|
||||||
|
weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
||||||
|
armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
||||||
|
adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
|
||||||
|
|
||||||
|
if (createdItem) {
|
||||||
|
if (item.quantity != 1) {
|
||||||
|
createdItem.data.data.quantity = item.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static addImportButton(html) {
|
||||||
|
const actionButtons = html.find(".header-actions");
|
||||||
|
actionButtons[0].insertAdjacentHTML(
|
||||||
|
"afterend",
|
||||||
|
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>`
|
||||||
|
);
|
||||||
|
|
||||||
|
let characterImportButton = $(".cs-import-button");
|
||||||
|
characterImportButton.click(() => {
|
||||||
|
let content = `<h1>Saved Character JSON Import</h1>
|
||||||
<label for="character-json">Paste character JSON here:</label>
|
<label for="character-json">Paste character JSON here:</label>
|
||||||
</br>
|
</br>
|
||||||
<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>`;
|
<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>`;
|
||||||
let importDialog = new Dialog({
|
let importDialog = new Dialog({
|
||||||
title: "Import Character from SW5e.com",
|
title: "Import Character from SW5e.com",
|
||||||
content: content,
|
content: content,
|
||||||
buttons: {
|
buttons: {
|
||||||
Import: {
|
Import: {
|
||||||
icon: `<i class="fas fa-file-import"></i>`,
|
icon: `<i class="fas fa-file-import"></i>`,
|
||||||
label: "Import Character",
|
label: "Import Character",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
let characterData = $("#character-json").val();
|
let characterData = $("#character-json").val();
|
||||||
console.log("Parsing Character JSON");
|
console.log("Parsing Character JSON");
|
||||||
CharacterImporter.transform(characterData);
|
CharacterImporter.transform(characterData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Cancel: {
|
Cancel: {
|
||||||
icon: `<i class="fas fa-times-circle"></i>`,
|
icon: `<i class="fas fa-times-circle"></i>`,
|
||||||
label: "Cancel",
|
label: "Cancel",
|
||||||
callback: () => {}
|
callback: () => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
importDialog.render(true);
|
importDialog.render(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
151
module/chat.js
151
module/chat.js
|
@ -1,30 +1,29 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlight critical success or failure on d20 rolls
|
* Highlight critical success or failure on d20 rolls
|
||||||
*/
|
*/
|
||||||
export const highlightCriticalSuccessFailure = function(message, html, data) {
|
export const highlightCriticalSuccessFailure = function (message, html, data) {
|
||||||
if ( !message.isRoll || !message.isContentVisible ) return;
|
if (!message.isRoll || !message.isContentVisible) return;
|
||||||
|
|
||||||
// Highlight rolls where the first part is a d20 roll
|
// Highlight rolls where the first part is a d20 roll
|
||||||
const roll = message.roll;
|
const roll = message.roll;
|
||||||
if ( !roll.dice.length ) return;
|
if (!roll.dice.length) return;
|
||||||
const d = roll.dice[0];
|
const d = roll.dice[0];
|
||||||
|
|
||||||
// Ensure it is an un-modified d20 roll
|
// Ensure it is an un-modified d20 roll
|
||||||
const isD20 = (d.faces === 20) && ( d.values.length === 1 );
|
const isD20 = d.faces === 20 && d.values.length === 1;
|
||||||
if ( !isD20 ) return;
|
if (!isD20) return;
|
||||||
const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
|
const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure;
|
||||||
if ( isModifiedRoll ) return;
|
if (isModifiedRoll) return;
|
||||||
|
|
||||||
// Highlight successes and failures
|
// Highlight successes and failures
|
||||||
const critical = d.options.critical || 20;
|
const critical = d.options.critical || 20;
|
||||||
const fumble = d.options.fumble || 1;
|
const fumble = d.options.fumble || 1;
|
||||||
if ( d.total >= critical ) html.find(".dice-total").addClass("critical");
|
if (d.total >= critical) html.find(".dice-total").addClass("critical");
|
||||||
else if ( d.total <= fumble ) html.find(".dice-total").addClass("fumble");
|
else if (d.total <= fumble) html.find(".dice-total").addClass("fumble");
|
||||||
else if ( d.options.target ) {
|
else if (d.options.target) {
|
||||||
if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
|
if (roll.total >= d.options.target) html.find(".dice-total").addClass("success");
|
||||||
else html.find(".dice-total").addClass("failure");
|
else html.find(".dice-total").addClass("failure");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -32,24 +31,24 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
|
||||||
/**
|
/**
|
||||||
* Optionally hide the display of chat card action buttons which cannot be performed by the user
|
* Optionally hide the display of chat card action buttons which cannot be performed by the user
|
||||||
*/
|
*/
|
||||||
export const displayChatActionButtons = function(message, html, data) {
|
export const displayChatActionButtons = function (message, html, data) {
|
||||||
const chatCard = html.find(".sw5e.chat-card");
|
const chatCard = html.find(".sw5e.chat-card");
|
||||||
if ( chatCard.length > 0 ) {
|
if (chatCard.length > 0) {
|
||||||
const flavor = html.find(".flavor-text");
|
const flavor = html.find(".flavor-text");
|
||||||
if ( flavor.text() === html.find(".item-name").text() ) flavor.remove();
|
if (flavor.text() === html.find(".item-name").text()) flavor.remove();
|
||||||
|
|
||||||
// If the user is the message author or the actor owner, proceed
|
// If the user is the message author or the actor owner, proceed
|
||||||
let actor = game.actors.get(data.message.speaker.actor);
|
let actor = game.actors.get(data.message.speaker.actor);
|
||||||
if ( actor && actor.isOwner ) return;
|
if (actor && actor.isOwner) return;
|
||||||
else if ( game.user.isGM || (data.author.id === game.user.id)) return;
|
else if (game.user.isGM || data.author.id === game.user.id) return;
|
||||||
|
|
||||||
// Otherwise conceal action buttons except for saving throw
|
// Otherwise conceal action buttons except for saving throw
|
||||||
const buttons = chatCard.find("button[data-action]");
|
const buttons = chatCard.find("button[data-action]");
|
||||||
buttons.each((i, btn) => {
|
buttons.each((i, btn) => {
|
||||||
if ( btn.dataset.action === "save" ) return;
|
if (btn.dataset.action === "save") return;
|
||||||
btn.style.display = "none"
|
btn.style.display = "none";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -63,38 +62,38 @@ export const displayChatActionButtons = function(message, html, data) {
|
||||||
*
|
*
|
||||||
* @return {Array} The extended options Array including new context choices
|
* @return {Array} The extended options Array including new context choices
|
||||||
*/
|
*/
|
||||||
export const addChatMessageContextOptions = function(html, options) {
|
export const addChatMessageContextOptions = function (html, options) {
|
||||||
let canApply = li => {
|
let canApply = (li) => {
|
||||||
const message = game.messages.get(li.data("messageId"));
|
const message = game.messages.get(li.data("messageId"));
|
||||||
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
|
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
|
||||||
};
|
};
|
||||||
options.push(
|
options.push(
|
||||||
{
|
{
|
||||||
name: game.i18n.localize("SW5E.ChatContextDamage"),
|
name: game.i18n.localize("SW5E.ChatContextDamage"),
|
||||||
icon: '<i class="fas fa-user-minus"></i>',
|
icon: '<i class="fas fa-user-minus"></i>',
|
||||||
condition: canApply,
|
condition: canApply,
|
||||||
callback: li => applyChatCardDamage(li, 1)
|
callback: (li) => applyChatCardDamage(li, 1)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: game.i18n.localize("SW5E.ChatContextHealing"),
|
name: game.i18n.localize("SW5E.ChatContextHealing"),
|
||||||
icon: '<i class="fas fa-user-plus"></i>',
|
icon: '<i class="fas fa-user-plus"></i>',
|
||||||
condition: canApply,
|
condition: canApply,
|
||||||
callback: li => applyChatCardDamage(li, -1)
|
callback: (li) => applyChatCardDamage(li, -1)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
|
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
|
||||||
icon: '<i class="fas fa-user-injured"></i>',
|
icon: '<i class="fas fa-user-injured"></i>',
|
||||||
condition: canApply,
|
condition: canApply,
|
||||||
callback: li => applyChatCardDamage(li, 2)
|
callback: (li) => applyChatCardDamage(li, 2)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
|
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
|
||||||
icon: '<i class="fas fa-user-shield"></i>',
|
icon: '<i class="fas fa-user-shield"></i>',
|
||||||
condition: canApply,
|
condition: canApply,
|
||||||
callback: li => applyChatCardDamage(li, 0.5)
|
callback: (li) => applyChatCardDamage(li, 0.5)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -108,12 +107,14 @@ export const addChatMessageContextOptions = function(html, options) {
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
function applyChatCardDamage(li, multiplier) {
|
function applyChatCardDamage(li, multiplier) {
|
||||||
const message = game.messages.get(li.data("messageId"));
|
const message = game.messages.get(li.data("messageId"));
|
||||||
const roll = message.roll;
|
const roll = message.roll;
|
||||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
return Promise.all(
|
||||||
const a = t.actor;
|
canvas.tokens.controlled.map((t) => {
|
||||||
return a.applyDamage(roll.total, multiplier);
|
const a = t.actor;
|
||||||
}));
|
return a.applyDamage(roll.total, multiplier);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
|
@ -1,4 +1 @@
|
||||||
export const ClassFeatures = {
|
export const ClassFeatures = {};
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the default Initiative formula to customize special behaviors of the SW5e system.
|
* Override the default Initiative formula to customize special behaviors of the SW5e system.
|
||||||
* Apply advantage, proficiency, or bonuses where appropriate
|
* Apply advantage, proficiency, or bonuses where appropriate
|
||||||
* Apply the dexterity score as a decimal tiebreaker if requested
|
* Apply the dexterity score as a decimal tiebreaker if requested
|
||||||
* See Combat._getInitiativeFormula for more detail.
|
* See Combat._getInitiativeFormula for more detail.
|
||||||
*/
|
*/
|
||||||
export const _getInitiativeFormula = function() {
|
export const _getInitiativeFormula = function () {
|
||||||
const actor = this.actor;
|
const actor = this.actor;
|
||||||
if ( !actor ) return "1d20";
|
if (!actor) return "1d20";
|
||||||
const init = actor.data.data.attributes.init;
|
const init = actor.data.data.attributes.init;
|
||||||
|
|
||||||
// Construct initiative formula parts
|
// Construct initiative formula parts
|
||||||
let nd = 1;
|
let nd = 1;
|
||||||
let mods = "";
|
let mods = "";
|
||||||
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
|
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
|
||||||
if (actor.getFlag("sw5e", "initiativeAdv")) {
|
if (actor.getFlag("sw5e", "initiativeAdv")) {
|
||||||
nd = 2;
|
nd = 2;
|
||||||
mods += "kh";
|
mods += "kh";
|
||||||
}
|
}
|
||||||
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
|
const parts = [
|
||||||
|
`${nd}d20${mods}`,
|
||||||
|
init.mod,
|
||||||
|
init.prof !== 0 ? init.prof : null,
|
||||||
|
init.bonus !== 0 ? init.bonus : null
|
||||||
|
];
|
||||||
|
|
||||||
// Optionally apply Dexterity tiebreaker
|
// Optionally apply Dexterity tiebreaker
|
||||||
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
|
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
|
||||||
if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
|
if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100);
|
||||||
return parts.filter(p => p !== null).join(" + ");
|
return parts.filter((p) => p !== null).join(" + ");
|
||||||
};
|
};
|
||||||
|
|
2233
module/config.js
2233
module/config.js
File diff suppressed because it is too large
Load diff
301
module/dice.js
301
module/dice.js
|
@ -12,50 +12,55 @@ export {default as DamageRoll} from "./dice/damage-roll.js";
|
||||||
* @return {string} The resulting simplified formula
|
* @return {string} The resulting simplified formula
|
||||||
*/
|
*/
|
||||||
export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) {
|
export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) {
|
||||||
const roll = new Roll(formula, data); // Parses the formula and replaces any @properties
|
const roll = new Roll(formula, data); // Parses the formula and replaces any @properties
|
||||||
const terms = roll.terms;
|
const terms = roll.terms;
|
||||||
|
|
||||||
// Some terms are "too complicated" for this algorithm to simplify
|
// Some terms are "too complicated" for this algorithm to simplify
|
||||||
// In this case, the original formula is returned.
|
// In this case, the original formula is returned.
|
||||||
if (terms.some(_isUnsupportedTerm)) return roll.formula;
|
if (terms.some(_isUnsupportedTerm)) return roll.formula;
|
||||||
|
|
||||||
const rollableTerms = []; // Terms that are non-constant, and their associated operators
|
const rollableTerms = []; // Terms that are non-constant, and their associated operators
|
||||||
const constantTerms = []; // Terms that are constant, and their associated operators
|
const constantTerms = []; // Terms that are constant, and their associated operators
|
||||||
let operators = []; // Temporary storage for operators before they are moved to one of the above
|
let operators = []; // Temporary storage for operators before they are moved to one of the above
|
||||||
|
|
||||||
for (let term of terms) { // For each term
|
for (let term of terms) {
|
||||||
if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
|
// For each term
|
||||||
else { // Otherwise the term is not an operator
|
if (term instanceof OperatorTerm) operators.push(term);
|
||||||
if (term instanceof DiceTerm) { // If the term is something rollable
|
// If the term is an addition/subtraction operator, push the term into the operators array
|
||||||
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
|
else {
|
||||||
rollableTerms.push(term); // Then place this rollable term into it as well
|
// Otherwise the term is not an operator
|
||||||
} //
|
if (term instanceof DiceTerm) {
|
||||||
else { // Otherwise, this must be a constant
|
// If the term is something rollable
|
||||||
constantTerms.push(...operators); // Place the operators into the constantTerms array
|
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
|
||||||
constantTerms.push(term); // Then also add this constant term to that array.
|
rollableTerms.push(term); // Then place this rollable term into it as well
|
||||||
} //
|
} //
|
||||||
operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
|
else {
|
||||||
|
// Otherwise, this must be a constant
|
||||||
|
constantTerms.push(...operators); // Place the operators into the constantTerms array
|
||||||
|
constantTerms.push(term); // Then also add this constant term to that array.
|
||||||
|
} //
|
||||||
|
operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
|
const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
|
||||||
const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
|
const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
|
||||||
|
|
||||||
// Mathematically evaluate the constant formula to produce a single constant term
|
// Mathematically evaluate the constant formula to produce a single constant term
|
||||||
let constantPart = undefined;
|
let constantPart = undefined;
|
||||||
if ( constantFormula ) {
|
if (constantFormula) {
|
||||||
try {
|
try {
|
||||||
constantPart = Roll.safeEval(constantFormula)
|
constantPart = Roll.safeEval(constantFormula);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
|
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Order the rollable and constant terms, either constant first or second depending on the optional argument
|
// Order the rollable and constant terms, either constant first or second depending on the optional argument
|
||||||
const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
|
const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
|
||||||
|
|
||||||
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
|
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
|
||||||
return new Roll(parts.filterJoin(" + ")).formula;
|
return new Roll(parts.filterJoin(" + ")).formula;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -66,11 +71,11 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
|
||||||
* @return {Boolean} True when unsupported, false if supported
|
* @return {Boolean} True when unsupported, false if supported
|
||||||
*/
|
*/
|
||||||
function _isUnsupportedTerm(term) {
|
function _isUnsupportedTerm(term) {
|
||||||
const diceTerm = term instanceof DiceTerm;
|
const diceTerm = term instanceof DiceTerm;
|
||||||
const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
|
const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
|
||||||
const number = term instanceof NumericTerm;
|
const number = term instanceof NumericTerm;
|
||||||
|
|
||||||
return !(diceTerm || operator || number);
|
return !(diceTerm || operator || number);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -111,54 +116,75 @@ function _isUnsupportedTerm(term) {
|
||||||
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
|
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
|
||||||
*/
|
*/
|
||||||
export async function d20Roll({
|
export async function d20Roll({
|
||||||
parts=[], data={}, // Roll creation
|
parts = [],
|
||||||
advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization
|
data = {}, // Roll creation
|
||||||
chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration
|
advantage,
|
||||||
chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization
|
disadvantage,
|
||||||
}={}) {
|
fumble = 1,
|
||||||
|
critical = 20,
|
||||||
// Handle input arguments
|
|
||||||
const formula = ["1d20"].concat(parts).join(" + ");
|
|
||||||
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
|
|
||||||
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
|
|
||||||
if ( chooseModifier && !isFF ) data["mod"] = "@mod";
|
|
||||||
|
|
||||||
// Construct the D20Roll instance
|
|
||||||
const roll = new CONFIG.Dice.D20Roll(formula, data, {
|
|
||||||
flavor: flavor || title,
|
|
||||||
advantageMode,
|
|
||||||
defaultRollMode,
|
|
||||||
critical,
|
|
||||||
fumble,
|
|
||||||
targetValue,
|
targetValue,
|
||||||
elvenAccuracy,
|
elvenAccuracy,
|
||||||
halflingLucky,
|
halflingLucky,
|
||||||
reliableTalent
|
reliableTalent, // Roll customization
|
||||||
});
|
chooseModifier = false,
|
||||||
|
fastForward = false,
|
||||||
|
event,
|
||||||
|
template,
|
||||||
|
title,
|
||||||
|
dialogOptions, // Dialog configuration
|
||||||
|
chatMessage = true,
|
||||||
|
messageData = {},
|
||||||
|
rollMode,
|
||||||
|
speaker,
|
||||||
|
flavor // Chat Message customization
|
||||||
|
} = {}) {
|
||||||
|
// Handle input arguments
|
||||||
|
const formula = ["1d20"].concat(parts).join(" + ");
|
||||||
|
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
|
||||||
|
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
|
||||||
|
if (chooseModifier && !isFF) data["mod"] = "@mod";
|
||||||
|
|
||||||
// Prompt a Dialog to further configure the D20Roll
|
// Construct the D20Roll instance
|
||||||
if ( !isFF ) {
|
const roll = new CONFIG.Dice.D20Roll(formula, data, {
|
||||||
const configured = await roll.configureDialog({
|
flavor: flavor || title,
|
||||||
title,
|
advantageMode,
|
||||||
chooseModifier,
|
defaultRollMode,
|
||||||
defaultRollMode: defaultRollMode,
|
critical,
|
||||||
defaultAction: advantageMode,
|
fumble,
|
||||||
defaultAbility: data?.item?.ability,
|
targetValue,
|
||||||
template
|
elvenAccuracy,
|
||||||
}, dialogOptions);
|
halflingLucky,
|
||||||
if ( configured === null ) return null;
|
reliableTalent
|
||||||
}
|
});
|
||||||
|
|
||||||
// Evaluate the configured roll
|
// Prompt a Dialog to further configure the D20Roll
|
||||||
await roll.evaluate({async: true});
|
if (!isFF) {
|
||||||
|
const configured = await roll.configureDialog(
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
chooseModifier,
|
||||||
|
defaultRollMode: defaultRollMode,
|
||||||
|
defaultAction: advantageMode,
|
||||||
|
defaultAbility: data?.item?.ability,
|
||||||
|
template
|
||||||
|
},
|
||||||
|
dialogOptions
|
||||||
|
);
|
||||||
|
if (configured === null) return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a Chat Message
|
// Evaluate the configured roll
|
||||||
if ( speaker ) {
|
await roll.evaluate({async: true});
|
||||||
console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`);
|
|
||||||
messageData.speaker = speaker;
|
// Create a Chat Message
|
||||||
}
|
if (speaker) {
|
||||||
if ( roll && chatMessage ) await roll.toMessage(messageData);
|
console.warn(
|
||||||
return roll;
|
`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`
|
||||||
|
);
|
||||||
|
messageData.speaker = speaker;
|
||||||
|
}
|
||||||
|
if (roll && chatMessage) await roll.toMessage(messageData);
|
||||||
|
return roll;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -167,12 +193,13 @@ export async function d20Roll({
|
||||||
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
|
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
|
||||||
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
|
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
|
||||||
*/
|
*/
|
||||||
function _determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward=false}={}) {
|
function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) {
|
||||||
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||||
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
|
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
|
||||||
if ( advantage || event?.altKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
|
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
|
||||||
else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
|
else if (disadvantage || event?.ctrlKey || event?.metaKey)
|
||||||
return {isFF, advantageMode};
|
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
|
||||||
|
return {isFF, advantageMode};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -210,49 +237,67 @@ function _determineAdvantageMode({event, advantage=false, disadvantage=false, fa
|
||||||
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
|
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
|
||||||
*/
|
*/
|
||||||
export async function damageRoll({
|
export async function damageRoll({
|
||||||
parts=[], data, // Roll creation
|
parts = [],
|
||||||
critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization
|
data, // Roll creation
|
||||||
fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration
|
critical = false,
|
||||||
chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization
|
|
||||||
}={}) {
|
|
||||||
|
|
||||||
// Handle input arguments
|
|
||||||
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
|
|
||||||
|
|
||||||
// Construct the DamageRoll instance
|
|
||||||
const formula = parts.join(" + ");
|
|
||||||
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
|
|
||||||
const roll = new CONFIG.Dice.DamageRoll(formula, data, {
|
|
||||||
flavor: flavor || title,
|
|
||||||
critical: isCritical,
|
|
||||||
criticalBonusDice,
|
criticalBonusDice,
|
||||||
criticalMultiplier,
|
criticalMultiplier,
|
||||||
multiplyNumeric,
|
multiplyNumeric,
|
||||||
powerfulCritical
|
powerfulCritical, // Damage customization
|
||||||
});
|
fastForward = false,
|
||||||
|
event,
|
||||||
|
allowCritical = true,
|
||||||
|
template,
|
||||||
|
title,
|
||||||
|
dialogOptions, // Dialog configuration
|
||||||
|
chatMessage = true,
|
||||||
|
messageData = {},
|
||||||
|
rollMode,
|
||||||
|
speaker,
|
||||||
|
flavor // Chat Message customization
|
||||||
|
} = {}) {
|
||||||
|
// Handle input arguments
|
||||||
|
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
|
||||||
|
|
||||||
// Prompt a Dialog to further configure the DamageRoll
|
// Construct the DamageRoll instance
|
||||||
if ( !isFF ) {
|
const formula = parts.join(" + ");
|
||||||
const configured = await roll.configureDialog({
|
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
|
||||||
title,
|
const roll = new CONFIG.Dice.DamageRoll(formula, data, {
|
||||||
defaultRollMode: defaultRollMode,
|
flavor: flavor || title,
|
||||||
defaultCritical: isCritical,
|
critical: isCritical,
|
||||||
template,
|
criticalBonusDice,
|
||||||
allowCritical
|
criticalMultiplier,
|
||||||
}, dialogOptions);
|
multiplyNumeric,
|
||||||
if ( configured === null ) return null;
|
powerfulCritical
|
||||||
}
|
});
|
||||||
|
|
||||||
// Evaluate the configured roll
|
// Prompt a Dialog to further configure the DamageRoll
|
||||||
await roll.evaluate({async: true});
|
if (!isFF) {
|
||||||
|
const configured = await roll.configureDialog(
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
defaultRollMode: defaultRollMode,
|
||||||
|
defaultCritical: isCritical,
|
||||||
|
template,
|
||||||
|
allowCritical
|
||||||
|
},
|
||||||
|
dialogOptions
|
||||||
|
);
|
||||||
|
if (configured === null) return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a Chat Message
|
// Evaluate the configured roll
|
||||||
if ( speaker ) {
|
await roll.evaluate({async: true});
|
||||||
console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`);
|
|
||||||
messageData.speaker = speaker;
|
// Create a Chat Message
|
||||||
}
|
if (speaker) {
|
||||||
if ( roll && chatMessage ) await roll.toMessage(messageData);
|
console.warn(
|
||||||
return roll;
|
`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`
|
||||||
|
);
|
||||||
|
messageData.speaker = speaker;
|
||||||
|
}
|
||||||
|
if (roll && chatMessage) await roll.toMessage(messageData);
|
||||||
|
return roll;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -261,8 +306,8 @@ export async function damageRoll({
|
||||||
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
|
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
|
||||||
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
|
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
|
||||||
*/
|
*/
|
||||||
function _determineCriticalMode({event, critical=false, fastForward=false}={}) {
|
function _determineCriticalMode({event, critical = false, fastForward = false} = {}) {
|
||||||
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||||
if ( event?.altKey ) critical = true;
|
if (event?.altKey) critical = true;
|
||||||
return {isFF, isCritical: critical};
|
return {isFF, isCritical: critical};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
export default class D20Roll extends Roll {
|
export default class D20Roll extends Roll {
|
||||||
constructor(formula, data, options) {
|
constructor(formula, data, options) {
|
||||||
super(formula, data, options);
|
super(formula, data, options);
|
||||||
if ( !((this.terms[0] instanceof Die) && (this.terms[0].faces === 20)) ) {
|
if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) {
|
||||||
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
|
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
|
||||||
}
|
}
|
||||||
this.configureModifiers();
|
this.configureModifiers();
|
||||||
|
@ -31,8 +31,8 @@ export default class D20Roll extends Roll {
|
||||||
static ADV_MODE = {
|
static ADV_MODE = {
|
||||||
NORMAL: 0,
|
NORMAL: 0,
|
||||||
ADVANTAGE: 1,
|
ADVANTAGE: 1,
|
||||||
DISADVANTAGE: -1,
|
DISADVANTAGE: -1
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTML template path used to configure evaluation of this Roll
|
* The HTML template path used to configure evaluation of this Roll
|
||||||
|
@ -71,28 +71,26 @@ export default class D20Roll extends Roll {
|
||||||
d20.modifiers = [];
|
d20.modifiers = [];
|
||||||
|
|
||||||
// Halfling Lucky
|
// Halfling Lucky
|
||||||
if ( this.options.halflingLucky ) d20.modifiers.push("r1=1");
|
if (this.options.halflingLucky) d20.modifiers.push("r1=1");
|
||||||
|
|
||||||
// Reliable Talent
|
// Reliable Talent
|
||||||
if ( this.options.reliableTalent ) d20.modifiers.push("min10");
|
if (this.options.reliableTalent) d20.modifiers.push("min10");
|
||||||
|
|
||||||
// Handle Advantage or Disadvantage
|
// Handle Advantage or Disadvantage
|
||||||
if ( this.hasAdvantage ) {
|
if (this.hasAdvantage) {
|
||||||
d20.number = this.options.elvenAccuracy ? 3 : 2;
|
d20.number = this.options.elvenAccuracy ? 3 : 2;
|
||||||
d20.modifiers.push("kh");
|
d20.modifiers.push("kh");
|
||||||
d20.options.advantage = true;
|
d20.options.advantage = true;
|
||||||
}
|
} else if (this.hasDisadvantage) {
|
||||||
else if ( this.hasDisadvantage ) {
|
|
||||||
d20.number = 2;
|
d20.number = 2;
|
||||||
d20.modifiers.push("kl");
|
d20.modifiers.push("kl");
|
||||||
d20.options.disadvantage = true;
|
d20.options.disadvantage = true;
|
||||||
}
|
} else d20.number = 1;
|
||||||
else d20.number = 1;
|
|
||||||
|
|
||||||
// Assign critical and fumble thresholds
|
// Assign critical and fumble thresholds
|
||||||
if ( this.options.critical ) d20.options.critical = this.options.critical;
|
if (this.options.critical) d20.options.critical = this.options.critical;
|
||||||
if ( this.options.fumble ) d20.options.fumble = this.options.fumble;
|
if (this.options.fumble) d20.options.fumble = this.options.fumble;
|
||||||
if ( this.options.targetValue ) d20.options.target = this.options.targetValue;
|
if (this.options.targetValue) d20.options.target = this.options.targetValue;
|
||||||
|
|
||||||
// Re-compile the underlying formula
|
// Re-compile the underlying formula
|
||||||
this._formula = this.constructor.getFormula(this.terms);
|
this._formula = this.constructor.getFormula(this.terms);
|
||||||
|
@ -101,22 +99,21 @@ export default class D20Roll extends Roll {
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
async toMessage(messageData={}, options={}) {
|
async toMessage(messageData = {}, options = {}) {
|
||||||
|
|
||||||
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play
|
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play
|
||||||
if ( !this._evaluated ) await this.evaluate({async: true});
|
if (!this._evaluated) await this.evaluate({async: true});
|
||||||
|
|
||||||
// Add appropriate advantage mode message flavor and sw5e roll flags
|
// Add appropriate advantage mode message flavor and sw5e roll flags
|
||||||
messageData.flavor = messageData.flavor || this.options.flavor;
|
messageData.flavor = messageData.flavor || this.options.flavor;
|
||||||
if ( this.hasAdvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
if (this.hasAdvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
||||||
else if ( this.hasDisadvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
else if (this.hasDisadvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
||||||
|
|
||||||
// Add reliable talent to the d20-term flavor text if it applied
|
// Add reliable talent to the d20-term flavor text if it applied
|
||||||
if ( this.options.reliableTalent ) {
|
if (this.options.reliableTalent) {
|
||||||
const d20 = this.dice[0];
|
const d20 = this.dice[0];
|
||||||
const isRT = d20.results.every(r => !r.active || (r.result < 10));
|
const isRT = d20.results.every((r) => !r.active || r.result < 10);
|
||||||
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
|
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
|
||||||
if ( isRT ) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
|
if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record the preferred rollMode
|
// Record the preferred rollMode
|
||||||
|
@ -140,8 +137,17 @@ export default class D20Roll extends Roll {
|
||||||
* @param {object} options Additional Dialog customization options
|
* @param {object} options Additional Dialog customization options
|
||||||
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
|
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
|
||||||
*/
|
*/
|
||||||
async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false, defaultAbility, template}={}, options={}) {
|
async configureDialog(
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
defaultRollMode,
|
||||||
|
defaultAction = D20Roll.ADV_MODE.NORMAL,
|
||||||
|
chooseModifier = false,
|
||||||
|
defaultAbility,
|
||||||
|
template
|
||||||
|
} = {},
|
||||||
|
options = {}
|
||||||
|
) {
|
||||||
// Render the Dialog inner HTML
|
// Render the Dialog inner HTML
|
||||||
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
|
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
|
||||||
formula: `${this.formula} + @bonus`,
|
formula: `${this.formula} + @bonus`,
|
||||||
|
@ -154,32 +160,39 @@ export default class D20Roll extends Roll {
|
||||||
|
|
||||||
let defaultButton = "normal";
|
let defaultButton = "normal";
|
||||||
switch (defaultAction) {
|
switch (defaultAction) {
|
||||||
case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break;
|
case D20Roll.ADV_MODE.ADVANTAGE:
|
||||||
case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break;
|
defaultButton = "advantage";
|
||||||
|
break;
|
||||||
|
case D20Roll.ADV_MODE.DISADVANTAGE:
|
||||||
|
defaultButton = "disadvantage";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the Dialog window and await submission of the form
|
// Create the Dialog window and await submission of the form
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
new Dialog({
|
new Dialog(
|
||||||
title,
|
{
|
||||||
content,
|
title,
|
||||||
buttons: {
|
content,
|
||||||
advantage: {
|
buttons: {
|
||||||
label: game.i18n.localize("SW5E.Advantage"),
|
advantage: {
|
||||||
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
|
label: game.i18n.localize("SW5E.Advantage"),
|
||||||
|
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
|
||||||
|
},
|
||||||
|
normal: {
|
||||||
|
label: game.i18n.localize("SW5E.Normal"),
|
||||||
|
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
|
||||||
|
},
|
||||||
|
disadvantage: {
|
||||||
|
label: game.i18n.localize("SW5E.Disadvantage"),
|
||||||
|
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
normal: {
|
default: defaultButton,
|
||||||
label: game.i18n.localize("SW5E.Normal"),
|
close: () => resolve(null)
|
||||||
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
|
|
||||||
},
|
|
||||||
disadvantage: {
|
|
||||||
label: game.i18n.localize("SW5E.Disadvantage"),
|
|
||||||
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
default: defaultButton,
|
options
|
||||||
close: () => resolve(null)
|
).render(true);
|
||||||
}, options).render(true);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,16 +208,16 @@ export default class D20Roll extends Roll {
|
||||||
const form = html[0].querySelector("form");
|
const form = html[0].querySelector("form");
|
||||||
|
|
||||||
// Append a situational bonus term
|
// Append a situational bonus term
|
||||||
if ( form.bonus.value ) {
|
if (form.bonus.value) {
|
||||||
const bonus = new Roll(form.bonus.value, this.data);
|
const bonus = new Roll(form.bonus.value, this.data);
|
||||||
if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
|
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
|
||||||
this.terms = this.terms.concat(bonus.terms);
|
this.terms = this.terms.concat(bonus.terms);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customize the modifier
|
// Customize the modifier
|
||||||
if ( form.ability?.value ) {
|
if (form.ability?.value) {
|
||||||
const abl = this.data.abilities[form.ability.value];
|
const abl = this.data.abilities[form.ability.value];
|
||||||
this.terms.findSplice(t => t.term === "@mod", new NumericTerm({number: abl.mod}));
|
this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod}));
|
||||||
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
|
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default class DamageRoll extends Roll {
|
||||||
constructor(formula, data, options) {
|
constructor(formula, data, options) {
|
||||||
super(formula, data, options);
|
super(formula, data, options);
|
||||||
// For backwards compatibility, skip rolls which do not have the "critical" option defined
|
// For backwards compatibility, skip rolls which do not have the "critical" option defined
|
||||||
if ( this.options.critical !== undefined ) this.configureDamage();
|
if (this.options.critical !== undefined) this.configureDamage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,44 +42,44 @@ export default class DamageRoll extends Roll {
|
||||||
*/
|
*/
|
||||||
configureDamage() {
|
configureDamage() {
|
||||||
let flatBonus = 0;
|
let flatBonus = 0;
|
||||||
for ( let [i, term] of this.terms.entries() ) {
|
for (let [i, term] of this.terms.entries()) {
|
||||||
|
|
||||||
// Multiply dice terms
|
// Multiply dice terms
|
||||||
if ( term instanceof DiceTerm ) {
|
if (term instanceof DiceTerm) {
|
||||||
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
|
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
|
||||||
term.number = term.options.baseNumber;
|
term.number = term.options.baseNumber;
|
||||||
if ( this.isCritical ) {
|
if (this.isCritical) {
|
||||||
let cm = this.options.criticalMultiplier ?? 2;
|
let cm = this.options.criticalMultiplier ?? 2;
|
||||||
|
|
||||||
// Powerful critical - maximize damage and reduce the multiplier by 1
|
// Powerful critical - maximize damage and reduce the multiplier by 1
|
||||||
if ( this.options.powerfulCritical ) {
|
if (this.options.powerfulCritical) {
|
||||||
flatBonus += (term.number * term.faces);
|
flatBonus += term.number * term.faces;
|
||||||
cm = Math.max(1, cm-1);
|
cm = Math.max(1, cm - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alter the damage term
|
// Alter the damage term
|
||||||
let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0;
|
let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0;
|
||||||
term.alter(cm, cb);
|
term.alter(cm, cb);
|
||||||
term.options.critical = true;
|
term.options.critical = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiply numeric terms
|
// Multiply numeric terms
|
||||||
else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) {
|
else if (this.options.multiplyNumeric && term instanceof NumericTerm) {
|
||||||
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
|
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
|
||||||
term.number = term.options.baseNumber;
|
term.number = term.options.baseNumber;
|
||||||
if ( this.isCritical ) {
|
if (this.isCritical) {
|
||||||
term.number *= (this.options.criticalMultiplier ?? 2);
|
term.number *= this.options.criticalMultiplier ?? 2;
|
||||||
term.options.critical = true;
|
term.options.critical = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add powerful critical bonus
|
// Add powerful critical bonus
|
||||||
if ( this.options.powerfulCritical && (flatBonus > 0) ) {
|
if (this.options.powerfulCritical && flatBonus > 0) {
|
||||||
this.terms.push(new OperatorTerm({operator: "+"}));
|
this.terms.push(new OperatorTerm({operator: "+"}));
|
||||||
this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")}));
|
this.terms.push(
|
||||||
|
new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-compile the underlying formula
|
// Re-compile the underlying formula
|
||||||
|
@ -89,9 +89,9 @@ export default class DamageRoll extends Roll {
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
toMessage(messageData={}, options={}) {
|
toMessage(messageData = {}, options = {}) {
|
||||||
messageData.flavor = messageData.flavor || this.options.flavor;
|
messageData.flavor = messageData.flavor || this.options.flavor;
|
||||||
if ( this.isCritical ) {
|
if (this.isCritical) {
|
||||||
const label = game.i18n.localize("SW5E.CriticalHit");
|
const label = game.i18n.localize("SW5E.CriticalHit");
|
||||||
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
|
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
|
||||||
}
|
}
|
||||||
|
@ -114,34 +114,39 @@ export default class DamageRoll extends Roll {
|
||||||
* @param {object} options Additional Dialog customization options
|
* @param {object} options Additional Dialog customization options
|
||||||
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
|
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
|
||||||
*/
|
*/
|
||||||
async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) {
|
async configureDialog(
|
||||||
|
{title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {},
|
||||||
|
options = {}
|
||||||
|
) {
|
||||||
// Render the Dialog inner HTML
|
// Render the Dialog inner HTML
|
||||||
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
|
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
|
||||||
formula: `${this.formula} + @bonus`,
|
formula: `${this.formula} + @bonus`,
|
||||||
defaultRollMode,
|
defaultRollMode,
|
||||||
rollModes: CONFIG.Dice.rollModes,
|
rollModes: CONFIG.Dice.rollModes
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the Dialog window and await submission of the form
|
// Create the Dialog window and await submission of the form
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
new Dialog({
|
new Dialog(
|
||||||
title,
|
{
|
||||||
content,
|
title,
|
||||||
buttons: {
|
content,
|
||||||
critical: {
|
buttons: {
|
||||||
condition: allowCritical,
|
critical: {
|
||||||
label: game.i18n.localize("SW5E.CriticalHit"),
|
condition: allowCritical,
|
||||||
callback: html => resolve(this._onDialogSubmit(html, true))
|
label: game.i18n.localize("SW5E.CriticalHit"),
|
||||||
|
callback: (html) => resolve(this._onDialogSubmit(html, true))
|
||||||
|
},
|
||||||
|
normal: {
|
||||||
|
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
||||||
|
callback: (html) => resolve(this._onDialogSubmit(html, false))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
normal: {
|
default: defaultCritical ? "critical" : "normal",
|
||||||
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
close: () => resolve(null)
|
||||||
callback: html => resolve(this._onDialogSubmit(html, false))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
default: defaultCritical ? "critical" : "normal",
|
options
|
||||||
close: () => resolve(null)
|
).render(true);
|
||||||
}, options).render(true);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,9 +162,9 @@ export default class DamageRoll extends Roll {
|
||||||
const form = html[0].querySelector("form");
|
const form = html[0].querySelector("form");
|
||||||
|
|
||||||
// Append a situational bonus term
|
// Append a situational bonus term
|
||||||
if ( form.bonus.value ) {
|
if (form.bonus.value) {
|
||||||
const bonus = new Roll(form.bonus.value, this.data);
|
const bonus = new Roll(form.bonus.value, this.data);
|
||||||
if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
|
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
|
||||||
this.terms = this.terms.concat(bonus.terms);
|
this.terms = this.terms.concat(bonus.terms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
85
module/effects.js
vendored
85
module/effects.js
vendored
|
@ -4,26 +4,28 @@
|
||||||
* @param {Actor|Item} owner The owning entity which manages this effect
|
* @param {Actor|Item} owner The owning entity which manages this effect
|
||||||
*/
|
*/
|
||||||
export function onManageActiveEffect(event, owner) {
|
export function onManageActiveEffect(event, owner) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const a = event.currentTarget;
|
const a = event.currentTarget;
|
||||||
const li = a.closest("li");
|
const li = a.closest("li");
|
||||||
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
|
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
|
||||||
switch ( a.dataset.action ) {
|
switch (a.dataset.action) {
|
||||||
case "create":
|
case "create":
|
||||||
return owner.createEmbeddedDocuments("ActiveEffect", [{
|
return owner.createEmbeddedDocuments("ActiveEffect", [
|
||||||
label: game.i18n.localize("SW5E.EffectNew"),
|
{
|
||||||
icon: "icons/svg/aura.svg",
|
"label": game.i18n.localize("SW5E.EffectNew"),
|
||||||
origin: owner.uuid,
|
"icon": "icons/svg/aura.svg",
|
||||||
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
|
"origin": owner.uuid,
|
||||||
disabled: li.dataset.effectType === "inactive"
|
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
|
||||||
}]);
|
"disabled": li.dataset.effectType === "inactive"
|
||||||
case "edit":
|
}
|
||||||
return effect.sheet.render(true);
|
]);
|
||||||
case "delete":
|
case "edit":
|
||||||
return effect.delete();
|
return effect.sheet.render(true);
|
||||||
case "toggle":
|
case "delete":
|
||||||
return effect.update({disabled: !effect.data.disabled});
|
return effect.delete();
|
||||||
}
|
case "toggle":
|
||||||
|
return effect.update({disabled: !effect.data.disabled});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,32 +34,31 @@ export function onManageActiveEffect(event, owner) {
|
||||||
* @return {object} Data for rendering
|
* @return {object} Data for rendering
|
||||||
*/
|
*/
|
||||||
export function prepareActiveEffectCategories(effects) {
|
export function prepareActiveEffectCategories(effects) {
|
||||||
|
|
||||||
// Define effect header categories
|
// Define effect header categories
|
||||||
const categories = {
|
const categories = {
|
||||||
temporary: {
|
temporary: {
|
||||||
type: "temporary",
|
type: "temporary",
|
||||||
label: game.i18n.localize("SW5E.EffectTemporary"),
|
label: game.i18n.localize("SW5E.EffectTemporary"),
|
||||||
effects: []
|
effects: []
|
||||||
},
|
},
|
||||||
passive: {
|
passive: {
|
||||||
type: "passive",
|
type: "passive",
|
||||||
label: game.i18n.localize("SW5E.EffectPassive"),
|
label: game.i18n.localize("SW5E.EffectPassive"),
|
||||||
effects: []
|
effects: []
|
||||||
},
|
},
|
||||||
inactive: {
|
inactive: {
|
||||||
type: "inactive",
|
type: "inactive",
|
||||||
label: game.i18n.localize("SW5E.EffectInactive"),
|
label: game.i18n.localize("SW5E.EffectInactive"),
|
||||||
effects: []
|
effects: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Iterate over active effects, classifying them into categories
|
// Iterate over active effects, classifying them into categories
|
||||||
for ( let e of effects ) {
|
for (let e of effects) {
|
||||||
e._getSourceName(); // Trigger a lookup for the source name
|
e._getSourceName(); // Trigger a lookup for the source name
|
||||||
if ( e.data.disabled ) categories.inactive.effects.push(e);
|
if (e.data.disabled) categories.inactive.effects.push(e);
|
||||||
else if ( e.isTemporary ) categories.temporary.effects.push(e);
|
else if (e.isTemporary) categories.temporary.effects.push(e);
|
||||||
else categories.passive.effects.push(e);
|
else categories.passive.effects.push(e);
|
||||||
}
|
}
|
||||||
return categories;
|
return categories;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,361 +1,370 @@
|
||||||
import TraitSelector from "../apps/trait-selector.js";
|
import TraitSelector from "../apps/trait-selector.js";
|
||||||
import { onManageActiveEffect, prepareActiveEffectCategories } from "../effects.js";
|
import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override and extend the core ItemSheet implementation to handle specific item types
|
* Override and extend the core ItemSheet implementation to handle specific item types
|
||||||
* @extends {ItemSheet}
|
* @extends {ItemSheet}
|
||||||
*/
|
*/
|
||||||
export default class ItemSheet5e extends ItemSheet {
|
export default class ItemSheet5e extends ItemSheet {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
// Expand the default size of the class sheet
|
// Expand the default size of the class sheet
|
||||||
if (this.object.data.type === "class") {
|
if (this.object.data.type === "class") {
|
||||||
this.options.width = this.position.width = 600;
|
this.options.width = this.position.width = 600;
|
||||||
this.options.height = this.position.height = 680;
|
this.options.height = this.position.height = 680;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
static get defaultOptions() {
|
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
|
||||||
width: 560,
|
|
||||||
height: 400,
|
|
||||||
classes: ["sw5e", "sheet", "item"],
|
|
||||||
resizable: true,
|
|
||||||
scrollY: [".tab.details"],
|
|
||||||
tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description" }]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
get template() {
|
|
||||||
const path = "systems/sw5e/templates/items/";
|
|
||||||
return `${path}/${this.item.data.type}.html`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @override */
|
|
||||||
async getData(options) {
|
|
||||||
const data = super.getData(options);
|
|
||||||
const itemData = data.data;
|
|
||||||
data.labels = this.item.labels;
|
|
||||||
data.config = CONFIG.SW5E;
|
|
||||||
|
|
||||||
// Item Type, Status, and Details
|
|
||||||
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
|
|
||||||
data.itemStatus = this._getItemStatus(itemData);
|
|
||||||
data.itemProperties = this._getItemProperties(itemData);
|
|
||||||
data.isPhysical = itemData.data.hasOwnProperty("quantity");
|
|
||||||
|
|
||||||
// Potential consumption targets
|
|
||||||
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
|
|
||||||
|
|
||||||
// Action Details
|
|
||||||
data.hasAttackRoll = this.item.hasAttack;
|
|
||||||
data.isHealing = itemData.data.actionType === "heal";
|
|
||||||
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
|
|
||||||
data.isLine = ["line", "wall"].includes(itemData.data.target?.type);
|
|
||||||
|
|
||||||
// Original maximum uses formula
|
|
||||||
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
|
|
||||||
if ( sourceMax ) itemData.data.uses.max = sourceMax;
|
|
||||||
|
|
||||||
// Vehicles
|
|
||||||
data.isCrewed = itemData.data.activation?.type === "crew";
|
|
||||||
data.isMountable = this._isItemMountable(itemData);
|
|
||||||
|
|
||||||
// Prepare Active Effects
|
|
||||||
data.effects = prepareActiveEffectCategories(this.item.effects);
|
|
||||||
|
|
||||||
// Re-define the template data references (backwards compatible)
|
|
||||||
data.item = itemData;
|
|
||||||
data.data = itemData.data;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the valid item consumption targets which exist on the actor
|
|
||||||
* @param {Object} item Item data for the item being displayed
|
|
||||||
* @return {{string: string}} An object of potential consumption targets
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_getItemConsumptionTargets(item) {
|
|
||||||
const consume = item.data.consume || {};
|
|
||||||
if (!consume.type) return [];
|
|
||||||
const actor = this.item.actor;
|
|
||||||
if (!actor) return {};
|
|
||||||
|
|
||||||
// Ammunition
|
|
||||||
if (consume.type === "ammo") {
|
|
||||||
return actor.itemTypes.consumable.reduce(
|
|
||||||
(ammo, i) => {
|
|
||||||
if (i.data.data.consumableType === "ammo") {
|
|
||||||
ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
|
|
||||||
}
|
|
||||||
return ammo;
|
|
||||||
},
|
|
||||||
{ [item._id]: `${item.name} (${item.data.quantity})` }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attributes
|
|
||||||
else if (consume.type === "attribute") {
|
|
||||||
const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
|
|
||||||
attributes.bar.forEach(a => a.push("value"));
|
|
||||||
return attributes.bar.concat(attributes.value).reduce((obj, a) => {
|
|
||||||
let k = a.join(".");
|
|
||||||
obj[k] = k;
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Materials
|
|
||||||
else if (consume.type === "material") {
|
|
||||||
return actor.items.reduce((obj, i) => {
|
|
||||||
if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
|
|
||||||
obj[i.id] = `${i.name} (${i.data.data.quantity})`;
|
|
||||||
}
|
}
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charges
|
/* -------------------------------------------- */
|
||||||
else if (consume.type === "charges") {
|
|
||||||
return actor.items.reduce((obj, i) => {
|
/** @inheritdoc */
|
||||||
// Limited-use items
|
static get defaultOptions() {
|
||||||
const uses = i.data.data.uses || {};
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
if (uses.per && uses.max) {
|
width: 560,
|
||||||
const label =
|
height: 400,
|
||||||
uses.per === "charges"
|
classes: ["sw5e", "sheet", "item"],
|
||||||
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})`
|
resizable: true,
|
||||||
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`;
|
scrollY: [".tab.details"],
|
||||||
obj[i.id] = i.name + label;
|
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
get template() {
|
||||||
|
const path = "systems/sw5e/templates/items/";
|
||||||
|
return `${path}/${this.item.data.type}.html`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
async getData(options) {
|
||||||
|
const data = super.getData(options);
|
||||||
|
const itemData = data.data;
|
||||||
|
data.labels = this.item.labels;
|
||||||
|
data.config = CONFIG.SW5E;
|
||||||
|
|
||||||
|
// Item Type, Status, and Details
|
||||||
|
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
|
||||||
|
data.itemStatus = this._getItemStatus(itemData);
|
||||||
|
data.itemProperties = this._getItemProperties(itemData);
|
||||||
|
data.isPhysical = itemData.data.hasOwnProperty("quantity");
|
||||||
|
|
||||||
|
// Potential consumption targets
|
||||||
|
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
|
||||||
|
|
||||||
|
// Action Details
|
||||||
|
data.hasAttackRoll = this.item.hasAttack;
|
||||||
|
data.isHealing = itemData.data.actionType === "heal";
|
||||||
|
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
|
||||||
|
data.isLine = ["line", "wall"].includes(itemData.data.target?.type);
|
||||||
|
|
||||||
|
// Original maximum uses formula
|
||||||
|
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
|
||||||
|
if (sourceMax) itemData.data.uses.max = sourceMax;
|
||||||
|
|
||||||
|
// Vehicles
|
||||||
|
data.isCrewed = itemData.data.activation?.type === "crew";
|
||||||
|
data.isMountable = this._isItemMountable(itemData);
|
||||||
|
|
||||||
|
// Prepare Active Effects
|
||||||
|
data.effects = prepareActiveEffectCategories(this.item.effects);
|
||||||
|
|
||||||
|
// Re-define the template data references (backwards compatible)
|
||||||
|
data.item = itemData;
|
||||||
|
data.data = itemData.data;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the valid item consumption targets which exist on the actor
|
||||||
|
* @param {Object} item Item data for the item being displayed
|
||||||
|
* @return {{string: string}} An object of potential consumption targets
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getItemConsumptionTargets(item) {
|
||||||
|
const consume = item.data.consume || {};
|
||||||
|
if (!consume.type) return [];
|
||||||
|
const actor = this.item.actor;
|
||||||
|
if (!actor) return {};
|
||||||
|
|
||||||
|
// Ammunition
|
||||||
|
if (consume.type === "ammo") {
|
||||||
|
return actor.itemTypes.consumable.reduce(
|
||||||
|
(ammo, i) => {
|
||||||
|
if (i.data.data.consumableType === "ammo") {
|
||||||
|
ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
|
||||||
|
}
|
||||||
|
return ammo;
|
||||||
|
},
|
||||||
|
{[item._id]: `${item.name} (${item.data.quantity})`}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recharging items
|
// Attributes
|
||||||
const recharge = i.data.data.recharge || {};
|
else if (consume.type === "attribute") {
|
||||||
if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
|
const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
|
||||||
return obj;
|
attributes.bar.forEach((a) => a.push("value"));
|
||||||
}, {});
|
return attributes.bar.concat(attributes.value).reduce((obj, a) => {
|
||||||
} else return {};
|
let k = a.join(".");
|
||||||
}
|
obj[k] = k;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Materials
|
||||||
|
else if (consume.type === "material") {
|
||||||
|
return actor.items.reduce((obj, i) => {
|
||||||
|
if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
|
||||||
|
obj[i.id] = `${i.name} (${i.data.data.quantity})`;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
// Charges
|
||||||
* Get the text item status which is shown beneath the Item type in the top-right corner of the sheet
|
else if (consume.type === "charges") {
|
||||||
* @return {string}
|
return actor.items.reduce((obj, i) => {
|
||||||
* @private
|
// Limited-use items
|
||||||
*/
|
const uses = i.data.data.uses || {};
|
||||||
_getItemStatus(item) {
|
if (uses.per && uses.max) {
|
||||||
if (item.type === "power") {
|
const label =
|
||||||
return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
|
uses.per === "charges"
|
||||||
} else if (["weapon", "equipment"].includes(item.type)) {
|
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
|
||||||
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
|
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
|
||||||
} else if (item.type === "tool") {
|
max: uses.max,
|
||||||
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
|
per: uses.per
|
||||||
}
|
})})`;
|
||||||
}
|
obj[i.id] = i.name + label;
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Recharging items
|
||||||
|
const recharge = i.data.data.recharge || {};
|
||||||
/**
|
if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
|
||||||
* Get the Array of item properties which are used in the small sidebar of the description tab
|
return obj;
|
||||||
* @return {Array}
|
}, {});
|
||||||
* @private
|
} else return {};
|
||||||
*/
|
|
||||||
_getItemProperties(item) {
|
|
||||||
const props = [];
|
|
||||||
const labels = this.item.labels;
|
|
||||||
|
|
||||||
if (item.type === "weapon") {
|
|
||||||
props.push(
|
|
||||||
...Object.entries(item.data.properties)
|
|
||||||
.filter((e) => e[1] === true)
|
|
||||||
.map((e) => CONFIG.SW5E.weaponProperties[e[0]])
|
|
||||||
);
|
|
||||||
} else if (item.type === "power") {
|
|
||||||
props.push(
|
|
||||||
labels.materials,
|
|
||||||
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
|
|
||||||
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
|
|
||||||
);
|
|
||||||
} else if (item.type === "equipment") {
|
|
||||||
props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
|
|
||||||
props.push(labels.armor);
|
|
||||||
} else if (item.type === "feat") {
|
|
||||||
props.push(labels.featType);
|
|
||||||
//TODO: Work out these
|
|
||||||
} else if (item.type === "species") {
|
|
||||||
//props.push(labels.species);
|
|
||||||
} else if (item.type === "archetype") {
|
|
||||||
//props.push(labels.archetype);
|
|
||||||
} else if (item.type === "background") {
|
|
||||||
//props.push(labels.background);
|
|
||||||
} else if (item.type === "classfeature") {
|
|
||||||
//props.push(labels.classfeature);
|
|
||||||
} else if (item.type === "deployment") {
|
|
||||||
//props.push(labels.deployment);
|
|
||||||
} else if (item.type === "venture") {
|
|
||||||
//props.push(labels.venture);
|
|
||||||
} else if (item.type === "fightingmastery") {
|
|
||||||
//props.push(labels.fightingmastery);
|
|
||||||
} else if (item.type === "fightingstyle") {
|
|
||||||
//props.push(labels.fightingstyle);
|
|
||||||
} else if (item.type === "lightsaberform") {
|
|
||||||
//props.push(labels.lightsaberform);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action type
|
/* -------------------------------------------- */
|
||||||
if (item.data.actionType) {
|
|
||||||
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
|
/**
|
||||||
|
* Get the text item status which is shown beneath the Item type in the top-right corner of the sheet
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getItemStatus(item) {
|
||||||
|
if (item.type === "power") {
|
||||||
|
return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
|
||||||
|
} else if (["weapon", "equipment"].includes(item.type)) {
|
||||||
|
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||||
|
} else if (item.type === "tool") {
|
||||||
|
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action usage
|
/* -------------------------------------------- */
|
||||||
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
|
|
||||||
props.push(labels.activation, labels.range, labels.target, labels.duration);
|
|
||||||
}
|
|
||||||
return props.filter((p) => !!p);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
|
* Get the Array of item properties which are used in the small sidebar of the description tab
|
||||||
|
* @return {Array}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getItemProperties(item) {
|
||||||
|
const props = [];
|
||||||
|
const labels = this.item.labels;
|
||||||
|
|
||||||
/**
|
if (item.type === "weapon") {
|
||||||
* Is this item a separate large object like a siege engine or vehicle
|
props.push(
|
||||||
* component that is usually mounted on fixtures rather than equipped, and
|
...Object.entries(item.data.properties)
|
||||||
* has its own AC and HP.
|
.filter((e) => e[1] === true)
|
||||||
* @param item
|
.map((e) => CONFIG.SW5E.weaponProperties[e[0]])
|
||||||
* @returns {boolean}
|
);
|
||||||
* @private
|
} else if (item.type === "power") {
|
||||||
*/
|
props.push(
|
||||||
_isItemMountable(item) {
|
labels.materials,
|
||||||
const data = item.data;
|
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
|
||||||
return (
|
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
|
||||||
(item.type === "weapon" && data.weaponType === "siege") ||
|
);
|
||||||
(item.type === "equipment" && data.armor.type === "vehicle")
|
} else if (item.type === "equipment") {
|
||||||
);
|
props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
|
||||||
}
|
props.push(labels.armor);
|
||||||
|
} else if (item.type === "feat") {
|
||||||
|
props.push(labels.featType);
|
||||||
|
//TODO: Work out these
|
||||||
|
} else if (item.type === "species") {
|
||||||
|
//props.push(labels.species);
|
||||||
|
} else if (item.type === "archetype") {
|
||||||
|
//props.push(labels.archetype);
|
||||||
|
} else if (item.type === "background") {
|
||||||
|
//props.push(labels.background);
|
||||||
|
} else if (item.type === "classfeature") {
|
||||||
|
//props.push(labels.classfeature);
|
||||||
|
} else if (item.type === "deployment") {
|
||||||
|
//props.push(labels.deployment);
|
||||||
|
} else if (item.type === "venture") {
|
||||||
|
//props.push(labels.venture);
|
||||||
|
} else if (item.type === "fightingmastery") {
|
||||||
|
//props.push(labels.fightingmastery);
|
||||||
|
} else if (item.type === "fightingstyle") {
|
||||||
|
//props.push(labels.fightingstyle);
|
||||||
|
} else if (item.type === "lightsaberform") {
|
||||||
|
//props.push(labels.lightsaberform);
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
// Action type
|
||||||
|
if (item.data.actionType) {
|
||||||
|
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
|
||||||
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
// Action usage
|
||||||
setPosition(position = {}) {
|
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
|
||||||
if (!(this._minimized || position.height)) {
|
props.push(labels.activation, labels.range, labels.target, labels.duration);
|
||||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
}
|
||||||
}
|
return props.filter((p) => !!p);
|
||||||
return super.setPosition(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
/* Form Submission */
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
_getSubmitData(updateData = {}) {
|
|
||||||
// Create the expanded update data object
|
|
||||||
const fd = new FormDataExtended(this.form, { editors: this.editors });
|
|
||||||
let data = fd.toObject();
|
|
||||||
if (updateData) data = mergeObject(data, updateData);
|
|
||||||
else data = expandObject(data);
|
|
||||||
|
|
||||||
// Handle Damage array
|
|
||||||
const damage = data.data?.damage;
|
|
||||||
if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]);
|
|
||||||
|
|
||||||
// Return the flattened submission data
|
|
||||||
return flattenObject(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
activateListeners(html) {
|
|
||||||
super.activateListeners(html);
|
|
||||||
if (this.isEditable) {
|
|
||||||
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
|
||||||
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this));
|
|
||||||
html.find(".effect-control").click((ev) => {
|
|
||||||
if (this.item.isOwned) return ui.notifications.warn("Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update.");
|
|
||||||
onManageActiveEffect(ev, this.item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or remove a damage part from the damage formula
|
|
||||||
* @param {Event} event The original click event
|
|
||||||
* @return {Promise}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
async _onDamageControl(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const a = event.currentTarget;
|
|
||||||
|
|
||||||
// Add new damage component
|
|
||||||
if (a.classList.contains("add-damage")) {
|
|
||||||
await this._onSubmit(event); // Submit any unsaved changes
|
|
||||||
const damage = this.item.data.data.damage;
|
|
||||||
return this.item.update({ "data.damage.parts": damage.parts.concat([["", ""]]) });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a damage component
|
/* -------------------------------------------- */
|
||||||
if (a.classList.contains("delete-damage")) {
|
|
||||||
await this._onSubmit(event); // Submit any unsaved changes
|
/**
|
||||||
const li = a.closest(".damage-part");
|
* Is this item a separate large object like a siege engine or vehicle
|
||||||
const damage = foundry.utils.deepClone(this.item.data.data.damage);
|
* component that is usually mounted on fixtures rather than equipped, and
|
||||||
damage.parts.splice(Number(li.dataset.damagePart), 1);
|
* has its own AC and HP.
|
||||||
return this.item.update({ "data.damage.parts": damage.parts });
|
* @param item
|
||||||
|
* @returns {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_isItemMountable(item) {
|
||||||
|
const data = item.data;
|
||||||
|
return (
|
||||||
|
(item.type === "weapon" && data.weaponType === "siege") ||
|
||||||
|
(item.type === "equipment" && data.armor.type === "vehicle")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* Handle spawning the TraitSelector application for selection various options.
|
setPosition(position = {}) {
|
||||||
* @param {Event} event The click event which originated the selection
|
if (!(this._minimized || position.height)) {
|
||||||
* @private
|
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||||
*/
|
}
|
||||||
_onConfigureTraits(event) {
|
return super.setPosition(position);
|
||||||
event.preventDefault();
|
|
||||||
const a = event.currentTarget;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
name: a.dataset.target,
|
|
||||||
title: a.parentElement.innerText,
|
|
||||||
choices: [],
|
|
||||||
allowCustom: false
|
|
||||||
};
|
|
||||||
|
|
||||||
switch(a.dataset.options) {
|
|
||||||
case 'saves':
|
|
||||||
options.choices = CONFIG.SW5E.abilities;
|
|
||||||
options.valueKey = null;
|
|
||||||
break;
|
|
||||||
case 'skills':
|
|
||||||
const skills = this.item.data.data.skills;
|
|
||||||
const choiceSet = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
|
|
||||||
options.choices = Object.fromEntries(Object.entries(CONFIG.SW5E.skills).filter(skill => choiceSet.includes(skill[0])));
|
|
||||||
options.maximum = skills.number;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
new TraitSelector(this.item, options).render(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
/* Form Submission */
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
async _onSubmit(...args) {
|
_getSubmitData(updateData = {}) {
|
||||||
if (this._tabs[0].active === "details") this.position.height = "auto";
|
// Create the expanded update data object
|
||||||
await super._onSubmit(...args);
|
const fd = new FormDataExtended(this.form, {editors: this.editors});
|
||||||
}
|
let data = fd.toObject();
|
||||||
|
if (updateData) data = mergeObject(data, updateData);
|
||||||
|
else data = expandObject(data);
|
||||||
|
|
||||||
|
// Handle Damage array
|
||||||
|
const damage = data.data?.damage;
|
||||||
|
if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]);
|
||||||
|
|
||||||
|
// Return the flattened submission data
|
||||||
|
return flattenObject(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
activateListeners(html) {
|
||||||
|
super.activateListeners(html);
|
||||||
|
if (this.isEditable) {
|
||||||
|
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
||||||
|
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this));
|
||||||
|
html.find(".effect-control").click((ev) => {
|
||||||
|
if (this.item.isOwned)
|
||||||
|
return ui.notifications.warn(
|
||||||
|
"Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."
|
||||||
|
);
|
||||||
|
onManageActiveEffect(ev, this.item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or remove a damage part from the damage formula
|
||||||
|
* @param {Event} event The original click event
|
||||||
|
* @return {Promise}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _onDamageControl(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const a = event.currentTarget;
|
||||||
|
|
||||||
|
// Add new damage component
|
||||||
|
if (a.classList.contains("add-damage")) {
|
||||||
|
await this._onSubmit(event); // Submit any unsaved changes
|
||||||
|
const damage = this.item.data.data.damage;
|
||||||
|
return this.item.update({"data.damage.parts": damage.parts.concat([["", ""]])});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a damage component
|
||||||
|
if (a.classList.contains("delete-damage")) {
|
||||||
|
await this._onSubmit(event); // Submit any unsaved changes
|
||||||
|
const li = a.closest(".damage-part");
|
||||||
|
const damage = foundry.utils.deepClone(this.item.data.data.damage);
|
||||||
|
damage.parts.splice(Number(li.dataset.damagePart), 1);
|
||||||
|
return this.item.update({"data.damage.parts": damage.parts});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle spawning the TraitSelector application for selection various options.
|
||||||
|
* @param {Event} event The click event which originated the selection
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onConfigureTraits(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const a = event.currentTarget;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
name: a.dataset.target,
|
||||||
|
title: a.parentElement.innerText,
|
||||||
|
choices: [],
|
||||||
|
allowCustom: false
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (a.dataset.options) {
|
||||||
|
case "saves":
|
||||||
|
options.choices = CONFIG.SW5E.abilities;
|
||||||
|
options.valueKey = null;
|
||||||
|
break;
|
||||||
|
case "skills":
|
||||||
|
const skills = this.item.data.data.skills;
|
||||||
|
const choiceSet =
|
||||||
|
skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
|
||||||
|
options.choices = Object.fromEntries(
|
||||||
|
Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
|
||||||
|
);
|
||||||
|
options.maximum = skills.number;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
new TraitSelector(this.item, options).render(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
async _onSubmit(...args) {
|
||||||
|
if (this._tabs[0].active === "details") this.position.height = "auto";
|
||||||
|
await super._onSubmit(...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Hotbar Macros */
|
/* Hotbar Macros */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -11,24 +10,24 @@
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export async function create5eMacro(data, slot) {
|
export async function create5eMacro(data, slot) {
|
||||||
if ( data.type !== "Item" ) return;
|
if (data.type !== "Item") return;
|
||||||
if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items");
|
if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items");
|
||||||
const item = data.data;
|
const item = data.data;
|
||||||
|
|
||||||
// Create the macro command
|
// Create the macro command
|
||||||
const command = `game.sw5e.rollItemMacro("${item.name}");`;
|
const command = `game.sw5e.rollItemMacro("${item.name}");`;
|
||||||
let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
|
let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
|
||||||
if ( !macro ) {
|
if (!macro) {
|
||||||
macro = await Macro.create({
|
macro = await Macro.create({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
type: "script",
|
type: "script",
|
||||||
img: item.img,
|
img: item.img,
|
||||||
command: command,
|
command: command,
|
||||||
flags: {"sw5e.itemMacro": true}
|
flags: {"sw5e.itemMacro": true}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
game.user.assignHotbarMacro(macro, slot);
|
game.user.assignHotbarMacro(macro, slot);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -40,20 +39,22 @@ export async function create5eMacro(data, slot) {
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export function rollItemMacro(itemName) {
|
export function rollItemMacro(itemName) {
|
||||||
const speaker = ChatMessage.getSpeaker();
|
const speaker = ChatMessage.getSpeaker();
|
||||||
let actor;
|
let actor;
|
||||||
if ( speaker.token ) actor = game.actors.tokens[speaker.token];
|
if (speaker.token) actor = game.actors.tokens[speaker.token];
|
||||||
if ( !actor ) actor = game.actors.get(speaker.actor);
|
if (!actor) actor = game.actors.get(speaker.actor);
|
||||||
|
|
||||||
// Get matching items
|
// Get matching items
|
||||||
const items = actor ? actor.items.filter(i => i.name === itemName) : [];
|
const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
|
||||||
if ( items.length > 1 ) {
|
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.`);
|
ui.notifications.warn(
|
||||||
} else if ( items.length === 0 ) {
|
`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`
|
||||||
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
|
);
|
||||||
}
|
} else if (items.length === 0) {
|
||||||
const item = items[0];
|
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
|
||||||
|
}
|
||||||
|
const item = items[0];
|
||||||
|
|
||||||
// Trigger the item roll
|
// Trigger the item roll
|
||||||
return item.roll();
|
return item.roll();
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,133 +1,132 @@
|
||||||
import { SW5E } from "../config.js";
|
import {SW5E} from "../config.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper class for building MeasuredTemplates for 5e powers and abilities
|
* A helper class for building MeasuredTemplates for 5e powers and abilities
|
||||||
* @extends {MeasuredTemplate}
|
* @extends {MeasuredTemplate}
|
||||||
*/
|
*/
|
||||||
export default 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
|
||||||
|
* @param {Item5e} item The Item object for which to construct the template
|
||||||
|
* @return {AbilityTemplate|null} The template object, or null if the item does not produce a template
|
||||||
|
*/
|
||||||
|
static fromItem(item) {
|
||||||
|
const target = getProperty(item.data, "data.target") || {};
|
||||||
|
const templateShape = SW5E.areaTargetTypes[target.type];
|
||||||
|
if (!templateShape) return null;
|
||||||
|
|
||||||
/**
|
// Prepare template data
|
||||||
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
|
const templateData = {
|
||||||
* @param {Item5e} item The Item object for which to construct the template
|
t: templateShape,
|
||||||
* @return {AbilityTemplate|null} The template object, or null if the item does not produce a template
|
user: game.user.data._id,
|
||||||
*/
|
distance: target.value,
|
||||||
static fromItem(item) {
|
direction: 0,
|
||||||
const target = getProperty(item.data, "data.target") || {};
|
x: 0,
|
||||||
const templateShape = SW5E.areaTargetTypes[target.type];
|
y: 0,
|
||||||
if ( !templateShape ) return null;
|
fillColor: game.user.color
|
||||||
|
};
|
||||||
|
|
||||||
// Prepare template data
|
// Additional type-specific data
|
||||||
const templateData = {
|
switch (templateShape) {
|
||||||
t: templateShape,
|
case "cone":
|
||||||
user: game.user.data._id,
|
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
|
||||||
distance: target.value,
|
break;
|
||||||
direction: 0,
|
case "rect": // 5e rectangular AoEs are always cubes
|
||||||
x: 0,
|
templateData.distance = Math.hypot(target.value, target.value);
|
||||||
y: 0,
|
templateData.width = target.value;
|
||||||
fillColor: game.user.color
|
templateData.direction = 45;
|
||||||
};
|
break;
|
||||||
|
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
|
||||||
|
templateData.width = target.width ?? canvas.dimensions.distance;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Additional type-specific data
|
// Return the template constructed from the item data
|
||||||
switch ( templateShape ) {
|
const cls = CONFIG.MeasuredTemplate.documentClass;
|
||||||
case "cone":
|
const template = new cls(templateData, {parent: canvas.scene});
|
||||||
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
|
const object = new this(template);
|
||||||
break;
|
object.item = item;
|
||||||
case "rect": // 5e rectangular AoEs are always cubes
|
object.actorSheet = item.actor?.sheet || null;
|
||||||
templateData.distance = Math.hypot(target.value, target.value);
|
return object;
|
||||||
templateData.width = target.value;
|
|
||||||
templateData.direction = 45;
|
|
||||||
break;
|
|
||||||
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
|
|
||||||
templateData.width = target.width ?? canvas.dimensions.distance;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the template constructed from the item data
|
/* -------------------------------------------- */
|
||||||
const cls = CONFIG.MeasuredTemplate.documentClass;
|
|
||||||
const template = new cls(templateData, {parent: canvas.scene});
|
|
||||||
const object = new this(template);
|
|
||||||
object.item = item;
|
|
||||||
object.actorSheet = item.actor?.sheet || null;
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
|
* Creates a preview of the power template
|
||||||
|
*/
|
||||||
|
drawPreview() {
|
||||||
|
const initialLayer = canvas.activeLayer;
|
||||||
|
|
||||||
/**
|
// Draw the template and switch to the template layer
|
||||||
* Creates a preview of the power template
|
this.draw();
|
||||||
*/
|
this.layer.activate();
|
||||||
drawPreview() {
|
this.layer.preview.addChild(this);
|
||||||
const initialLayer = canvas.activeLayer;
|
|
||||||
|
|
||||||
// Draw the template and switch to the template layer
|
// Hide the sheet that originated the preview
|
||||||
this.draw();
|
if (this.actorSheet) this.actorSheet.minimize();
|
||||||
this.layer.activate();
|
|
||||||
this.layer.preview.addChild(this);
|
|
||||||
|
|
||||||
// Hide the sheet that originated the preview
|
// Activate interactivity
|
||||||
if ( this.actorSheet ) this.actorSheet.minimize();
|
this.activatePreviewListeners(initialLayer);
|
||||||
|
}
|
||||||
|
|
||||||
// Activate interactivity
|
/* -------------------------------------------- */
|
||||||
this.activatePreviewListeners(initialLayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/**
|
||||||
|
* Activate listeners for the template preview
|
||||||
|
* @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete
|
||||||
|
*/
|
||||||
|
activatePreviewListeners(initialLayer) {
|
||||||
|
const handlers = {};
|
||||||
|
let moveTime = 0;
|
||||||
|
|
||||||
/**
|
// Update placement (mouse-move)
|
||||||
* Activate listeners for the template preview
|
handlers.mm = (event) => {
|
||||||
* @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete
|
event.stopPropagation();
|
||||||
*/
|
let now = Date.now(); // Apply a 20ms throttle
|
||||||
activatePreviewListeners(initialLayer) {
|
if (now - moveTime <= 20) return;
|
||||||
const handlers = {};
|
const center = event.data.getLocalPosition(this.layer);
|
||||||
let moveTime = 0;
|
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
|
||||||
|
this.data.update({x: snapped.x, y: snapped.y});
|
||||||
|
this.refresh();
|
||||||
|
moveTime = now;
|
||||||
|
};
|
||||||
|
|
||||||
// Update placement (mouse-move)
|
// Cancel the workflow (right-click)
|
||||||
handlers.mm = event => {
|
handlers.rc = (event) => {
|
||||||
event.stopPropagation();
|
this.layer.preview.removeChildren();
|
||||||
let now = Date.now(); // Apply a 20ms throttle
|
canvas.stage.off("mousemove", handlers.mm);
|
||||||
if ( now - moveTime <= 20 ) return;
|
canvas.stage.off("mousedown", handlers.lc);
|
||||||
const center = event.data.getLocalPosition(this.layer);
|
canvas.app.view.oncontextmenu = null;
|
||||||
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
|
canvas.app.view.onwheel = null;
|
||||||
this.data.update({x: snapped.x, y: snapped.y});
|
initialLayer.activate();
|
||||||
this.refresh();
|
this.actorSheet.maximize();
|
||||||
moveTime = now;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Cancel the workflow (right-click)
|
// Confirm the workflow (left-click)
|
||||||
handlers.rc = event => {
|
handlers.lc = (event) => {
|
||||||
this.layer.preview.removeChildren();
|
handlers.rc(event);
|
||||||
canvas.stage.off("mousemove", handlers.mm);
|
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
|
||||||
canvas.stage.off("mousedown", handlers.lc);
|
this.data.update(destination);
|
||||||
canvas.app.view.oncontextmenu = null;
|
canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
|
||||||
canvas.app.view.onwheel = null;
|
};
|
||||||
initialLayer.activate();
|
|
||||||
this.actorSheet.maximize();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Confirm the workflow (left-click)
|
// Rotate the template by 3 degree increments (mouse-wheel)
|
||||||
handlers.lc = event => {
|
handlers.mw = (event) => {
|
||||||
handlers.rc(event);
|
if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
|
||||||
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
|
event.stopPropagation();
|
||||||
this.data.update(destination);
|
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
|
||||||
canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
|
let snap = event.shiftKey ? delta : 5;
|
||||||
};
|
this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)});
|
||||||
|
this.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
// Rotate the template by 3 degree increments (mouse-wheel)
|
// Activate listeners
|
||||||
handlers.mw = event => {
|
canvas.stage.on("mousemove", handlers.mm);
|
||||||
if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
|
canvas.stage.on("mousedown", handlers.lc);
|
||||||
event.stopPropagation();
|
canvas.app.view.oncontextmenu = handlers.rc;
|
||||||
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
|
canvas.app.view.onwheel = handlers.mw;
|
||||||
let snap = event.shiftKey ? delta : 5;
|
}
|
||||||
this.data.update({direction: this.data.direction + (snap * Math.sign(event.deltaY))});
|
|
||||||
this.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Activate listeners
|
|
||||||
canvas.stage.on("mousemove", handlers.mm);
|
|
||||||
canvas.stage.on("mousedown", handlers.lc);
|
|
||||||
canvas.app.view.oncontextmenu = handlers.rc;
|
|
||||||
canvas.app.view.onwheel = handlers.mw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,145 +1,144 @@
|
||||||
export const registerSystemSettings = function() {
|
export const registerSystemSettings = function () {
|
||||||
|
/**
|
||||||
|
* Track the system version upon which point a migration was last applied
|
||||||
|
*/
|
||||||
|
game.settings.register("sw5e", "systemMigrationVersion", {
|
||||||
|
name: "System Migration Version",
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: String,
|
||||||
|
default: game.system.data.version
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track the system version upon which point a migration was last applied
|
* Register resting variants
|
||||||
*/
|
*/
|
||||||
game.settings.register("sw5e", "systemMigrationVersion", {
|
game.settings.register("sw5e", "restVariant", {
|
||||||
name: "System Migration Version",
|
name: "SETTINGS.5eRestN",
|
||||||
scope: "world",
|
hint: "SETTINGS.5eRestL",
|
||||||
config: false,
|
scope: "world",
|
||||||
type: String,
|
config: true,
|
||||||
default: game.system.data.version
|
default: "normal",
|
||||||
});
|
type: String,
|
||||||
|
choices: {
|
||||||
|
normal: "SETTINGS.5eRestPHB",
|
||||||
|
gritty: "SETTINGS.5eRestGritty",
|
||||||
|
epic: "SETTINGS.5eRestEpic"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register resting variants
|
* Register diagonal movement rule setting
|
||||||
*/
|
*/
|
||||||
game.settings.register("sw5e", "restVariant", {
|
game.settings.register("sw5e", "diagonalMovement", {
|
||||||
name: "SETTINGS.5eRestN",
|
name: "SETTINGS.5eDiagN",
|
||||||
hint: "SETTINGS.5eRestL",
|
hint: "SETTINGS.5eDiagL",
|
||||||
scope: "world",
|
scope: "world",
|
||||||
config: true,
|
config: true,
|
||||||
default: "normal",
|
default: "555",
|
||||||
type: String,
|
type: String,
|
||||||
choices: {
|
choices: {
|
||||||
"normal": "SETTINGS.5eRestPHB",
|
555: "SETTINGS.5eDiagPHB",
|
||||||
"gritty": "SETTINGS.5eRestGritty",
|
5105: "SETTINGS.5eDiagDMG",
|
||||||
"epic": "SETTINGS.5eRestEpic",
|
EUCL: "SETTINGS.5eDiagEuclidean"
|
||||||
}
|
},
|
||||||
});
|
onChange: (rule) => (canvas.grid.diagonalRule = rule)
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register diagonal movement rule setting
|
* Register Initiative formula setting
|
||||||
*/
|
*/
|
||||||
game.settings.register("sw5e", "diagonalMovement", {
|
game.settings.register("sw5e", "initiativeDexTiebreaker", {
|
||||||
name: "SETTINGS.5eDiagN",
|
name: "SETTINGS.5eInitTBN",
|
||||||
hint: "SETTINGS.5eDiagL",
|
hint: "SETTINGS.5eInitTBL",
|
||||||
scope: "world",
|
scope: "world",
|
||||||
config: true,
|
config: true,
|
||||||
default: "555",
|
default: false,
|
||||||
type: String,
|
type: Boolean
|
||||||
choices: {
|
});
|
||||||
"555": "SETTINGS.5eDiagPHB",
|
|
||||||
"5105": "SETTINGS.5eDiagDMG",
|
|
||||||
"EUCL": "SETTINGS.5eDiagEuclidean",
|
|
||||||
},
|
|
||||||
onChange: rule => canvas.grid.diagonalRule = rule
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register Initiative formula setting
|
* Require Currency Carrying Weight
|
||||||
*/
|
*/
|
||||||
game.settings.register("sw5e", "initiativeDexTiebreaker", {
|
game.settings.register("sw5e", "currencyWeight", {
|
||||||
name: "SETTINGS.5eInitTBN",
|
name: "SETTINGS.5eCurWtN",
|
||||||
hint: "SETTINGS.5eInitTBL",
|
hint: "SETTINGS.5eCurWtL",
|
||||||
scope: "world",
|
scope: "world",
|
||||||
config: true,
|
config: true,
|
||||||
default: false,
|
default: true,
|
||||||
type: Boolean
|
type: Boolean
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Require Currency Carrying Weight
|
* Option to disable XP bar for session-based or story-based advancement.
|
||||||
*/
|
*/
|
||||||
game.settings.register("sw5e", "currencyWeight", {
|
game.settings.register("sw5e", "disableExperienceTracking", {
|
||||||
name: "SETTINGS.5eCurWtN",
|
name: "SETTINGS.5eNoExpN",
|
||||||
hint: "SETTINGS.5eCurWtL",
|
hint: "SETTINGS.5eNoExpL",
|
||||||
scope: "world",
|
scope: "world",
|
||||||
config: true,
|
config: true,
|
||||||
default: true,
|
default: false,
|
||||||
type: Boolean
|
type: Boolean
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Option to disable XP bar for session-based or story-based advancement.
|
* Option to automatically collapse Item Card descriptions
|
||||||
*/
|
*/
|
||||||
game.settings.register("sw5e", "disableExperienceTracking", {
|
game.settings.register("sw5e", "autoCollapseItemCards", {
|
||||||
name: "SETTINGS.5eNoExpN",
|
name: "SETTINGS.5eAutoCollapseCardN",
|
||||||
hint: "SETTINGS.5eNoExpL",
|
hint: "SETTINGS.5eAutoCollapseCardL",
|
||||||
scope: "world",
|
scope: "client",
|
||||||
config: true,
|
config: true,
|
||||||
default: false,
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
});
|
onChange: (s) => {
|
||||||
|
ui.chat.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Option to automatically collapse Item Card descriptions
|
* Option to allow GMs to restrict polymorphing to GMs only.
|
||||||
*/
|
*/
|
||||||
game.settings.register("sw5e", "autoCollapseItemCards", {
|
game.settings.register("sw5e", "allowPolymorphing", {
|
||||||
name: "SETTINGS.5eAutoCollapseCardN",
|
name: "SETTINGS.5eAllowPolymorphingN",
|
||||||
hint: "SETTINGS.5eAutoCollapseCardL",
|
hint: "SETTINGS.5eAllowPolymorphingL",
|
||||||
scope: "client",
|
scope: "world",
|
||||||
config: true,
|
config: true,
|
||||||
default: false,
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean
|
||||||
onChange: s => {
|
});
|
||||||
ui.chat.render();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Option to allow GMs to restrict polymorphing to GMs only.
|
* Remember last-used polymorph settings.
|
||||||
*/
|
*/
|
||||||
game.settings.register('sw5e', 'allowPolymorphing', {
|
game.settings.register("sw5e", "polymorphSettings", {
|
||||||
name: 'SETTINGS.5eAllowPolymorphingN',
|
scope: "client",
|
||||||
hint: 'SETTINGS.5eAllowPolymorphingL',
|
default: {
|
||||||
scope: 'world',
|
keepPhysical: false,
|
||||||
config: true,
|
keepMental: false,
|
||||||
default: false,
|
keepSaves: false,
|
||||||
type: Boolean
|
keepSkills: false,
|
||||||
});
|
mergeSaves: false,
|
||||||
|
mergeSkills: false,
|
||||||
/**
|
keepClass: false,
|
||||||
* Remember last-used polymorph settings.
|
keepFeats: false,
|
||||||
*/
|
keepPowers: false,
|
||||||
game.settings.register('sw5e', 'polymorphSettings', {
|
keepItems: false,
|
||||||
scope: 'client',
|
keepBio: false,
|
||||||
default: {
|
keepVision: true,
|
||||||
keepPhysical: false,
|
transformTokens: true
|
||||||
keepMental: false,
|
}
|
||||||
keepSaves: false,
|
});
|
||||||
keepSkills: false,
|
game.settings.register("sw5e", "colorTheme", {
|
||||||
mergeSaves: false,
|
name: "SETTINGS.SWColorN",
|
||||||
mergeSkills: false,
|
hint: "SETTINGS.SWColorL",
|
||||||
keepClass: false,
|
scope: "world",
|
||||||
keepFeats: false,
|
config: true,
|
||||||
keepPowers: false,
|
default: "light",
|
||||||
keepItems: false,
|
type: String,
|
||||||
keepBio: false,
|
choices: {
|
||||||
keepVision: true,
|
light: "SETTINGS.SWColorLight",
|
||||||
transformTokens: true
|
dark: "SETTINGS.SWColorDark"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
game.settings.register("sw5e", "colorTheme", {
|
|
||||||
name: "SETTINGS.SWColorN",
|
|
||||||
hint: "SETTINGS.SWColorL",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
default: "light",
|
|
||||||
type: String,
|
|
||||||
choices: {
|
|
||||||
"light": "SETTINGS.SWColorLight",
|
|
||||||
"dark": "SETTINGS.SWColorDark"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,34 +3,33 @@
|
||||||
* Pre-loaded templates are compiled and cached for fast access when rendering
|
* Pre-loaded templates are compiled and cached for fast access when rendering
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export const preloadHandlebarsTemplates = async function() {
|
export const preloadHandlebarsTemplates = async function () {
|
||||||
return loadTemplates([
|
return loadTemplates([
|
||||||
|
// Shared Partials
|
||||||
|
"systems/sw5e/templates/actors/parts/active-effects.html",
|
||||||
|
|
||||||
// Shared Partials
|
// Actor Sheet Partials
|
||||||
"systems/sw5e/templates/actors/parts/active-effects.html",
|
"systems/sw5e/templates/actors/oldActor/parts/actor-traits.html",
|
||||||
|
"systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html",
|
||||||
|
"systems/sw5e/templates/actors/oldActor/parts/actor-features.html",
|
||||||
|
"systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html",
|
||||||
|
"systems/sw5e/templates/actors/oldActor/parts/actor-notes.html",
|
||||||
|
|
||||||
// Actor Sheet Partials
|
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
|
||||||
"systems/sw5e/templates/actors/oldActor/parts/actor-traits.html",
|
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
|
||||||
"systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html",
|
"systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
|
||||||
"systems/sw5e/templates/actors/oldActor/parts/actor-features.html",
|
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
|
||||||
"systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html",
|
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
|
||||||
"systems/sw5e/templates/actors/oldActor/parts/actor-notes.html",
|
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",
|
||||||
|
"systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html",
|
||||||
|
"systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html",
|
||||||
|
"systems/sw5e/templates/actors/newActor/parts/swalt-resources.html",
|
||||||
|
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.html",
|
||||||
|
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
|
// Item Sheet Partials
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
|
"systems/sw5e/templates/items/parts/item-action.html",
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
|
"systems/sw5e/templates/items/parts/item-activation.html",
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
|
"systems/sw5e/templates/items/parts/item-description.html",
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
|
"systems/sw5e/templates/items/parts/item-mountable.html"
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",
|
]);
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html",
|
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html",
|
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-resources.html",
|
|
||||||
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.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-mountable.html"
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,11 +3,10 @@
|
||||||
* @extends {TokenDocument}
|
* @extends {TokenDocument}
|
||||||
*/
|
*/
|
||||||
export class TokenDocument5e extends TokenDocument {
|
export class TokenDocument5e extends TokenDocument {
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
getBarAttribute(...args) {
|
getBarAttribute(...args) {
|
||||||
const data = super.getBarAttribute(...args);
|
const data = super.getBarAttribute(...args);
|
||||||
if ( data && (data.attribute === "attributes.hp") ) {
|
if (data && data.attribute === "attributes.hp") {
|
||||||
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
|
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
|
||||||
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
|
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
|
||||||
}
|
}
|
||||||
|
@ -15,19 +14,16 @@ export class TokenDocument5e extends TokenDocument {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the base Token class to implement additional system-specific logic.
|
* Extend the base Token class to implement additional system-specific logic.
|
||||||
* @extends {Token}
|
* @extends {Token}
|
||||||
*/
|
*/
|
||||||
export class Token5e extends Token {
|
export class Token5e extends Token {
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
_drawBar(number, bar, data) {
|
_drawBar(number, bar, data) {
|
||||||
if ( data.attribute === "attributes.hp" ) return this._drawHPBar(number, bar, data);
|
if (data.attribute === "attributes.hp") return this._drawHPBar(number, bar, data);
|
||||||
return super._drawBar(number, bar, data);
|
return super._drawBar(number, bar, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +37,6 @@ export class Token5e extends Token {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_drawHPBar(number, bar, data) {
|
_drawHPBar(number, bar, data) {
|
||||||
|
|
||||||
// Extract health data
|
// Extract health data
|
||||||
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
|
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
|
||||||
temp = Number(temp || 0);
|
temp = Number(temp || 0);
|
||||||
|
@ -58,42 +53,50 @@ export class Token5e extends Token {
|
||||||
|
|
||||||
// Determine colors to use
|
// Determine colors to use
|
||||||
const blk = 0x000000;
|
const blk = 0x000000;
|
||||||
const hpColor = PIXI.utils.rgb2hex([(1-(colorPct/2)), colorPct, 0]);
|
const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]);
|
||||||
const c = CONFIG.SW5E.tokenHPColors;
|
const c = CONFIG.SW5E.tokenHPColors;
|
||||||
|
|
||||||
// Determine the container size (logic borrowed from core)
|
// Determine the container size (logic borrowed from core)
|
||||||
const w = this.w;
|
const w = this.w;
|
||||||
let h = Math.max((canvas.dimensions.size / 12), 8);
|
let h = Math.max(canvas.dimensions.size / 12, 8);
|
||||||
if ( this.data.height >= 2 ) h *= 1.6;
|
if (this.data.height >= 2) h *= 1.6;
|
||||||
const bs = Math.clamped(h / 8, 1, 2);
|
const bs = Math.clamped(h / 8, 1, 2);
|
||||||
const bs1 = bs+1;
|
const bs1 = bs + 1;
|
||||||
|
|
||||||
// Overall bar container
|
// Overall bar container
|
||||||
bar.clear()
|
bar.clear();
|
||||||
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
|
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
|
||||||
|
|
||||||
// Temporary maximum HP
|
// Temporary maximum HP
|
||||||
if (tempmax > 0) {
|
if (tempmax > 0) {
|
||||||
const pct = max / effectiveMax;
|
const pct = max / effectiveMax;
|
||||||
bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
|
bar.beginFill(c.tempmax, 1.0)
|
||||||
|
.lineStyle(1, blk, 1.0)
|
||||||
|
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximum HP penalty
|
// Maximum HP penalty
|
||||||
else if (tempmax < 0) {
|
else if (tempmax < 0) {
|
||||||
const pct = (max + tempmax) / max;
|
const pct = (max + tempmax) / max;
|
||||||
bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
|
bar.beginFill(c.negmax, 1.0)
|
||||||
|
.lineStyle(1, blk, 1.0)
|
||||||
|
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health bar
|
// Health bar
|
||||||
bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, valuePct*w, h, 2)
|
bar.beginFill(hpColor, 1.0)
|
||||||
|
.lineStyle(bs, blk, 1.0)
|
||||||
|
.drawRoundedRect(0, 0, valuePct * w, h, 2);
|
||||||
|
|
||||||
// Temporary hit points
|
// Temporary hit points
|
||||||
if ( temp > 0 ) {
|
if (temp > 0) {
|
||||||
bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1);
|
bar.beginFill(c.temp, 1.0)
|
||||||
|
.lineStyle(0)
|
||||||
|
.drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set position
|
// Set position
|
||||||
let posY = (number === 0) ? (this.h - h) : 0;
|
let posY = number === 0 ? this.h - h : 0;
|
||||||
bar.position.set(0, posY);
|
bar.position.set(0, posY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
455
sw5e.js
455
sw5e.js
|
@ -8,17 +8,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Import Modules
|
// Import Modules
|
||||||
import { SW5E } from "./module/config.js";
|
import {SW5E} from "./module/config.js";
|
||||||
import { registerSystemSettings } from "./module/settings.js";
|
import {registerSystemSettings} from "./module/settings.js";
|
||||||
import { preloadHandlebarsTemplates } from "./module/templates.js";
|
import {preloadHandlebarsTemplates} from "./module/templates.js";
|
||||||
import { _getInitiativeFormula } from "./module/combat.js";
|
import {_getInitiativeFormula} from "./module/combat.js";
|
||||||
import { measureDistances } from "./module/canvas.js";
|
import {measureDistances} from "./module/canvas.js";
|
||||||
|
|
||||||
// Import Documents
|
// Import Documents
|
||||||
import Actor5e from "./module/actor/entity.js";
|
import Actor5e from "./module/actor/entity.js";
|
||||||
import Item5e from "./module/item/entity.js";
|
import Item5e from "./module/item/entity.js";
|
||||||
import CharacterImporter from "./module/characterImporter.js";
|
import CharacterImporter from "./module/characterImporter.js";
|
||||||
import { TokenDocument5e, Token5e } from "./module/token.js"
|
import {TokenDocument5e, Token5e} from "./module/token.js";
|
||||||
|
|
||||||
// Import Applications
|
// Import Applications
|
||||||
import AbilityTemplate from "./module/pixi/ability-template.js";
|
import AbilityTemplate from "./module/pixi/ability-template.js";
|
||||||
|
@ -46,122 +46,137 @@ import * as migrations from "./module/migration.js";
|
||||||
/* Foundry VTT Initialization */
|
/* Foundry VTT Initialization */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
// Keep on while migrating to Foundry version 0.8
|
Hooks.once("init", function () {
|
||||||
CONFIG.debug.hooks = true;
|
console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
|
||||||
|
|
||||||
Hooks.once("init", function() {
|
// Create a SW5E namespace within the game global
|
||||||
console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
|
game.sw5e = {
|
||||||
|
applications: {
|
||||||
|
AbilityUseDialog,
|
||||||
|
ActorSheetFlags,
|
||||||
|
ActorSheet5eCharacter,
|
||||||
|
ActorSheet5eCharacterNew,
|
||||||
|
ActorSheet5eNPC,
|
||||||
|
ActorSheet5eNPCNew,
|
||||||
|
ActorSheet5eVehicle,
|
||||||
|
ItemSheet5e,
|
||||||
|
ShortRestDialog,
|
||||||
|
TraitSelector,
|
||||||
|
ActorMovementConfig,
|
||||||
|
ActorSensesConfig
|
||||||
|
},
|
||||||
|
canvas: {
|
||||||
|
AbilityTemplate
|
||||||
|
},
|
||||||
|
config: SW5E,
|
||||||
|
dice: dice,
|
||||||
|
entities: {
|
||||||
|
Actor5e,
|
||||||
|
Item5e,
|
||||||
|
TokenDocument5e,
|
||||||
|
Token5e
|
||||||
|
},
|
||||||
|
macros: macros,
|
||||||
|
migrations: migrations,
|
||||||
|
rollItemMacro: macros.rollItemMacro
|
||||||
|
};
|
||||||
|
|
||||||
// Create a SW5E namespace within the game global
|
// Record Configuration Values
|
||||||
game.sw5e = {
|
CONFIG.SW5E = SW5E;
|
||||||
applications: {
|
CONFIG.Actor.documentClass = Actor5e;
|
||||||
AbilityUseDialog,
|
CONFIG.Item.documentClass = Item5e;
|
||||||
ActorSheetFlags,
|
CONFIG.Token.documentClass = TokenDocument5e;
|
||||||
ActorSheet5eCharacter,
|
CONFIG.Token.objectClass = Token5e;
|
||||||
ActorSheet5eCharacterNew,
|
CONFIG.time.roundTime = 6;
|
||||||
ActorSheet5eNPC,
|
CONFIG.fontFamilies = ["Engli-Besh", "Open Sans", "Russo One"];
|
||||||
ActorSheet5eNPCNew,
|
|
||||||
ActorSheet5eVehicle,
|
|
||||||
ItemSheet5e,
|
|
||||||
ShortRestDialog,
|
|
||||||
TraitSelector,
|
|
||||||
ActorMovementConfig,
|
|
||||||
ActorSensesConfig
|
|
||||||
},
|
|
||||||
canvas: {
|
|
||||||
AbilityTemplate
|
|
||||||
},
|
|
||||||
config: SW5E,
|
|
||||||
dice: dice,
|
|
||||||
entities: {
|
|
||||||
Actor5e,
|
|
||||||
Item5e,
|
|
||||||
TokenDocument5e,
|
|
||||||
Token5e,
|
|
||||||
},
|
|
||||||
macros: macros,
|
|
||||||
migrations: migrations,
|
|
||||||
rollItemMacro: macros.rollItemMacro
|
|
||||||
};
|
|
||||||
|
|
||||||
// Record Configuration Values
|
CONFIG.Dice.DamageRoll = dice.DamageRoll;
|
||||||
CONFIG.SW5E = SW5E;
|
CONFIG.Dice.D20Roll = dice.D20Roll;
|
||||||
CONFIG.Actor.documentClass = Actor5e;
|
|
||||||
CONFIG.Item.documentClass = Item5e;
|
|
||||||
CONFIG.Token.documentClass = TokenDocument5e;
|
|
||||||
CONFIG.Token.objectClass = Token5e;
|
|
||||||
CONFIG.time.roundTime = 6;
|
|
||||||
CONFIG.fontFamilies = [
|
|
||||||
"Engli-Besh",
|
|
||||||
"Open Sans",
|
|
||||||
"Russo One"
|
|
||||||
];
|
|
||||||
|
|
||||||
CONFIG.Dice.DamageRoll = dice.DamageRoll;
|
// 5e cone RAW should be 53.13 degrees
|
||||||
CONFIG.Dice.D20Roll = dice.D20Roll;
|
CONFIG.MeasuredTemplate.defaults.angle = 53.13;
|
||||||
|
|
||||||
// 5e cone RAW should be 53.13 degrees
|
// Add DND5e namespace for module compatability
|
||||||
CONFIG.MeasuredTemplate.defaults.angle = 53.13;
|
game.dnd5e = game.sw5e;
|
||||||
|
CONFIG.DND5E = CONFIG.SW5E;
|
||||||
|
|
||||||
// Add DND5e namespace for module compatability
|
// Register System Settings
|
||||||
game.dnd5e = game.sw5e;
|
registerSystemSettings();
|
||||||
CONFIG.DND5E = CONFIG.SW5E;
|
|
||||||
|
|
||||||
// Register System Settings
|
// Patch Core Functions
|
||||||
registerSystemSettings();
|
CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
|
||||||
|
Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
|
||||||
|
|
||||||
// Patch Core Functions
|
// Register Roll Extensions
|
||||||
CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
|
CONFIG.Dice.rolls.push(dice.D20Roll);
|
||||||
Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
|
CONFIG.Dice.rolls.push(dice.DamageRoll);
|
||||||
|
|
||||||
// Register Roll Extensions
|
// Register sheet application classes
|
||||||
CONFIG.Dice.rolls.push(dice.D20Roll);
|
Actors.unregisterSheet("core", ActorSheet);
|
||||||
CONFIG.Dice.rolls.push(dice.DamageRoll);
|
Actors.registerSheet("sw5e", ActorSheet5eCharacterNew, {
|
||||||
|
types: ["character"],
|
||||||
|
makeDefault: true,
|
||||||
|
label: "SW5E.SheetClassCharacter"
|
||||||
|
});
|
||||||
|
Actors.registerSheet("sw5e", ActorSheet5eCharacter, {
|
||||||
|
types: ["character"],
|
||||||
|
makeDefault: false,
|
||||||
|
label: "SW5E.SheetClassCharacterOld"
|
||||||
|
});
|
||||||
|
Actors.registerSheet("sw5e", ActorSheet5eNPCNew, {
|
||||||
|
types: ["npc"],
|
||||||
|
makeDefault: true,
|
||||||
|
label: "SW5E.SheetClassNPC"
|
||||||
|
});
|
||||||
|
Actors.registerSheet("sw5e", ActorSheet5eNPC, {
|
||||||
|
types: ["npc"],
|
||||||
|
makeDefault: false,
|
||||||
|
label: "SW5E.SheetClassNPCOld"
|
||||||
|
});
|
||||||
|
// Actors.registerSheet("sw5e", ActorSheet5eStarship, {
|
||||||
|
// types: ["starship"],
|
||||||
|
// makeDefault: true,
|
||||||
|
// label: "SW5E.SheetClassStarship"
|
||||||
|
// });
|
||||||
|
Actors.registerSheet("sw5e", ActorSheet5eVehicle, {
|
||||||
|
types: ["vehicle"],
|
||||||
|
makeDefault: true,
|
||||||
|
label: "SW5E.SheetClassVehicle"
|
||||||
|
});
|
||||||
|
Items.unregisterSheet("core", ItemSheet);
|
||||||
|
Items.registerSheet("sw5e", ItemSheet5e, {
|
||||||
|
types: [
|
||||||
|
"weapon",
|
||||||
|
"equipment",
|
||||||
|
"consumable",
|
||||||
|
"tool",
|
||||||
|
"loot",
|
||||||
|
"class",
|
||||||
|
"power",
|
||||||
|
"feat",
|
||||||
|
"species",
|
||||||
|
"backpack",
|
||||||
|
"archetype",
|
||||||
|
"classfeature",
|
||||||
|
"background",
|
||||||
|
"fightingmastery",
|
||||||
|
"fightingstyle",
|
||||||
|
"lightsaberform",
|
||||||
|
"deployment",
|
||||||
|
"deploymentfeature",
|
||||||
|
"starship",
|
||||||
|
"starshipfeature",
|
||||||
|
"starshipmod",
|
||||||
|
"venture"
|
||||||
|
],
|
||||||
|
makeDefault: true,
|
||||||
|
label: "SW5E.SheetClassItem"
|
||||||
|
});
|
||||||
|
|
||||||
// Register sheet application classes
|
// Preload Handlebars Templates
|
||||||
Actors.unregisterSheet("core", ActorSheet);
|
return preloadHandlebarsTemplates();
|
||||||
Actors.registerSheet("sw5e", ActorSheet5eCharacterNew, {
|
|
||||||
types: ["character"],
|
|
||||||
makeDefault: true,
|
|
||||||
label: "SW5E.SheetClassCharacter"
|
|
||||||
});
|
|
||||||
Actors.registerSheet("sw5e", ActorSheet5eCharacter, {
|
|
||||||
types: ["character"],
|
|
||||||
makeDefault: false,
|
|
||||||
label: "SW5E.SheetClassCharacterOld"
|
|
||||||
});
|
|
||||||
Actors.registerSheet("sw5e", ActorSheet5eNPCNew, {
|
|
||||||
types: ["npc"],
|
|
||||||
makeDefault: true,
|
|
||||||
label: "SW5E.SheetClassNPC"
|
|
||||||
});
|
|
||||||
Actors.registerSheet("sw5e", ActorSheet5eNPC, {
|
|
||||||
types: ["npc"],
|
|
||||||
makeDefault: false,
|
|
||||||
label: "SW5E.SheetClassNPCOld"
|
|
||||||
});
|
|
||||||
// Actors.registerSheet("sw5e", ActorSheet5eStarship, {
|
|
||||||
// types: ["starship"],
|
|
||||||
// makeDefault: true,
|
|
||||||
// label: "SW5E.SheetClassStarship"
|
|
||||||
// });
|
|
||||||
Actors.registerSheet('sw5e', ActorSheet5eVehicle, {
|
|
||||||
types: ['vehicle'],
|
|
||||||
makeDefault: true,
|
|
||||||
label: "SW5E.SheetClassVehicle"
|
|
||||||
});
|
|
||||||
Items.unregisterSheet("core", ItemSheet);
|
|
||||||
Items.registerSheet("sw5e", ItemSheet5e, {
|
|
||||||
types: ['weapon', 'equipment', 'consumable', 'tool', 'loot', 'class', 'power', 'feat', 'species', 'backpack', 'archetype', 'classfeature', 'background', 'fightingmastery', 'fightingstyle', 'lightsaberform', 'deployment', 'deploymentfeature', 'starship', 'starshipfeature', 'starshipmod', 'venture'],
|
|
||||||
makeDefault: true,
|
|
||||||
label: "SW5E.SheetClassItem"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Preload Handlebars Templates
|
|
||||||
return preloadHandlebarsTemplates();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Foundry VTT Setup */
|
/* Foundry VTT Setup */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
@ -169,131 +184,175 @@ Hooks.once("init", function() {
|
||||||
/**
|
/**
|
||||||
* This function runs after game data has been requested and loaded from the servers, so entities exist
|
* This function runs after game data has been requested and loaded from the servers, so entities exist
|
||||||
*/
|
*/
|
||||||
Hooks.once("setup", function() {
|
Hooks.once("setup", function () {
|
||||||
|
// Localize CONFIG objects once up-front
|
||||||
|
const toLocalize = [
|
||||||
|
"abilities",
|
||||||
|
"abilityAbbreviations",
|
||||||
|
"abilityActivationTypes",
|
||||||
|
"abilityConsumptionTypes",
|
||||||
|
"actorSizes",
|
||||||
|
"alignments",
|
||||||
|
"armorProficiencies",
|
||||||
|
"armorPropertiesTypes",
|
||||||
|
"conditionTypes",
|
||||||
|
"consumableTypes",
|
||||||
|
"cover",
|
||||||
|
"currencies",
|
||||||
|
"damageResistanceTypes",
|
||||||
|
"damageTypes",
|
||||||
|
"distanceUnits",
|
||||||
|
"equipmentTypes",
|
||||||
|
"healingTypes",
|
||||||
|
"itemActionTypes",
|
||||||
|
"languages",
|
||||||
|
"limitedUsePeriods",
|
||||||
|
"movementTypes",
|
||||||
|
"movementUnits",
|
||||||
|
"polymorphSettings",
|
||||||
|
"proficiencyLevels",
|
||||||
|
"senses",
|
||||||
|
"skills",
|
||||||
|
"starshipRolessm",
|
||||||
|
"starshipRolesmed",
|
||||||
|
"starshipRoleslg",
|
||||||
|
"starshipRoleshuge",
|
||||||
|
"starshipRolesgrg",
|
||||||
|
"starshipSkills",
|
||||||
|
"powerComponents",
|
||||||
|
"powerLevels",
|
||||||
|
"powerPreparationModes",
|
||||||
|
"powerScalingModes",
|
||||||
|
"powerSchools",
|
||||||
|
"targetTypes",
|
||||||
|
"timePeriods",
|
||||||
|
"toolProficiencies",
|
||||||
|
"weaponProficiencies",
|
||||||
|
"weaponProperties",
|
||||||
|
"weaponSizes",
|
||||||
|
"weaponTypes"
|
||||||
|
];
|
||||||
|
|
||||||
// Localize CONFIG objects once up-front
|
// Exclude some from sorting where the default order matters
|
||||||
const toLocalize = [
|
const noSort = [
|
||||||
"abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments",
|
"abilities",
|
||||||
"armorProficiencies", "armorPropertiesTypes", "conditionTypes", "consumableTypes", "cover", "currencies", "damageResistanceTypes",
|
"alignments",
|
||||||
"damageTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", "languages",
|
"currencies",
|
||||||
"limitedUsePeriods", "movementTypes", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills",
|
"distanceUnits",
|
||||||
"starshipRolessm", "starshipRolesmed", "starshipRoleslg", "starshipRoleshuge", "starshipRolesgrg", "starshipSkills",
|
"movementUnits",
|
||||||
"powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes",
|
"itemActionTypes",
|
||||||
"timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponSizes", "weaponTypes"
|
"proficiencyLevels",
|
||||||
];
|
"limitedUsePeriods",
|
||||||
|
"powerComponents",
|
||||||
|
"powerLevels",
|
||||||
|
"powerPreparationModes",
|
||||||
|
"weaponTypes"
|
||||||
|
];
|
||||||
|
|
||||||
// Exclude some from sorting where the default order matters
|
// Localize and sort CONFIG objects
|
||||||
const noSort = [
|
for (let o of toLocalize) {
|
||||||
"abilities", "alignments", "currencies", "distanceUnits", "movementUnits", "itemActionTypes", "proficiencyLevels",
|
const localized = Object.entries(CONFIG.SW5E[o]).map((e) => {
|
||||||
"limitedUsePeriods", "powerComponents", "powerLevels", "powerPreparationModes", "weaponTypes"
|
return [e[0], game.i18n.localize(e[1])];
|
||||||
];
|
});
|
||||||
|
if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1]));
|
||||||
// Localize and sort CONFIG objects
|
CONFIG.SW5E[o] = localized.reduce((obj, e) => {
|
||||||
for ( let o of toLocalize ) {
|
obj[e[0]] = e[1];
|
||||||
const localized = Object.entries(CONFIG.SW5E[o]).map(e => {
|
return obj;
|
||||||
return [e[0], game.i18n.localize(e[1])];
|
}, {});
|
||||||
});
|
}
|
||||||
if ( !noSort.includes(o) ) localized.sort((a, b) => a[1].localeCompare(b[1]));
|
// add DND5E translation for module compatability
|
||||||
CONFIG.SW5E[o] = localized.reduce((obj, e) => {
|
game.i18n.translations.DND5E = game.i18n.translations.SW5E;
|
||||||
obj[e[0]] = e[1];
|
// console.log(game.settings.get("sw5e", "colorTheme"));
|
||||||
return obj;
|
let theme = game.settings.get("sw5e", "colorTheme") + "-theme";
|
||||||
}, {});
|
document.body.classList.add(theme);
|
||||||
}
|
|
||||||
// add DND5E translation for module compatability
|
|
||||||
game.i18n.translations.DND5E = game.i18n.translations.SW5E;
|
|
||||||
// console.log(game.settings.get("sw5e", "colorTheme"));
|
|
||||||
let theme = game.settings.get("sw5e", "colorTheme") + '-theme';
|
|
||||||
document.body.classList.add(theme);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/**
|
/**
|
||||||
* Once the entire VTT framework is initialized, check to see if we should perform a data migration
|
* Once the entire VTT framework is initialized, check to see if we should perform a data migration
|
||||||
*/
|
*/
|
||||||
Hooks.once("ready", function() {
|
Hooks.once("ready", function () {
|
||||||
|
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
|
||||||
|
Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot));
|
||||||
|
|
||||||
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
|
// Determine whether a system migration is required and feasible
|
||||||
Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot));
|
if (!game.user.isGM) return;
|
||||||
|
const currentVersion = game.settings.get("sw5e", "systemMigrationVersion");
|
||||||
|
const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6";
|
||||||
|
// Check for R1 SW5E versions
|
||||||
|
const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6";
|
||||||
|
const COMPATIBLE_MIGRATION_VERSION = 0.8;
|
||||||
|
const needsMigration =
|
||||||
|
currentVersion &&
|
||||||
|
(isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) ||
|
||||||
|
isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion));
|
||||||
|
if (!needsMigration && needsMigration !== "") return;
|
||||||
|
|
||||||
// Determine whether a system migration is required and feasible
|
// Perform the migration
|
||||||
if ( !game.user.isGM ) return;
|
if (currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion)) {
|
||||||
const currentVersion = game.settings.get("sw5e", "systemMigrationVersion");
|
const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`;
|
||||||
const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6";
|
ui.notifications.error(warning, {permanent: true});
|
||||||
// Check for R1 SW5E versions
|
}
|
||||||
const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6";
|
migrations.migrateWorld();
|
||||||
const COMPATIBLE_MIGRATION_VERSION = 0.80;
|
|
||||||
const needsMigration = currentVersion && (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion));
|
|
||||||
if (!needsMigration && needsMigration !== "") return;
|
|
||||||
|
|
||||||
// Perform the migration
|
|
||||||
if ( currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion) ) {
|
|
||||||
const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`;
|
|
||||||
ui.notifications.error(warning, {permanent: true});
|
|
||||||
}
|
|
||||||
migrations.migrateWorld();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Canvas Initialization */
|
/* Canvas Initialization */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
Hooks.on("canvasInit", function() {
|
Hooks.on("canvasInit", function () {
|
||||||
// Extend Diagonal Measurement
|
// Extend Diagonal Measurement
|
||||||
canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement");
|
canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement");
|
||||||
SquareGrid.prototype.measureDistances = measureDistances;
|
SquareGrid.prototype.measureDistances = measureDistances;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
/* Other Hooks */
|
/* Other Hooks */
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
||||||
Hooks.on("renderChatMessage", (app, html, data) => {
|
Hooks.on("renderChatMessage", (app, html, data) => {
|
||||||
|
// Display action buttons
|
||||||
|
chat.displayChatActionButtons(app, html, data);
|
||||||
|
|
||||||
// Display action buttons
|
// Highlight critical success or failure die
|
||||||
chat.displayChatActionButtons(app, html, data);
|
chat.highlightCriticalSuccessFailure(app, html, data);
|
||||||
|
|
||||||
// Highlight critical success or failure die
|
// Optionally collapse the content
|
||||||
chat.highlightCriticalSuccessFailure(app, html, data);
|
if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide();
|
||||||
|
|
||||||
// Optionally collapse the content
|
|
||||||
if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide();
|
|
||||||
});
|
});
|
||||||
Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions);
|
Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions);
|
||||||
Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
|
Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
|
||||||
Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html));
|
Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html));
|
||||||
Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions);
|
Hooks.on("getActorDirectoryEntryContext", Actor5e.addDirectoryContextOptions);
|
||||||
Hooks.on("renderSceneDirectory", (app, html, data)=> {
|
Hooks.on("renderSceneDirectory", (app, html, data) => {
|
||||||
//console.log(html.find("header.folder-header"));
|
//console.log(html.find("header.folder-header"));
|
||||||
setFolderBackground(html);
|
setFolderBackground(html);
|
||||||
});
|
});
|
||||||
Hooks.on("renderActorDirectory", (app, html, data)=> {
|
Hooks.on("renderActorDirectory", (app, html, data) => {
|
||||||
setFolderBackground(html);
|
setFolderBackground(html);
|
||||||
CharacterImporter.addImportButton(html);
|
CharacterImporter.addImportButton(html);
|
||||||
});
|
});
|
||||||
Hooks.on("renderItemDirectory", (app, html, data)=> {
|
Hooks.on("renderItemDirectory", (app, html, data) => {
|
||||||
setFolderBackground(html);
|
setFolderBackground(html);
|
||||||
});
|
});
|
||||||
Hooks.on("renderJournalDirectory", (app, html, data)=> {
|
Hooks.on("renderJournalDirectory", (app, html, data) => {
|
||||||
setFolderBackground(html);
|
setFolderBackground(html);
|
||||||
});
|
});
|
||||||
Hooks.on("renderRollTableDirectory", (app, html, data)=> {
|
Hooks.on("renderRollTableDirectory", (app, html, data) => {
|
||||||
setFolderBackground(html);
|
setFolderBackground(html);
|
||||||
});
|
});
|
||||||
Hooks.on("ActorSheet5eCharacterNew", (app, html, data) => {
|
Hooks.on("ActorSheet5eCharacterNew", (app, html, data) => {
|
||||||
console.log("renderSwaltSheet");
|
console.log("renderSwaltSheet");
|
||||||
});
|
});
|
||||||
// FIXME: This helper is needed for the vehicle sheet. It should probably be refactored.
|
// FIXME: This helper is needed for the vehicle sheet. It should probably be refactored.
|
||||||
Handlebars.registerHelper('getProperty', function (data, property) {
|
Handlebars.registerHelper("getProperty", function (data, property) {
|
||||||
return getProperty(data, property);
|
return getProperty(data, property);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function setFolderBackground(html) {
|
function setFolderBackground(html) {
|
||||||
html.find("header.folder-header").each(function() {
|
html.find("header.folder-header").each(function () {
|
||||||
let bgColor = $(this).css("background-color");
|
let bgColor = $(this).css("background-color");
|
||||||
if(bgColor == undefined)
|
if (bgColor == undefined) bgColor = "rgb(255,255,255)";
|
||||||
bgColor = "rgb(255,255,255)";
|
$(this).closest("li").css("background-color", bgColor);
|
||||||
$(this).closest('li').css("background-color", bgColor);
|
});
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue