From 68a1b6a9f0600c50ce4bcecf9d23b3dd797a74c6 Mon Sep 17 00:00:00 2001 From: supervj <64861570+supervj@users.noreply.github.com> Date: Thu, 12 Nov 2020 17:30:07 -0500 Subject: [PATCH] System 1.1.1 ** Requires Foundry 0.7.6 System main update to be inline with dnd5e 1.1.1 Added active effects to as many sheets as I thought applicable. Please check loot, I made an attempt but it may be broken All .less .css and actor .html updates were made to the old actors. New actors may be broken with this update removed templates\actors\oldActor\parts\actor-effects.html for newer templates\actors\parts\active-effects.html removed module\apps\cast-dialog, templates\apps\cast-cast.html, and templates\items\cast.html. I do not think they are used, I think they were deprecated when powers were treated as items, if not we can add them back in. **NOTE** REQUIRES Foundry 0.7.6 --- lang/en.json | 23 ++ less/original/actors.less | 124 ++------ less/original/apps.less | 102 +++++++ less/original/items.less | 10 +- module/actor/entity.js | 185 +++++------- module/actor/sheets/base.js | 202 +++---------- module/actor/sheets/oldSheets/character.js | 106 +++---- module/actor/sheets/oldSheets/npc.js | 4 +- module/actor/sheets/oldSheets/vehicle.js | 16 +- module/apps/ability-use-dialog.js | 2 +- module/apps/actor-flags.js | 38 +-- module/apps/cast-dialog.js | 102 ------- module/apps/movement-config.js | 32 ++ module/apps/short-rest.js | 4 +- module/chat.js | 11 +- module/combat.js | 43 +-- module/config.js | 49 +++- module/dice.js | 273 +++++++++--------- module/effects.js | 63 ++++ module/item/entity.js | 217 +++++++------- module/item/sheet.js | 107 ++++--- module/migration.js | 105 ++++--- module/pixi/ability-template.js | 2 +- module/settings.js | 18 +- module/templates.js | 12 +- sw5e.css | 257 +++++++---------- sw5e.js | 51 ++-- system.json | 34 +-- template.json | 72 +++-- .../actors/oldActor/character-sheet.html | 28 +- templates/actors/oldActor/npc-sheet.html | 28 +- .../actors/oldActor/parts/actor-effects.html | 28 -- .../actors/oldActor/parts/actor-features.html | 10 +- .../oldActor/parts/actor-inventory.html | 75 +++-- .../oldActor/parts/actor-powerbook.html | 16 +- .../actors/oldActor/parts/actor-traits.html | 5 + templates/actors/oldActor/vehicle-sheet.html | 9 +- templates/actors/parts/active-effects.html | 38 +++ templates/apps/actor-flags.html | 72 ++--- templates/apps/cast-cast.html | 34 --- templates/apps/movement-config.html | 34 +++ templates/apps/trait-selector.html | 2 +- templates/chat/tool-card.html | 2 +- templates/items/archetype.html | 11 +- templates/items/background.html | 97 +++---- templates/items/cast.html | 151 ---------- templates/items/classfeature.html | 7 + templates/items/consumable.html | 6 + templates/items/equipment.html | 6 + templates/items/feat.html | 7 + templates/items/fightingmastery.html | 36 +-- templates/items/fightingstyle.html | 36 +-- templates/items/lightsaberform.html | 36 +-- templates/items/loot.html | 20 +- templates/items/parts/item-activation.html | 3 +- templates/items/power.html | 46 ++- templates/items/species.html | 10 +- templates/items/weapon.html | 6 + 58 files changed, 1417 insertions(+), 1706 deletions(-) delete mode 100644 module/apps/cast-dialog.js create mode 100644 module/apps/movement-config.js create mode 100644 module/effects.js delete mode 100644 templates/actors/oldActor/parts/actor-effects.html create mode 100644 templates/actors/parts/active-effects.html delete mode 100644 templates/apps/cast-cast.html create mode 100644 templates/apps/movement-config.html delete mode 100644 templates/items/cast.html diff --git a/lang/en.json b/lang/en.json index 7ceef2e9..fea235ff 100644 --- a/lang/en.json +++ b/lang/en.json @@ -263,6 +263,11 @@ "SW5E.FeatureActionRecharge": "Action Recharge", "SW5E.Flaws": "Flaws", +"SW5E.EffectCreate": "Create Effect", +"SW5E.EffectToggle": "Toggle Effect", +"SW5E.EffectEdit": "Edit Effect", +"SW5E.EffectDelete": "Delete Effect", + "SW5E.ItemTypeArchetype": "Archetype", "SW5E.ItemTypeBackground": "Background", "Sw5E.ItemTypeBackgroundPl": "Backgrounds", @@ -332,6 +337,12 @@ "SW5E.FlagsRemarkableAthleteHint": "Half-Proficiency (rounded-up) to physical Ability Checks and Initiative.", "SW5E.FlagsCritThreshold": "Critical Hit Threshold", "SW5E.FlagsCritThresholdHint": "Allow for expanded critical range; for example Improved or Superior Critical", +"SW5E.FlagsWeaponCritThreshold": "Weapon Critical Hit Threshold", +"SW5E.FlagsWeaponCritThresholdHint": "An expanded critical hit threshold for weapon attacks.", +"SW5E.FlagsPowerCritThreshold": "Power Critical Hit Threshold", +"SW5E.FlagsPowerCritThresholdHint": "An expanded critical hit threshold for power attacks.", +"SW5E.FlagsMeleeCriticalDice": "Melee Critical Damage Dice", +"SW5E.FlagsMeleeCriticalDiceHint": "A number of additional damage dice added to melee weapon critical hits.", "SW5E.Flat": "Flat", "SW5E.Formula": "Formula", @@ -582,6 +593,17 @@ "SW5E.RollMode": "Roll Mode", "SW5E.RollSituationalBonus": "Situational Bonus?", "SW5E.Save": "Save", + +"SW5E.MovementConfig": "Configure Movement Speed", +"SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.", +"SW5E.MovementWalk": "Walk", +"SW5E.MovementBurrow": "Burrow", +"SW5E.MovementClimb": "Climb", +"SW5E.MovementHover": "Hover", +"SW5E.MovementFly": "Fly", +"SW5E.MovementSwim": "Swim", +"SW5E.MovementUnits": "Units", + "SW5E.SheetClassCharacter": "Default Character Sheet", "SW5E.SheetClassCharacterOld": "Old Character Sheet", "SW5E.SheetClassNPC": "Default NPC Sheet", @@ -836,6 +858,7 @@ "SW5E.available": "available", "SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.", "SW5E.of": "of", +"SW5E.per": "per", "SW5E.power": "power", "SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.", "SETTINGS.5eAllowPolymorphingN": "Allow Polymorphing", diff --git a/less/original/actors.less b/less/original/actors.less index af469f0b..7831bd55 100644 --- a/less/original/actors.less +++ b/less/original/actors.less @@ -90,6 +90,7 @@ .russoOne(14px); color: @colorOlive; border-bottom: 1px solid @colorFaint; + white-space: nowrap; } /* ----------------------------------------- */ @@ -142,6 +143,7 @@ font-family: "Signika", sans-serif; font-size: 12px; font-weight: 400; + white-space: nowrap; } } } @@ -413,46 +415,18 @@ } } + // Inventory item lists .inventory-list { - list-style: none; - margin: 0; padding: 0 5px; - overflow-y: auto; - scrollbar-width: thin; - color: @colorTan; - - // Inventory Item .item { - line-height: 30px; - padding: 0 2px; // to align with the header border - border-bottom: 1px solid @colorFaint; - &:last-child { border-bottom: none; } - - // Item Header Name .item-name { cursor: pointer; - max-height: 30px; - overflow: hidden; - - .item-image { - flex: 0 0 30px; - background-size: 30px; - margin-right: 5px; - } - - h4 { - margin: 0; - white-space: nowrap; - overflow-x: hidden; - } - &.rollable:hover .item-image { background-image: url("../../icons/svg/d20-grey.svg") !important; } &.rollable .item-image:hover { background-image: url("../../icons/svg/d20-black.svg") !important; } - i.attuned { color: @colorTan; } @@ -474,49 +448,26 @@ flex: 0 0 80px; text-align: right; font-size: 11px; - color: @colorTan; white-space: nowrap; } } // Inventory Header .inventory-header { - margin: 2px 0; - padding: 0; - align-items: center; - background: rgba(0, 0, 0, 0.05); - border: @borderGroove; - font-weight: bold; - line-height: 24px; - - h3 { - margin: 0 -5px 0 0; - padding-left: 5px; - .russoOne(); - font-size: 16px; - } - .item-controls a.item-create { flex: 0 0 100%; } } - // Item names - .item-name { - color: @colorDark; - } - // Item Detail Sections .item-detail { flex: 0 0 70px; font-size: 12px; - color: @colorTan; text-align: center; border-right: 1px solid @colorFaint; word-break: break-word; white-space: nowrap; overflow: hidden; - &:last-child { border-right: none; } &.item-action {flex: 0 0 100px} } @@ -527,24 +478,9 @@ border-right: 1px solid @colorFaint; } - .item-list { - list-style: none; - margin: 0; - padding: 0; - } - // Item Control Buttons .item-controls { flex: 0 0 44px; - .flexrow(); - justify-content: flex-end; - - a { - flex: 0 0 22px; - font-size: 12px; - text-align: center; - color: @colorTan; - } } // Item Dropdown Summary @@ -553,6 +489,7 @@ font-size: 12px; line-height: 16px; padding: 0.25em 0.5em; + color: @colorDark; border-top: 1px solid @colorFaint; } } @@ -693,44 +630,6 @@ // Empty powerbook controls .powerbook-empty .item-controls { flex: 1; } - /* ----------------------------------------- */ - /* Active Effects */ - /* ----------------------------------------- */ - - .effects { - .effect-name{ - flex: 2; - align-items: center; - color: @colorDark; - h4 { margin: 0; } - } - - .effect-icon { - flex: 0 0 30px; - height: 30px; - margin-right: 5px; - border: none; - } - - .effect-source, - .effect-duration { - text-align: center; - border-left: 1px solid @colorFaint; - border-right: 1px solid @colorFaint; - } - - .effect-controls { - flex: 0 0 60px; - text-align: right; - } - - .effect { - align-items: center; - border-bottom: 1px solid @colorFaint; - &:last-child { border-bottom: none; } - } - } - /* ----------------------------------------- */ /* TinyMCE */ /* ----------------------------------------- */ @@ -739,3 +638,18 @@ padding: 0 8px; } } + +#actor-flags { + .window-content { + overflow-y: hidden; + } + form { + height: 100%; + } + .form-body { + height: calc(100% - 40px); + padding-right: 8px; + margin-bottom: 4px; + overflow-y: auto; + } +} \ No newline at end of file diff --git a/less/original/apps.less b/less/original/apps.less index 6963b0ec..d123b6ec 100644 --- a/less/original/apps.less +++ b/less/original/apps.less @@ -362,6 +362,108 @@ padding: 0; } } + + /* ----------------------------------------- */ + /* Items Lists */ + /* ----------------------------------------- */ + + .items-list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + scrollbar-width: thin; + color: @colorTan; + + // Child lists + .item-list { + list-style: none; + margin: 0; + padding: 0; + } + + // Individual Item + .item { + align-items: center; + padding: 0 2px; // to align with the header border + border-bottom: 1px solid @colorFaint; + &:last-child { border-bottom: none; } + + .item-name { + color: @colorDark; + .item-image { + flex: 0 0 30px; + height: 30px; + background-size: 30px; + border: none; + margin-right: 5px; + } + h4 { + margin: 0; + white-space: nowrap; + overflow-x: hidden; + } + } + } + + // Section Header + .items-header { + height: 28px; + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: @borderGroove; + font-weight: bold; + > * { + font-size: 12px; + text-align: center; + } + .item-name { + padding-left: 5px; + .modesto(); + font-size: 16px; + } + } + + // Item Name + .item-name { + flex: 2; + margin: 0; + overflow: hidden; + font-size: 13px; + text-align: left; + align-items: center; + } + + // Control Buttons + .item-controls { + flex: 0 0 60px; + justify-content: space-between; + a { + font-size: 12px; + text-align: center; + } + } + } + + /* ----------------------------------------- */ + /* Active Effects */ + /* ----------------------------------------- */ + + .effects .item { + .effect-source, + .effect-duration, + .effect-controls { + text-align: center; + border-left: 1px solid @colorFaint; + border-right: 1px solid @colorFaint; + font-size: 12px; + } + .effect-controls { + border: none; + } + } } diff --git a/less/original/items.less b/less/original/items.less index 54fa791e..239903e9 100644 --- a/less/original/items.less +++ b/less/original/items.less @@ -116,17 +116,17 @@ } .form-group.uses-per { + .form-fields { + flex-wrap: nowrap; + } input { - flex: 1; + flex: 0 0 32px; } span { flex: 0 0 16px; - } - select { - flex: 3; + margin: 0 4px 0 0; } } - span.sep { flex: 0 0 8px; } diff --git a/module/actor/entity.js b/module/actor/entity.js index ca4632a0..0b85921b 100644 --- a/module/actor/entity.js +++ b/module/actor/entity.js @@ -6,7 +6,7 @@ import AbilityTemplate from "../pixi/ability-template.js"; import {SW5E} from '../config.js'; /** - * Extend the base Actor class to implement additional logic specialized for SW5e. + * Extend the base Actor class to implement additional system-specific logic for SW5e. */ export default class Actor5e extends Actor { @@ -20,48 +20,6 @@ export default class Actor5e extends Actor { /* -------------------------------------------- */ - /** - * @override - * TODO: This becomes unnecessary after 0.7.x is released - */ - initialize() { - try { - this.prepareData(); - } catch(err) { - console.error(`Failed to initialize data for ${this.constructor.name} ${this.id}:`); - console.error(err); - } - } - - /* -------------------------------------------- */ - - /** - * @override - * TODO: This becomes unnecessary after 0.7.x is released - */ - prepareData() { - const is07x = !isNewerVersion("0.7.1", game.data.version); - if ( is07x ) this.data = duplicate(this._data); - if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN; - if ( !this.data.name ) this.data.name = "New " + this.entity; - this.prepareBaseData(); - this.prepareEmbeddedEntities(); - if ( is07x ) this.applyActiveEffects(); - this.prepareDerivedData(); - } - - /* -------------------------------------------- */ - - /** - * @override - * TODO: This becomes unnecessary after 0.7.x is released - */ - applyActiveEffects() { - if (!isNewerVersion("0.7.1", game.data.version)) return super.applyActiveEffects(); - } - - /* -------------------------------------------- */ - /** @override */ prepareBaseData() { switch ( this.data.type ) { @@ -116,6 +74,11 @@ export default class Actor5e extends Actor { abl.save = Math.max(abl.save, originalSaves[id].save); } } + + // Inventory encumbrance + data.attributes.encumbrance = this._computeEncumbrance(actorData); + + // Prepare skills this._prepareSkills(actorData, bonuses, checkBonus, originalSkills); // Determine Initiative Modifier @@ -126,7 +89,8 @@ export default class Actor5e extends Actor { if ( joat ) init.prof = Math.floor(0.5 * data.attributes.prof); else if ( athlete ) init.prof = Math.ceil(0.5 * data.attributes.prof); else init.prof = 0; - init.bonus = Number(init.value + (flags.initiativeAlert ? 5 : 0)); + init.value = init.value ?? 0; + init.bonus = init.value + (flags.initiativeAlert ? 5 : 0); init.total = init.mod + init.prof + init.bonus; // Prepare power-casting data @@ -169,7 +133,7 @@ export default class Actor5e extends Actor { } return obj; }, {}); - data.prof = this.data.data.attributes.prof; + data.prof = this.data.data.attributes.prof || 0; return data; } @@ -177,33 +141,36 @@ export default class Actor5e extends Actor { /** * Return the features which a character is awarded for each class level - * @param cls {Object} Data object for class, equivalent to Item5e.data or raw compendium entry + * @param {string} className The class name being added + * @param {string} subclassName The subclass of the class being added, if any + * @param {number} level The number of levels in the added class + * @param {number} priorLevel The previous level of the added class * @return {Promise} Array of Item5e entities */ - static async getClassFeatures(cls) { - const level = cls.data.levels; - const className = cls.name.toLowerCase(); + static async getClassFeatures({className="", subclassName="", level=1, priorLevel=0}={}) { + className = className.toLowerCase(); + subclassName = subclassName.slugify(); // Get the configuration of features which may be added const clsConfig = CONFIG.SW5E.classFeatures[className]; - if (!clsConfig) return []; - let featureIDs = clsConfig["features"][level] || []; - const subclassName = cls.data.subclass.toLowerCase().slugify(); + if (!clsConfig) return []; - // Identify subclass features - if ( subclassName !== "" ) { - const subclassConfig = clsConfig["subclasses"][subclassName]; - if ( subclassConfig !== undefined ) { - const subclassFeatureIDs = subclassConfig["features"][level]; - if ( subclassFeatureIDs ) { - featureIDs = featureIDs.concat(subclassFeatureIDs); - } - } - else console.warn("Invalid subclass: " + subclassName); + // Acquire class features + let ids = []; + for ( let [l, f] of Object.entries(clsConfig.features || {}) ) { + l = parseInt(l); + if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f); + } + + // Acquire subclass features + const subConfig = clsConfig.subclasses[subclassName] || {}; + for ( let [l, f] of Object.entries(subConfig.features || {}) ) { + l = parseInt(l); + if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f); } // Load item data for all identified features - const features = await Promise.all(featureIDs.map(id => fromUuid(id))); + const features = await Promise.all(ids.map(id => fromUuid(id))); // Class powers should always be prepared for ( const feature of features ) { @@ -237,35 +204,28 @@ export default class Actor5e extends Actor { for (let u of updated instanceof Array ? updated : [updated]) { const item = this.items.get(u._id); if (!item || (item.data.type !== "class")) continue; - const classData = duplicate(item.data); - let changed = false; + const updateData = expandObject(u); + const config = { + className: updateData.name || item.data.name, + subclassName: updateData.data.subclass || item.data.data.subclass, + level: getProperty(updateData, "data.levels"), + priorLevel: item ? item.data.data.levels : 0 + } // Get and create features for an increased class level - const newLevels = getProperty(u, "data.levels"); - if (newLevels && (newLevels > item.data.data.levels)) { - classData.data.levels = newLevels; - changed = true; - } + let changed = false; + if ( config.level && (config.level > config.priorLevel)) changed = true; + if ( config.subclassName !== item.data.data.subclass ) changed = true; - // Get features for a newly changed subclass - const newSubclass = getProperty(u, "data.subclass"); - if (newSubclass && (newSubclass !== item.data.data.subclass)) { - classData.data.subclass = newSubclass; - changed = true; - } - - // Get the new features + // Get features to create if ( changed ) { - const features = await Actor5e.getClassFeatures(classData); - if ( features.length ) toCreate.push(...features); + const existing = new Set(this.items.map(i => i.name)); + const features = await Actor5e.getClassFeatures(config); + for ( let f of features ) { + if ( !existing.has(f.name) ) toCreate.push(f); + } } } - - // De-dupe created items with ones that already exist (by name) - if ( toCreate.length ) { - const existing = new Set(this.items.map(i => i.name)); - toCreate = toCreate.filter(c => !existing.has(c.name)); - } return toCreate } @@ -301,9 +261,6 @@ export default class Actor5e extends Actor { const required = xp.max - prior; const pct = Math.round((xp.value - prior) * 100 / required); xp.pct = Math.clamped(pct, 0, 100); - - // Inventory encumbrance - data.attributes.encumbrance = this._computeEncumbrance(actorData); } /* -------------------------------------------- */ @@ -369,17 +326,17 @@ export default class Actor5e extends Actor { } if ( joat && (skl.value === 0 ) ) multi = 0.5; + // Retain the maximum skill proficiency when skill proficiencies are merged + if ( originalSkills ) { + skl.value = Math.max(skl.value, originalSkills[id].value); + } + // Compute modifier skl.bonus = checkBonus + skillBonus; skl.mod = data.abilities[skl.ability].mod; skl.prof = round(multi * data.attributes.prof); skl.total = skl.mod + skl.prof + skl.bonus; - // If we merged skills when transforming, take the highest bonus here. - if (originalSkills && skl.value > 0.5) { - skl.total = Math.max(skl.total, originalSkills[id].total); - } - // Compute passive bonus const passive = observant && (feats.observantFeat.skills.includes(id)) ? 5 : 0; skl.passive = 10 + skl.total + passive; @@ -489,8 +446,8 @@ export default class Actor5e extends Actor { else powers.pact.max = Math.max(1, Math.min(pl, 2), Math.min(pl - 8, 3), Math.min(pl - 13, 4)); powers.pact.value = Math.min(powers.pact.value, powers.pact.max); } else { - powers.pact.level = 0; - powers.pact.max = 0; + powers.pact.max = parseInt(powers.pact.override) || 0 + powers.pact.level = powers.pact.max > 0 ? 1 : 0; } } @@ -513,14 +470,14 @@ export default class Actor5e extends Actor { if ( !physicalItems.includes(i.type) ) return weight; const q = i.data.quantity || 0; const w = i.data.weight || 0; - return weight + Math.round(q * w * 10) / 10; + return weight + (q * w); }, 0); // [Optional] add Currency Weight if ( game.settings.get("sw5e", "currencyWeight") ) { const currency = actorData.data.currency; const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0); - weight += Math.round((numCoins * 10) / CONFIG.SW5E.encumbrance.currencyPerWeight) / 10; + weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; } // Determine the encumbrance size class @@ -535,9 +492,10 @@ export default class Actor5e extends Actor { if ( this.getFlag("sw5e", "powerfulBuild") ) mod = Math.min(mod * 2, 8); // Compute Encumbrance percentage + weight = weight.toNearest(0.1); const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod; - const pct = Math.clamped((weight* 100) / max, 0, 100); - return { value: weight, max, pct, encumbered: pct > (2/3) }; + const pct = Math.clamped((weight * 100) / max, 0, 100); + return { value: weight.toNearest(0.1), max, pct, encumbered: pct > (2/3) }; } /* -------------------------------------------- */ @@ -564,9 +522,6 @@ export default class Actor5e extends Actor { /** @override */ async update(data, options={}) { - // TODO: 0.7.1 compatibility - remove when stable - if ( !data.hasOwnProperty("data") ) data = expandObject(data); - // Apply changes in Actor size to Token width/height const newSize = getProperty(data, "data.traits.size"); if ( newSize && (newSize !== getProperty(this.data, "data.traits.size")) ) { @@ -577,7 +532,7 @@ export default class Actor5e extends Actor { data["token.width"] = size; } } - + // Reset death save counters if ( (this.data.data.attributes.hp.value <= 0) && (getProperty(data, "data.attributes.hp.value") > 0) ) { setProperty(data, "data.attributes.death.success", 0); @@ -858,7 +813,7 @@ export default class Actor5e extends Actor { rollAbilitySave(abilityId, options={}) { const label = CONFIG.SW5E.abilities[abilityId]; const abl = this.data.data.abilities[abilityId]; - + // Construct parts const parts = ["@mod"]; const data = {mod: abl.mod}; @@ -937,7 +892,7 @@ export default class Actor5e extends Actor { // Take action depending on the result const success = roll.total >= 10; - const d20 = roll.dice[0].total; + const d20 = roll.dice[0].total; // Save success if ( success ) { @@ -1429,14 +1384,16 @@ export default class Actor5e extends Actor { if ( !original ) return; // Get the Tokens which represent this actor - const tokens = this.getActiveTokens(true); - const tokenUpdates = tokens.map(t => { - const tokenData = duplicate(original.data.token); - tokenData._id = t.id; - tokenData.actorId = original.id; - return tokenData; - }); - canvas.scene.updateEmbeddedEntity("Token", tokenUpdates); + if ( canvas.ready ) { + const tokens = this.getActiveTokens(true); + const tokenUpdates = tokens.map(t => { + const tokenData = duplicate(original.data.token); + tokenData._id = t.id; + tokenData.actorId = original.id; + return tokenData; + }); + canvas.scene.updateEmbeddedEntity("Token", tokenUpdates); + } // Delete the polymorphed Actor and maybe re-render the original sheet const isRendered = this.sheet.rendered; diff --git a/module/actor/sheets/base.js b/module/actor/sheets/base.js index 557aba7b..b21dc715 100644 --- a/module/actor/sheets/base.js +++ b/module/actor/sheets/base.js @@ -1,10 +1,12 @@ import Item5e from "../../item/entity.js"; import TraitSelector from "../../apps/trait-selector.js"; import ActorSheetFlags from "../../apps/actor-flags.js"; +import MovementConfig from "../../apps/movement-config.js"; import {SW5E} from '../../config.js'; +import {onManageActiveEffect, prepareActiveEffectCategories} from "../../effects.js"; /** - * Extend the basic ActorSheet class to do all the SW5e things! + * Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality. * This sheet is an Abstract layer which is not used. * @extends {ActorSheet} */ @@ -94,6 +96,9 @@ export default class ActorSheet5e extends ActorSheet { } } + // Movement speeds + data.movement = this._getMovementSpeed(data.actor); + // Update traits this._prepareTraits(data.actor.data.traits); @@ -101,7 +106,7 @@ export default class ActorSheet5e extends ActorSheet { this._prepareItems(data); // Prepare active effects - this._prepareEffects(data); + data.effects = prepareActiveEffectCategories(this.entity.effects); // Return data to the sheet return data @@ -109,6 +114,28 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ + /** + * Prepare the display of movement speed data for the Actor + * @param {object} actorData + * @returns {{primary: string, special: string}} + * @private + */ + _getMovementSpeed(actorData) { + const movement = actorData.data.attributes.movement; + const speeds = [ + [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], + [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], + [movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")], + [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] + ].filter(s => !!s[0]).sort((a, b) => b[0] - a[0]); + return { + primary: `${movement.walk || 0} ${movement.units}`, + special: speeds.length ? speeds.map(s => s[1]).join(", ") : "" + } + } + + /* -------------------------------------------- */ + /** * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies * @param {object} traits The raw traits data object from the actor data @@ -147,43 +174,6 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ - /** - * Prepare the data structure for Active Effects which are currently applied to the Actor. - * @param {object} data The object of rendering data which is being prepared - * @private - */ - _prepareEffects(data) { - - // Define effect header categories - const categories = { - temporary: { - label: "Temporary Effects", - effects: [] - }, - passive: { - label: "Passive Effects", - effects: [] - }, - inactive: { - label: "Inactive Effects", - effects: [] - } - }; - - // Iterate over active effects, classifying them into categories - for ( let e of this.actor.effects ) { - e._getSourceName(); // Trigger a lookup for the source name - if ( e.data.disabled ) categories.inactive.effects.push(e); - else if ( e.isTemporary ) categories.temporary.effects.push(e); - else categories.passive.effects.push(e); - } - - // Add the prepared categories of effects to the rendering data - return data.effects = categories; - } - - /* -------------------------------------------- */ - /** * Insert a power into the powerbook object when rendering the character sheet * @param {Object} data The Actor data being prepared @@ -242,7 +232,7 @@ export default class ActorSheet5e extends ActorSheet { registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); } } - + // Pact magic users have cantrips and a pact magic section if ( levels.pact && levels.pact.max ) { if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); @@ -363,7 +353,7 @@ export default class ActorSheet5e extends ActorSheet { filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); // Item summaries - html.find('.item .item-name h4').click(event => this._onItemSummary(event)); + html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event)); // Editable Only Listeners if ( this.isEditable ) { @@ -383,6 +373,7 @@ export default class ActorSheet5e extends ActorSheet { html.find('.trait-selector').click(this._onTraitSelector.bind(this)); // Configure Special Flags + html.find('.configure-movement').click(this._onMovementConfig.bind(this)); html.find('.configure-flags').click(this._onConfigureFlags.bind(this)); // Owned Item management @@ -393,8 +384,7 @@ export default class ActorSheet5e extends ActorSheet { html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this)); // Active Effect management - html.find(".effect-control").click(this._onManageActiveEffect.bind(this)); - + html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity)); } // Owner Only Listeners @@ -565,7 +555,7 @@ export default class ActorSheet5e extends ActorSheet { } /* -------------------------------------------- */ - + /** @override */ async _onDropItemCreate(itemData) { @@ -576,9 +566,7 @@ export default class ActorSheet5e extends ActorSheet { } // Create the owned item as normal - // TODO remove conditional logic in 0.7.x - if (isNewerVersion(game.data.version, "0.6.9")) return super._onDropItemCreate(itemData); - else return this.actor.createEmbeddedEntity("OwnedItem", itemData); + return super._onDropItemCreate(itemData); } /* -------------------------------------------- */ @@ -731,28 +719,6 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ - /** - * Manage Active Effect instances through the Actor Sheet via effect control buttons. - * @param {MouseEvent} event The left-click event on the effect control - * @private - */ - _onManageActiveEffect(event) { - event.preventDefault(); - const a = event.currentTarget; - const li = a.closest(".effect"); - const effect = this.actor.effects.get(li.dataset.effectId); - switch ( a.dataset.action ) { - case "edit": - return effect.sheet.render(true); - case "delete": - return effect.delete(); - case "toggle": - return effect.update({disabled: !effect.data.disabled}); - } - } - - /* -------------------------------------------- */ - /** * Handle rolling an Ability check, either a test or a saving throw * @param {Event} event The originating click event @@ -825,6 +791,18 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onMovementConfig(event) { + event.preventDefault(); + new MovementConfig(this.object).render(true); + } + + /* -------------------------------------------- */ + /** @override */ _getHeaderButtons() { let buttons = super._getHeaderButtons(); @@ -839,90 +817,4 @@ export default class ActorSheet5e extends ActorSheet { }); return buttons; } - - /* -------------------------------------------- */ - /* DEPRECATED */ - /* -------------------------------------------- */ - - /** - * TODO: Remove once 0.7.x is release - * @deprecated since 0.7.0 - */ - async _onDrop (event) { - event.preventDefault(); - - // Get dropped data - let data; - try { - data = JSON.parse(event.dataTransfer.getData('text/plain')); - } catch (err) { - return false; - } - if ( !data ) return false; - - // Handle the drop with a Hooked function - const allowed = Hooks.call("dropActorSheetData", this.actor, this, data); - if ( allowed === false ) return; - - // Case 1 - Dropped Item - if ( data.type === "Item" ) { - return this._onDropItem(event, data); - } - - // Case 2 - Dropped Actor - if ( data.type === "Actor" ) { - return this._onDropActor(event, data); - } - } - - /* -------------------------------------------- */ - - /** - * TODO: Remove once 0.7.x is release - * @deprecated since 0.7.0 - */ - async _onDropItem(event, data) { - if ( !this.actor.owner ) return false; - let itemData = await this._getItemDropData(event, data); - - // Handle item sorting within the same Actor - const actor = this.actor; - let sameActor = (data.actorId === actor._id) || (actor.isToken && (data.tokenId === actor.token.id)); - if (sameActor) return this._onSortItem(event, itemData); - - // Create a new item - this._onDropItemCreate(itemData); - } - - /* -------------------------------------------- */ - - /** - * TODO: Remove once 0.7.x is release - * @deprecated since 0.7.0 - */ - async _getItemDropData(event, data) { - let itemData = null; - - // Case 1 - Import from a Compendium pack - if (data.pack) { - const pack = game.packs.get(data.pack); - if (pack.metadata.entity !== "Item") return; - itemData = await pack.getEntry(data.id); - } - - // Case 2 - Data explicitly provided - else if (data.data) { - itemData = data.data; - } - - // Case 3 - Import from World entity - else { - let item = game.items.get(data.id); - if (!item) return; - itemData = item.data; - } - - // Return a copy of the extracted data - return duplicate(itemData); - } } \ No newline at end of file diff --git a/module/actor/sheets/oldSheets/character.js b/module/actor/sheets/oldSheets/character.js index a36ed6a6..5c7e375e 100644 --- a/module/actor/sheets/oldSheets/character.js +++ b/module/actor/sheets/oldSheets/character.js @@ -68,7 +68,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} }, loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} } }; - + // Partition items by category let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { @@ -109,7 +109,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { for ( let i of items ) { i.data.quantity = i.data.quantity || 0; i.data.weight = i.data.weight || 0; - i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10; + i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); inventory[i.type].items.push(i); } @@ -123,12 +123,12 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { const features = { classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true }, - archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, - species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, - background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, - fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, - fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, - lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, + archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, + species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, + background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, + fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, + fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, + lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } }; @@ -138,13 +138,13 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { } classes.sort((a, b) => b.levels - a.levels); features.classes.items = classes; - features.classfeatures.items = classfeatures; - features.archetype.items = archetypes; - features.species.items = species; - features.background.items = backgrounds; - features.fightingstyles.items = fightingstyles; - features.fightingmasteries.items = fightingmasteries; - features.lightsaberforms.items = lightsaberforms; + features.classfeatures.items = classfeatures; + features.archetype.items = archetypes; + features.species.items = species; + features.background.items = backgrounds; + features.fightingstyles.items = fightingstyles; + features.fightingmasteries.items = fightingmasteries; + features.lightsaberforms.items = lightsaberforms; // Assign and return data.inventory = Object.values(inventory); @@ -189,9 +189,6 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { super.activateListeners(html); if ( !this.options.editable ) return; - // Inventory Functions - html.find(".currency-convert").click(this._onConvertCurrency.bind(this)); - // Item State Toggling html.find('.item-toggle').click(this._onToggleItem.bind(this)); @@ -199,8 +196,8 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { html.find('.short-rest').click(this._onShortRest.bind(this)); html.find('.long-rest').click(this._onLongRest.bind(this)); - // Death saving throws - html.find('.death-save').click(this._onDeathSave.bind(this)); + // Rollable sheet actions + html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); } /* -------------------------------------------- */ @@ -210,14 +207,19 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { * @param {MouseEvent} event The originating click event * @private */ - _onDeathSave(event) { + _onSheetAction(event) { event.preventDefault(); - return this.actor.rollDeathSave({event: event}); + const button = event.currentTarget; + switch( button.dataset.action ) { + case "rollDeathSave": + return this.actor.rollDeathSave({event: event}); + case "rollInitiative": + return this.actor.rollInitiative({createCombatants: true}); + } } /* -------------------------------------------- */ - /** * Handle toggling the state of an Owned Item within the Actor * @param {Event} event The triggering click event @@ -259,53 +261,39 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { /* -------------------------------------------- */ - /** - * Handle mouse click events to convert currency to the highest possible denomination - * @param {MouseEvent} event The originating click event - * @private - */ - async _onConvertCurrency(event) { - event.preventDefault(); - return Dialog.confirm({ - title: `${game.i18n.localize("SW5E.CurrencyConvert")}`, - content: `

${game.i18n.localize("SW5E.CurrencyConvertHint")}

`, - yes: () => this.actor.convertCurrency() - }); - } - - /* -------------------------------------------- */ - /** @override */ async _onDropItemCreate(itemData) { + let addLevel = false; // Upgrade the number of class levels a character has and add features if ( itemData.type === "class" ) { const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); - const classWasAlreadyPresent = !!cls; + let priorLevel = cls?.data.data.levels ?? 0; + const hasClass = !!cls; - // Add new features for class level - if ( !classWasAlreadyPresent ) { - Actor5e.getClassFeatures(itemData).then(features => { - this.actor.createEmbeddedEntity("OwnedItem", features); - }); + // Increment levels instead of creating a new item + if ( hasClass ) { + const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); + if ( next > priorLevel ) { + itemData.levels = next; + await cls.update({"data.levels": next}); + addLevel = true; + } } - // If the actor already has the class, increment the level instead of creating a new item - // then add new features as long as level increases - if ( classWasAlreadyPresent ) { - const lvl = cls.data.data.levels; - const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level); - if ( !(lvl === newLvl) ) { - cls.update({"data.levels": newLvl}); - itemData.data.levels = newLvl; - Actor5e.getClassFeatures(itemData).then(features => { - this.actor.createEmbeddedEntity("OwnedItem", features); - }); - } - return + // Add class features + if ( !hasClass || addLevel ) { + const features = await Actor5e.getClassFeatures({ + className: itemData.name, + subclassName: itemData.data.subclass, + level: itemData.levels, + priorLevel: priorLevel + }); + await this.actor.createEmbeddedEntity("OwnedItem", features); } } - super._onDropItemCreate(itemData); + // Default drop handling if levels were not added + if ( !addLevel ) super._onDropItemCreate(itemData); } } diff --git a/module/actor/sheets/oldSheets/npc.js b/module/actor/sheets/oldSheets/npc.js index d5b6bff8..124fa8b2 100644 --- a/module/actor/sheets/oldSheets/npc.js +++ b/module/actor/sheets/oldSheets/npc.js @@ -106,7 +106,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e { /** @override */ activateListeners(html) { super.activateListeners(html); - html.find(".health .rollable").click(this._onRollHealthFormula.bind(this)); + html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); } /* -------------------------------------------- */ @@ -116,7 +116,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e { * @param {Event} event The original click event * @private */ - _onRollHealthFormula(event) { + _onRollHPFormula(event) { event.preventDefault(); const formula = this.actor.data.data.attributes.hp.formula; if ( !formula ) return; diff --git a/module/actor/sheets/oldSheets/vehicle.js b/module/actor/sheets/oldSheets/vehicle.js index 099cf606..602f984c 100644 --- a/module/actor/sheets/oldSheets/vehicle.js +++ b/module/actor/sheets/oldSheets/vehicle.js @@ -49,12 +49,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; // Compute overall encumbrance - const enc = { - max: actorData.data.attributes.capacity.cargo, - value: Math.round(totalWeight * 10) / 10 - }; - enc.pct = Math.min(enc.value * 100 / enc.max, 99); - return enc; + const max = actorData.data.attributes.capacity.cargo; + const pct = Math.clamped((totalWeight * 100) / max, 0, 100); + return {value: totalWeight.toNearest(0.1), max, pct}; } /* -------------------------------------------- */ @@ -89,6 +86,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { /* -------------------------------------------- */ + /** @override */ + _getMovementSpeed(actorData) { + return {primary: "", special: ""}; + } + + /* -------------------------------------------- */ + /** * Organize Owned Items for rendering the Vehicle sheet. * @private diff --git a/module/apps/ability-use-dialog.js b/module/apps/ability-use-dialog.js index 13fe0963..8a914e71 100644 --- a/module/apps/ability-use-dialog.js +++ b/module/apps/ability-use-dialog.js @@ -70,7 +70,7 @@ export default class AbilityUseDialog extends Dialog { dlg.render(true); }); } - + /* -------------------------------------------- */ /* Helpers */ /* -------------------------------------------- */ diff --git a/module/apps/actor-flags.js b/module/apps/actor-flags.js index 9d7a9da1..16241782 100644 --- a/module/apps/actor-flags.js +++ b/module/apps/actor-flags.js @@ -1,9 +1,9 @@ /** * An application class which provides advanced configuration for special character flags which modify an Actor - * @extends {BaseEntitySheet} + * @implements {BaseEntitySheet} */ export default class ActorSheetFlags extends BaseEntitySheet { - static get defaultOptions() { + static get defaultOptions() { const options = super.defaultOptions; return mergeObject(options, { id: "actor-flags", @@ -16,22 +16,16 @@ export default class ActorSheetFlags extends BaseEntitySheet { /* -------------------------------------------- */ - /** - * Configure the title of the special traits selection window to include the Actor name - * @type {String} - */ + /** @override */ get title() { return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`; } /* -------------------------------------------- */ - /** - * Prepare data used to render the special Actor traits selection UI - * @return {Object} - */ + /** @override */ getData() { - const data = super.getData(); + const data = {}; data.actor = this.object; data.flags = this._getFlags(); data.bonuses = this._getBonuses(); @@ -43,17 +37,18 @@ export default class ActorSheetFlags extends BaseEntitySheet { /** * Prepare an object of flags data which groups flags by section * Add some additional data for rendering - * @return {Object} + * @return {object} */ _getFlags() { const flags = {}; + const baseData = this.entity._data; for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) { if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {}; let flag = duplicate(v); flag.type = v.type.name; flag.isCheckbox = v.type === Boolean; flag.isSelect = v.hasOwnProperty('choices'); - flag.value = this.entity.getFlag("sw5e", k); + flag.value = getProperty(baseData.flags, `sw5e.${k}`); flags[v.section][`flags.sw5e.${k}`] = flag; } return flags; @@ -63,7 +58,7 @@ export default class ActorSheetFlags extends BaseEntitySheet { /** * Get the bonuses fields and their localization strings - * @return {Array} + * @return {Array} * @private */ _getBonuses() { @@ -82,17 +77,14 @@ export default class ActorSheetFlags extends BaseEntitySheet { {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"} ]; for ( let b of bonuses ) { - b.value = getProperty(this.object.data, b.name) || ""; + b.value = getProperty(this.object._data, b.name) || ""; } return bonuses; } /* -------------------------------------------- */ - /** - * Update the Actor using the configured flags - * Remove/unset any flags which are no longer configured - */ + /** @override */ async _updateObject(event, formData) { const actor = this.object; let updateData = expandObject(formData); @@ -100,10 +92,12 @@ export default class ActorSheetFlags extends BaseEntitySheet { // 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}`) ) { + if ( hasProperty(actor._data.flags, `sw5e.${k}`) ) { unset = true; flags[`-=${k}`] = null; } @@ -118,10 +112,6 @@ export default class ActorSheetFlags extends BaseEntitySheet { } // Diff the data against any applied overrides and apply - // TODO: Remove this logical gate once 0.7.x is release channel - if ( !isNewerVersion("0.7.1", game.data.version) ){ - updateData = diffObject(this.object.data, updateData); - } await actor.update(updateData, {diff: false}); } } diff --git a/module/apps/cast-dialog.js b/module/apps/cast-dialog.js deleted file mode 100644 index e31bfb72..00000000 --- a/module/apps/cast-dialog.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * A specialized Dialog subclass for casting a cast item at a certain level - * @type {Dialog} - */ -export class CastDialog extends Dialog { - constructor(actor, item, dialogData={}, options={}) { - super(dialogData, options); - this.options.classes = ["sw5e", "dialog"]; - - /** - * Store a reference to the Actor entity which is casting the cast - * @type {Actor5e} - */ - this.actor = actor; - - /** - * Store a reference to the Item entity which is the cast being cast - * @type {Item5e} - */ - this.item = item; - } - - /* -------------------------------------------- */ - /* Rendering */ - /* -------------------------------------------- */ - - /** - * A constructor function which displays the Cast Cast Dialog app for a given Actor and Item. - * Returns a Promise which resolves to the dialog FormData once the workflow has been completed. - * @param {Actor5e} actor - * @param {Item5e} item - * @return {Promise} - */ - static async create(actor, item) { - const ad = actor.data.data; - const id = item.data.data; - - // Determine whether the cast may be upcast - const lvl = id.level; - const canUpcast = (lvl > 0) && CONFIG.SW5E.castUpcastModes.includes(id.preparation.mode); - - // Determine the levels which are feasible - let lmax = 0; - const castLevels = Array.fromRange(10).reduce((arr, i) => { - if ( i < lvl ) return arr; - const l = ad.casts["cast"+i] || {max: 0, override: null}; - let max = parseInt(l.override || l.max || 0); - let slots = Math.clamped(parseInt(l.value || 0), 0, max); - if ( max > 0 ) lmax = i; - arr.push({ - level: i, - label: i > 0 ? `${CONFIG.SW5E.castLevels[i]} (${slots} Slots)` : CONFIG.SW5E.castLevels[i], - canCast: canUpcast && (max > 0), - hasSlots: slots > 0 - }); - return arr; - }, []).filter(sl => sl.level <= lmax); - - const pact = ad.casts.pact; - if (pact.level >= lvl) { - // If this character has pact slots, present them as an option for - // casting the cast. - castLevels.push({ - level: 'pact', - label: game.i18n.localize('SW5E.CastLevelPact') - + ` (${game.i18n.localize('SW5E.Level')} ${pact.level}) ` - + `(${pact.value} ${game.i18n.localize('SW5E.Slots')})`, - canCast: canUpcast, - hasSlots: pact.value > 0 - }); - } - - const canCast = castLevels.some(l => l.hasSlots); - - // Render the Cast casting template - const html = await renderTemplate("systems/sw5e/templates/apps/cast-cast.html", { - item: item.data, - canCast: canCast, - canUpcast: canUpcast, - castLevels, - hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget - }); - - // Create the Dialog and return as a Promise - return new Promise((resolve, reject) => { - const dlg = new this(actor, item, { - title: `${item.name}: Cast Configuration`, - content: html, - buttons: { - cast: { - icon: '', - label: "Cast", - callback: html => resolve(new FormData(html[0].querySelector("#cast-config-form"))) - } - }, - default: "cast", - close: reject - }); - dlg.render(true); - }); - } -} diff --git a/module/apps/movement-config.js b/module/apps/movement-config.js new file mode 100644 index 00000000..3d60a582 --- /dev/null +++ b/module/apps/movement-config.js @@ -0,0 +1,32 @@ +/** + * A simple form to set actor movement speeds + * @implements {BaseEntitySheet} + */ +export default class MovementConfig extends BaseEntitySheet { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + title: "SW5E.MovementConfig", + classes: ["sw5e"], + template: "systems/sw5e/templates/apps/movement-config.html", + width: 240, + height: "auto" + }); + } + + /* -------------------------------------------- */ + + /** @override */ + getData(options) { + const data = { + movement: duplicate(this.entity._data.data.attributes.movement), + units: CONFIG.SW5E.movementUnits + } + for ( let [k, v] of Object.entries(data.movement) ) { + if ( ["units", "hover"].includes(k) ) continue; + data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0; + } + return data; + } +} diff --git a/module/apps/short-rest.js b/module/apps/short-rest.js index 6394b531..6791cbac 100644 --- a/module/apps/short-rest.js +++ b/module/apps/short-rest.js @@ -36,7 +36,7 @@ export default class ShortRestDialog extends Dialog { /** @override */ getData() { const data = super.getData(); - + // Determine Hit Dice data.availableHD = this.actor.data.items.reduce((hd, item) => { if ( item.type === "class" ) { @@ -49,7 +49,7 @@ export default class ShortRestDialog extends Dialog { }, {}); data.canRoll = this.actor.data.data.attributes.hd > 0; data.denomination = this._denom; - + // Determine rest type const variant = game.settings.get("sw5e", "restVariant"); data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute diff --git a/module/chat.js b/module/chat.js index 0d4bcd3d..f5b72e8d 100644 --- a/module/chat.js +++ b/module/chat.js @@ -11,14 +11,16 @@ export const highlightCriticalSuccessFailure = function(message, html, data) { const d = roll.dice[0]; // Ensure it is an un-modified d20 roll - const isD20 = (d.faces === 20) && ( d.results.length === 1 ); + const isD20 = (d.faces === 20) && ( d.values.length === 1 ); if ( !isD20 ) return; const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure; if ( isModifiedRoll ) return; // Highlight successes and failures - if ( d.options.critical && (d.total >= d.options.critical) ) html.find(".dice-total").addClass("critical"); - else if ( d.options.fumble && (d.total <= d.options.fumble) ) html.find(".dice-total").addClass("fumble"); + const critical = d.options.critical || 20; + const fumble = d.options.fumble || 1; + if ( d.total >= critical ) html.find(".dice-total").addClass("critical"); + else if ( d.total <= fumble ) html.find(".dice-total").addClass("fumble"); else if ( d.options.target ) { if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success"); else html.find(".dice-total").addClass("failure"); @@ -33,7 +35,8 @@ export const highlightCriticalSuccessFailure = function(message, html, data) { export const displayChatActionButtons = function(message, html, data) { const chatCard = html.find(".sw5e.chat-card"); if ( chatCard.length > 0 ) { - html.find(".flavor-text").remove(); + const flavor = html.find(".flavor-text"); + if ( flavor.text() === html.find(".item-name").text() ) flavor.remove(); // If the user is the message author or the actor owner, proceed let actor = game.actors.get(data.message.speaker.actor); diff --git a/module/combat.js b/module/combat.js index 8b50b13a..ed12959b 100644 --- a/module/combat.js +++ b/module/combat.js @@ -27,41 +27,14 @@ export const _getInitiativeFormula = function(combatant) { return parts.filter(p => p !== null).join(" + "); }; - -/* -------------------------------------------- */ - - /** - * TODO: A temporary shim until 0.7.x becomes stable - * @override + * When the Combat encounter updates - re-render open Actor sheets for combatants in the encounter. */ -TokenConfig.getTrackedAttributes = function(data, _path=[]) { - - // Track the path and record found attributes - const attributes = { - "bar": [], - "value": [] - }; - - // Recursively explore the object - for ( let [k, v] of Object.entries(data) ) { - let p = _path.concat([k]); - - // Check objects for both a "value" and a "max" - if ( v instanceof Object ) { - const isBar = ("value" in v) && ("max" in v); - if ( isBar ) attributes.bar.push(p); - else { - const inner = this.getTrackedAttributes(data[k], p); - attributes.bar.push(...inner.bar); - attributes.value.push(...inner.value); - } - } - - // Otherwise identify values which are numeric or null - else if ( Number.isNumeric(v) || (v === null) ) { - attributes.value.push(p); - } +Hooks.on("updateCombat", (combat, data, options, userId) => { + const updateTurn = ("turn" in data) || ("round" in data); + if ( !updateTurn ) return; + for ( let t of combat.turns ) { + const a = t.actor; + if ( t.actor ) t.actor.sheet.render(false); } - return attributes; -}; \ No newline at end of file +}); diff --git a/module/config.js b/module/config.js index 9e5e5a98..c145f7f7 100644 --- a/module/config.js +++ b/module/config.js @@ -325,17 +325,31 @@ SW5E.armorPropertiesTypes = { "Versatile": "SW5E.ArmorProperVersatile" }; -/* -------------------------------------------- */ +/** + * The valid units of measure for movement distances in the game system. + * By default this uses the imperial units of feet and miles. + * @type {Object} + */ +SW5E.movementUnits = { + "ft": "SW5E.DistFt", + "mi": "SW5E.DistMi" +} +/** + * The valid units of measure for the range of an action or effect. + * This object automatically includes the movement units from SW5E.movementUnits + * @type {Object} + */ SW5E.distanceUnits = { "none": "SW5E.None", "self": "SW5E.DistSelf", "touch": "SW5E.DistTouch", - "ft": "SW5E.DistFt", - "mi": "SW5E.DistMi", "spec": "SW5E.Special", "any": "SW5E.DistAny" }; +for ( let [k, v] of Object.entries(SW5E.movementUnits) ) { + SW5E.distanceUnits[k] = v; +} /* -------------------------------------------- */ @@ -413,7 +427,7 @@ SW5E.healingTypes = { * Enumerate the denominations of hit dice which can apply to classes in the SW5E system * @type {Array.} */ -SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12"]; +SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12", "d20"]; /* -------------------------------------------- */ @@ -461,15 +475,14 @@ SW5E.skills = { /* -------------------------------------------- */ SW5E.powerPreparationModes = { + "prepared": "SW5E.PowerPrepPrepared", "always": "SW5E.PowerPrepAlways", "atwill": "SW5E.PowerPrepAtWill", - "innate": "SW5E.PowerPrepInnate", - "prepared": "SW5E.PowerPrepPrepared" + "innate": "SW5E.PowerPrepInnate" }; SW5E.powerUpcastModes = ["always", "pact", "prepared"]; - SW5E.powerProgression = { "none": "SW5E.PowerNone", "full": "SW5E.PowerProgFull", @@ -901,15 +914,27 @@ SW5E.characterFlags = { type: Boolean }, "weaponCriticalThreshold": { - name: "SW5E.FlagsCritThreshold", - hint: "SW5E.FlagsCritThresholdHint", + name: "SW5E.FlagsWeaponCritThreshold", + hint: "SW5E.FlagsWeaponCritThresholdHint", section: "Feats", type: Number, placeholder: 20 + }, + "powerCriticalThreshold": { + name: "SW5E.FlagsPowerCritThreshold", + hint: "SW5E.FlagsPowerCritThresholdHint", + section: "Feats", + type: Number, + placeholder: 20 + }, + "meleeCriticalDamageDice": { + name: "SW5E.FlagsMeleeCriticalDice", + hint: "SW5E.FlagsMeleeCriticalDiceHint", + section: "Feats", + type: Number, + placeholder: 0 } }; // Configure allowed status flags -SW5E.allowedActorFlags = [ - "isPolymorphed", "originalActor" -].concat(Object.keys(SW5E.characterFlags)); +SW5E.allowedActorFlags = ["isPolymorphed", "originalActor"].concat(Object.keys(SW5E.characterFlags)); diff --git a/module/dice.js b/module/dice.js index dd647155..5eff6768 100644 --- a/module/dice.js +++ b/module/dice.js @@ -1,9 +1,9 @@ - /** - * A standardized helper function for managing core 5e "d20 rolls" - * - * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". - * This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively - * +/** + * A standardized helper function for managing core 5e "d20 rolls" + * + * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". + * This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively + * * @param {Array} parts The dice roll component parts, excluding the initial d20 * @param {Object} data Actor or item data against which to parse the roll * @param {Event|object} event The triggering event which initiated the roll @@ -27,72 +27,72 @@ * @param {object} messageData Additional data which is applied to the created Chat Message, if any * * @return {Promise} A Promise which resolves once the roll workflow has completed - */ - export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null, - flavor=null, fastForward=null, dialogOptions, - advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null, - elvenAccuracy=false, halflingLucky=false, reliableTalent=false, - chatMessage=true, messageData={}}={}) { - - // Prepare Message Data - messageData.flavor = flavor || title; - messageData.speaker = speaker || ChatMessage.getSpeaker(); - const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")}; - parts = parts.concat(["@bonus"]); - - // Handle fast-forward events - let adv = 0; - fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); - if (fastForward) { - if ( advantage || event.altKey ) adv = 1; - else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1; + */ +export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null, + flavor=null, fastForward=null, dialogOptions, + advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null, + elvenAccuracy=false, halflingLucky=false, reliableTalent=false, + chatMessage=true, messageData={}}={}) { + + // Prepare Message Data + messageData.flavor = flavor || title; + messageData.speaker = speaker || ChatMessage.getSpeaker(); + const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")}; + parts = parts.concat(["@bonus"]); + + // Handle fast-forward events + let adv = 0; + fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); + if (fastForward) { + if ( advantage || event.altKey ) adv = 1; + else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1; + } + + // Define the inner roll function + const _roll = (parts, adv, form) => { + + // Determine the d20 roll and modifiers + let nd = 1; + let mods = halflingLucky ? "r=1" : ""; + + // Handle advantage + if (adv === 1) { + nd = elvenAccuracy ? 3 : 2; + messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`; + if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true; + mods += "kh"; } - - // Define the inner roll function - const _roll = (parts, adv, form) => { - - // Determine the d20 roll and modifiers - let nd = 1; - let mods = halflingLucky ? "r=1" : ""; - // Handle advantage - if (adv === 1) { - nd = elvenAccuracy ? 3 : 2; - messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`; - if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true; - mods += "kh"; - } - - // Handle disadvantage - else if (adv === -1) { - nd = 2; - messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`; - if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true; - mods += "kl"; - } + // Handle disadvantage + else if (adv === -1) { + nd = 2; + messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`; + if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true; + mods += "kl"; + } // Prepend the d20 roll let formula = `${nd}d20${mods}`; if (reliableTalent) formula = `{${nd}d20${mods},10}kh`; parts.unshift(formula); - // Optionally include a situational bonus - if ( form ) { - data['bonus'] = form.bonus.value; - messageOptions.rollMode = form.rollMode.value; - } - if (!data["bonus"]) parts.pop(); - - // Optionally include an ability score selection (used for tool checks) - const ability = form ? form.ability : null; - if (ability && ability.value) { - data.ability = ability.value; - const abl = data.abilities[data.ability]; - if (abl) { - data.mod = abl.mod; - messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`; - } + // Optionally include a situational bonus + if ( form ) { + data['bonus'] = form.bonus.value; + messageOptions.rollMode = form.rollMode.value; + } + if (!data["bonus"]) parts.pop(); + + // Optionally include an ability score selection (used for tool checks) + const ability = form ? form.ability : null; + if (ability && ability.value) { + data.ability = ability.value; + const abl = data.abilities[data.ability]; + if (abl) { + data.mod = abl.mod; + messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`; } + } // Execute the roll let roll = new Roll(parts.join(" + "), data); @@ -139,73 +139,76 @@ */ async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) { - // Render modal dialog - template = template || "systems/sw5e/templates/chat/roll-dialog.html"; - let dialogData = { - formula: parts.join(" + "), - data: data, - rollMode: rollMode, - rollModes: CONFIG.Dice.rollModes, - config: CONFIG.SW5E - }; - const html = await renderTemplate(template, dialogData); + // Render modal dialog + template = template || "systems/sw5e/templates/chat/roll-dialog.html"; + let dialogData = { + formula: parts.join(" + "), + data: data, + rollMode: rollMode, + rollModes: CONFIG.Dice.rollModes, + config: CONFIG.SW5E + }; + const html = await renderTemplate(template, dialogData); - // Create the Dialog window - return new Promise(resolve => { - new Dialog({ - title: title, - content: html, - buttons: { - advantage: { - label: game.i18n.localize("SW5E.Advantage"), - callback: html => resolve(roll(parts, 1, html[0].querySelector("form"))) - }, - normal: { - label: game.i18n.localize("SW5E.Normal"), - callback: html => resolve(roll(parts, 0, html[0].querySelector("form"))) - }, - disadvantage: { - label: game.i18n.localize("SW5E.Disadvantage"), - callback: html => resolve(roll(parts, -1, html[0].querySelector("form"))) - } + // Create the Dialog window + return new Promise(resolve => { + new Dialog({ + title: title, + content: html, + buttons: { + advantage: { + label: game.i18n.localize("SW5E.Advantage"), + callback: html => resolve(roll(parts, 1, html[0].querySelector("form"))) }, - default: "normal", - close: () => resolve(null) - }, dialogOptions).render(true); - }); - } + normal: { + label: game.i18n.localize("SW5E.Normal"), + callback: html => resolve(roll(parts, 0, html[0].querySelector("form"))) + }, + disadvantage: { + label: game.i18n.localize("SW5E.Disadvantage"), + callback: html => resolve(roll(parts, -1, html[0].querySelector("form"))) + } + }, + default: "normal", + close: () => resolve(null) + }, dialogOptions).render(true); + }); +} - /* -------------------------------------------- */ +/* -------------------------------------------- */ + +/** + * A standardized helper function for managing core 5e "d20 rolls" + * + * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". + * This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively + * + * @param {Array} parts The dice roll component parts, excluding the initial d20 + * @param {Actor} actor The Actor making the damage roll + * @param {Object} data Actor or item data against which to parse the roll + * @param {Event|object}[event The triggering event which initiated the roll + * @param {string} rollMode A specific roll mode to apply as the default for the resulting roll + * @param {String} template The HTML template used to render the roll dialog + * @param {String} title The dice roll UI window title + * @param {Object} speaker The ChatMessage speaker to pass when creating the chat + * @param {string} flavor Flavor text to use in the posted chat message + * @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled + * @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls + * @param {number} criticalBonusDice A number of bonus damage dice that are added for critical hits + * @param {number} criticalMultiplier A critical hit multiplier which is applied to critical hits + * @param {Boolean} fastForward Allow fast-forward advantage selection + * @param {Function} onClose Callback for actions to take when the dialog form is closed + * @param {Object} dialogOptions Modal dialog options + * @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll + * @param {object} messageData Additional data which is applied to the created Chat Message, if any + * + * @return {Promise} A Promise which resolves once the roll workflow has completed + */ +export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor, + allowCritical=true, critical=false, criticalBonusDice=0, criticalMultiplier=2, fastForward=null, + dialogOptions={}, chatMessage=true, messageData={}}={}) { - /** - * A standardized helper function for managing core 5e "d20 rolls" - * - * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". - * This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively - * - * @param {Array} parts The dice roll component parts, excluding the initial d20 - * @param {Actor} actor The Actor making the damage roll - * @param {Object} data Actor or item data against which to parse the roll - * @param {Event|object}[event The triggering event which initiated the roll - * @param {string} rollMode A specific roll mode to apply as the default for the resulting roll - * @param {String} template The HTML template used to render the roll dialog - * @param {String} title The dice roll UI window title - * @param {Object} speaker The ChatMessage speaker to pass when creating the chat - * @param {string} flavor Flavor text to use in the posted chat message - * @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled - * @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls - * @param {Boolean} fastForward Allow fast-forward advantage selection - * @param {Function} onClose Callback for actions to take when the dialog form is closed - * @param {Object} dialogOptions Modal dialog options - * @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll - * @param {object} messageData Additional data which is applied to the created Chat Message, if any - * - * @return {Promise} A Promise which resolves once the roll workflow has completed - */ - export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor, - allowCritical=true, critical=false, fastForward=null, dialogOptions, chatMessage=true, messageData={}}={}) { - // Prepare Message Data messageData.flavor = flavor || title; messageData.speaker = speaker || ChatMessage.getSpeaker(); @@ -213,8 +216,8 @@ async function _d20RollDialog({template, title, parts, data, rollMode, dialogOpt parts = parts.concat(["@bonus"]); fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); - // Define inner roll function - const _roll = function(parts, crit, form) { + // Define inner roll function + const _roll = function(parts, crit, form) { // Optionally include a situational bonus if ( form ) { @@ -224,17 +227,17 @@ async function _d20RollDialog({template, title, parts, data, rollMode, dialogOpt if (!data["bonus"]) parts.pop(); // Create the damage roll - let roll = new Roll(parts.join("+"), data); + let roll = new Roll(parts.join("+"), data); // Modify the damage formula for critical hits if ( crit === true ) { - let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0; - let mult = 2; - // TODO Backwards compatibility - REMOVE LATER - if (isNewerVersion(game.data.version, "0.6.9")) roll.alter(mult, add); - else roll.alter(add, mult); - messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`; - if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true; + roll.alter(criticalMultiplier, 0); // Multiply all dice + if ( roll.terms[0] instanceof Die ) { // Add bonus dice for only the main dice term + roll.terms[0].alter(1, criticalBonusDice); + roll._formula = roll.formula; + } + messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`; + if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true; } // Execute the roll diff --git a/module/effects.js b/module/effects.js new file mode 100644 index 00000000..609aa061 --- /dev/null +++ b/module/effects.js @@ -0,0 +1,63 @@ +/** + * Manage Active Effect instances through the Actor Sheet via effect control buttons. + * @param {MouseEvent} event The left-click event on the effect control + * @param {Actor|Item} owner The owning entity which manages this effect + */ +export function onManageActiveEffect(event, owner) { + event.preventDefault(); + const a = event.currentTarget; + const li = a.closest("li"); + const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; + switch ( a.dataset.action ) { + case "create": + return ActiveEffect.create({ + label: "New Effect", + icon: "icons/svg/aura.svg", + origin: owner.uuid, + "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, + disabled: li.dataset.effectType === "inactive" + }, owner).create(); + case "edit": + return effect.sheet.render(true); + case "delete": + return effect.delete(); + case "toggle": + return effect.update({disabled: !effect.data.disabled}); + } +} + +/** + * Prepare the data structure for Active Effects which are currently applied to an Actor or Item. + * @param {ActiveEffect[]} effects The array of Active Effect instances to prepare sheet data for + * @return {object} Data for rendering + */ +export function prepareActiveEffectCategories(effects) { + + // Define effect header categories + const categories = { + temporary: { + type: "temporary", + label: "Temporary Effects", + effects: [] + }, + passive: { + type: "passive", + label: "Passive Effects", + effects: [] + }, + inactive: { + type: "inactive", + label: "Inactive Effects", + effects: [] + } + }; + + // Iterate over active effects, classifying them into categories + for ( let e of effects ) { + e._getSourceName(); // Trigger a lookup for the source name + if ( e.data.disabled ) categories.inactive.effects.push(e); + else if ( e.isTemporary ) categories.temporary.effects.push(e); + else categories.passive.effects.push(e); + } + return categories; +} \ No newline at end of file diff --git a/module/item/entity.js b/module/item/entity.js index 5e360b0d..504e3bc9 100644 --- a/module/item/entity.js +++ b/module/item/entity.js @@ -161,6 +161,7 @@ export default class Item5e extends Item { // Power Level, School, and Components if ( itemData.type === "power" ) { + data.preparation.mode = data.preparation.mode || "prepared"; labels.level = C.powerLevels[data.level]; labels.school = C.powerSchools[data.school]; labels.components = Object.entries(data.components).reduce((arr, c) => { @@ -180,33 +181,33 @@ export default class Item5e extends Item { else labels.featType = game.i18n.localize("SW5E.Passive"); } - // Species Items - else if ( itemData.type === "species" ) { - // labels.species = C.species[data.species]; - } - // Archetype Items - else if ( itemData.type === "archetype" ) { + // Species Items + else if ( itemData.type === "species" ) { + // labels.species = C.species[data.species]; + } + // Archetype Items + else if ( itemData.type === "archetype" ) { // labels.archetype = C.archetype[data.archetype]; } - // Background Items - else if ( itemData.type === "background" ) { - - } - // Class Feature Items + // Background Items + else if ( itemData.type === "background" ) { + // labels.background = C.background[data.background]; + } + // Class Feature Items else if ( itemData.type === "classfeature" ) { - + // labels.classFeature = C.classFeature[data.classFeature]; } // Fighting Style Items else if ( itemData.type === "fightingstyle" ) { - + // labels.fightingstyle = C.fightingstyle[data.fightingstyle]; } // Fighting Mastery Items else if ( itemData.type === "fightingmastery" ) { - + // labels.fightingmastery = C.fightingmastery[data.fightingmastery]; } // Lightsaber Form Items else if ( itemData.type === "lightsaberform" ) { - + // labels.lightsaberform = C.lightsaberform[data.lightsaberform]; } // Equipment Items @@ -322,7 +323,7 @@ export default class Item5e extends Item { user: game.user._id, type: CONST.CHAT_MESSAGE_TYPES.OTHER, content: html, - flavor: this.name, + flavor: this.data.data.chatFlavor || this.name, speaker: { actor: this.actor._id, token: this.actor.token, @@ -348,7 +349,7 @@ export default class Item5e extends Item { /* -------------------------------------------- */ - /** + /** * For items which consume a resource, handle the consumption of that resource when the item is used. * There are four types of ability consumptions which are handled: * 1. Ammunition (on attack rolls) @@ -367,7 +368,6 @@ export default class Item5e extends Item { if ( !consume.type ) return true; const actor = this.actor; const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type]; - const amount = parseInt(consume.amount || 1); // Only handle certain types for certain actions if ( ((consume.type === "ammo") && !isAttack ) || ((consume.type !== "ammo") && !isCard) ) return true; @@ -380,6 +380,7 @@ export default class Item5e extends Item { // Identify the consumed resource and it's quantity let consumed = null; + let amount = parseInt(consume.amount || 1); let quantity = 0; switch ( consume.type ) { case "attribute": @@ -393,7 +394,13 @@ export default class Item5e extends Item { break; case "charges": consumed = actor.items.get(consume.target); - quantity = consumed ? consumed.data.data.uses.value : 0; + if ( !consumed ) break; + const uses = consumed.data.data.uses; + if ( uses.per && uses.max ) quantity = uses.value; + else if ( consumed.data.data.recharge?.value ) { + quantity = consumed.data.data.recharge.charged ? 1 : 0; + amount = 1; + } break; } @@ -418,7 +425,11 @@ export default class Item5e extends Item { await consumed.update({"data.quantity": remaining}); break; case "charges": - await consumed.update({"data.uses.value": remaining}); + const uses = consumed.data.data.uses || {}; + const recharge = consumed.data.data.recharge || {}; + if ( uses.per && uses.max ) await consumed.update({"data.uses.value": remaining}); + else if ( recharge.value ) await consumed.update({"data.recharge.charged": false}); + break; } return true; } @@ -436,7 +447,7 @@ export default class Item5e extends Item { // Configure whether to consume a limited use or to place a template const charge = this.data.data.recharge; const uses = this.data.data.uses; - let usesCharges = !!uses.per && (uses.max > 0); + let usesCharges = !!uses.per && !!uses.max; let placeTemplate = false; let consume = charge.value || usesCharges; @@ -637,40 +648,37 @@ export default class Item5e extends Item { } // Attack Bonus + if ( itemData.attackBonus ) parts.push(itemData.attackBonus); const actorBonus = actorData?.bonuses?.[itemData.actionType] || {}; - if ( itemData.attackBonus || actorBonus.attack ) { - parts.push("@atk"); - rollData["atk"] = [itemData.attackBonus, actorBonus.attack].filterJoin(" + "); - } + if ( actorBonus.attack ) parts.push(actorBonus.attack); - // Ammunition Bonus - delete this._ammo; - const consume = itemData.consume; - if ( consume?.type === "ammo" ) { - const ammo = this.actor.items.get(consume.target); - if(ammo?.data){ - const q = ammo.data.data.quantity; - const consumeAmount = consume.amount ?? 0; - if ( q && (q - consumeAmount >= 0) ) { - let ammoBonus = ammo.data.data.attackBonus; - if ( ammoBonus ) { - parts.push("@ammo"); - rollData["ammo"] = ammoBonus; - title += ` [${ammo.name}]`; - this._ammo = ammo; - } + // Ammunition Bonus + delete this._ammo; + const consume = itemData.consume; + if ( consume?.type === "ammo" ) { + const ammo = this.actor.items.get(consume.target); + if(ammo?.data){ + const q = ammo.data.data.quantity; + const consumeAmount = consume.amount ?? 0; + if ( q && (q - consumeAmount >= 0) ) { + this._ammo = ammo; + let ammoBonus = ammo.data.data.attackBonus; + if ( ammoBonus ) { + parts.push("@ammo"); + rollData["ammo"] = ammoBonus; + title += ` [${ammo.name}]`; } - //}else{ - // ui.notifications.error(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel})); } } - + } + // Compose roll options const rollConfig = mergeObject({ parts: parts, actor: this.actor, data: rollData, title: title, + flavor: title, speaker: ChatMessage.getSpeaker({actor: this.actor}), dialogOptions: { width: 400, @@ -681,9 +689,11 @@ export default class Item5e extends Item { }, options); rollConfig.event = options.event; - // Expanded weapon critical threshold + // Expanded critical hit thresholds if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) { rollConfig.critical = parseInt(flags.weaponCriticalThreshold); + } else if (( this.data.type === "power" ) && flags.powerCriticalThreshold) { + rollConfig.critical = parseInt(flags.powerCriticalThreshold); } // Elven Accuracy @@ -710,28 +720,41 @@ export default class Item5e extends Item { /** * Place a damage roll using an item (weapon, feat, power, or equipment) - * Rely upon the damageRoll logic for the core implementation - * - * @return {Promise} A Promise which resolves to the created Roll instance + * Rely upon the damageRoll logic for the core implementation. + * @param {MouseEvent} [event] An event which triggered this roll, if any + * @param {number} [powerLevel] If the item is a power, override the level for damage scaling + * @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula + * @param {object} [options] Additional options passed to the damageRoll function + * @return {Promise} A Promise which resolves to the created Roll instance */ - rollDamage({event, powerLevel=null, versatile=false}={}) { + rollDamage({event, powerLevel=null, versatile=false, options={}}={}) { + if ( !this.hasDamage ) throw new Error("You may not make a Damage Roll with this Item."); const itemData = this.data.data; const actorData = this.actor.data.data; - if ( !this.hasDamage ) { - throw new Error("You may not make a Damage Roll with this Item."); - } const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id }}; // Get roll data + const parts = itemData.damage.parts.map(d => d[0]); const rollData = this.getRollData(); if ( powerLevel ) rollData.item.level = powerLevel; - // Get message labels + // Configure the damage roll const title = `${this.name} - ${game.i18n.localize("SW5E.DamageRoll")}`; - let flavor = this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title; - - // Define Roll parts - const parts = itemData.damage.parts.map(d => d[0]); + const rollConfig = { + event: event, + parts: parts, + actor: this.actor, + data: rollData, + title: title, + flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title, + speaker: ChatMessage.getSpeaker({actor: this.actor}), + dialogOptions: { + width: 400, + top: event ? event.clientY - 80 : null, + left: window.innerWidth - 710 + }, + messageData: messageData + }; // Adjust damage from versatile usage if ( versatile && itemData.damage.versatile ) { @@ -751,37 +774,27 @@ export default class Item5e extends Item { } } - // Define Roll Data + // Add damage bonus formula const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {}; - if ( actorBonus.damage && parseInt(actorBonus.damage) !== 0 ) { - parts.push("@dmg"); - rollData["dmg"] = actorBonus.damage; + if ( actorBonus.damage && (parseInt(actorBonus.damage) !== 0) ) { + parts.push(actorBonus.damage); } - // Ammunition Damage + // Add ammunition damage if ( this._ammo ) { parts.push("@ammo"); rollData["ammo"] = this._ammo.data.data.damage.parts.map(p => p[0]).join("+"); - flavor += ` [${this._ammo.name}]`; + rollConfig.flavor += ` [${this._ammo.name}]`; delete this._ammo; } + // Scale melee critical hit damage + if ( itemData.actionType === "mwak" ) { + rollConfig.criticalBonusDice = this.actor.getFlag("sw5e", "meleeCriticalDamageDice") ?? 0; + } + // Call the roll helper utility - return damageRoll({ - event: event, - parts: parts, - actor: this.actor, - data: rollData, - title: title, - flavor: flavor, - speaker: ChatMessage.getSpeaker({actor: this.actor}), - dialogOptions: { - width: 400, - top: event ? event.clientY - 80 : null, - left: window.innerWidth - 710 - }, - messageData - }); + return damageRoll(mergeObject(rollConfig, options)); } /* -------------------------------------------- */ @@ -793,22 +806,7 @@ export default class Item5e extends Item { _scaleAtWillDamage(parts, scale, level, rollData) { const add = Math.floor((level + 1) / 6); if ( add === 0 ) return; - - // FUTURE SOLUTION - 0.7.0 AND LATER - if (isNewerVersion(game.data.version, "0.6.9")) { - this._scaleDamage(parts, scale || parts.join(" + "), add, rollData) - - } - - // LEGACY SOLUTION - 0.6.x AND OLDER - // TODO: Deprecate the legacy solution one FVTT 0.7.x is RELEASE - else { - if ( scale && (scale !== parts[0]) ) { - parts[0] = parts[0] + " + " + scale.replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${add}d${d}`); - } else { - parts[0] = parts[0].replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${parseInt(nd)+add}d${d}`); - } - } + this._scaleDamage(parts, scale || parts.join(" + "), add, rollData); } /* -------------------------------------------- */ @@ -826,20 +824,7 @@ export default class Item5e extends Item { _scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) { const upcastLevels = Math.max(powerLevel - baseLevel, 0); if ( upcastLevels === 0 ) return parts; - - // FUTURE SOLUTION - 0.7.0 AND LATER - if (isNewerVersion(game.data.version, "0.6.9")) { - this._scaleDamage(parts, formula, upcastLevels, rollData); - } - - // LEGACY SOLUTION - 0.6.x AND OLDER - // TODO: Deprecate the legacy solution one FVTT 0.7.x is RELEASE - else { - const bonus = new Roll(formula); - bonus.alter(0, upcastLevels); - parts.push(bonus.formula); - } - return parts; + this._scaleDamage(parts, formula, upcastLevels, rollData); } /* -------------------------------------------- */ @@ -882,7 +867,7 @@ export default class Item5e extends Item { /** * Place an attack roll using an item (weapon, feat, power, or equipment) * Rely upon the d20Roll logic for the core implementation - * + * * @return {Promise} A Promise which resolves to the created Roll instance */ async rollFormula(options={}) { @@ -899,7 +884,7 @@ export default class Item5e extends Item { const roll = new Roll(rollData.item.formula, rollData).roll(); roll.toMessage({ speaker: ChatMessage.getSpeaker({actor: this.actor}), - flavor: this.data.data.chatFlavor || title, + flavor: title, rollMode: game.settings.get("core", "rollMode"), messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id }} }); @@ -910,7 +895,7 @@ export default class Item5e extends Item { /** * Use a consumable item, deducting from the quantity or charges of the item. - * @param {boolean} configureDialog Whether to show a configuration dialog + * @param {boolean} configureDialog Whether to show a configuration dialog * @return {boolean} Whether further execution should be prevented * @private */ @@ -972,7 +957,7 @@ export default class Item5e extends Item { if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize(); } return true; - } + } /* -------------------------------------------- */ @@ -1021,7 +1006,7 @@ export default class Item5e extends Item { template: "systems/sw5e/templates/chat/tool-roll-dialog.html", title: title, speaker: ChatMessage.getSpeaker({actor: this.actor}), - flavor: `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`, + flavor: title, dialogOptions: { width: 400, top: options.event ? options.event.clientY - 80 : null, @@ -1055,8 +1040,8 @@ export default class Item5e extends Item { } // Include a proficiency score - const prof = "proficient" in rollData.item ? (rollData.item.proficient || 0) : 1; - rollData["prof"] = Math.floor(prof * rollData.attributes.prof); + const prof = ("proficient" in rollData.item) ? (rollData.item.proficient || 0) : 1; + rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0)); return rollData; } @@ -1189,7 +1174,7 @@ export default class Item5e extends Item { if ( !targets.length ) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken")); return targets; } - + /* -------------------------------------------- */ /* Factory Methods */ /* -------------------------------------------- */ diff --git a/module/item/sheet.js b/module/item/sheet.js index 9442a0b7..fc13c5e0 100644 --- a/module/item/sheet.js +++ b/module/item/sheet.js @@ -1,4 +1,5 @@ import TraitSelector from "../apps/trait-selector.js"; +import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js"; /** * Override and extend the core ItemSheet implementation to handle specific item types @@ -7,8 +8,11 @@ import TraitSelector from "../apps/trait-selector.js"; export default class ItemSheet5e extends ItemSheet { constructor(...args) { super(...args); + + // Expand the default size of the class sheet if ( this.object.data.type === "class" ) { - this.options.width = 600; + this.options.width = this.position.width = 600; + this.options.height = this.position.height = 680; } } @@ -18,7 +22,7 @@ export default class ItemSheet5e extends ItemSheet { static get defaultOptions() { return mergeObject(super.defaultOptions, { width: 560, - height: "auto", + height: 400, classes: ["sw5e", "sheet", "item"], resizable: true, scrollY: [".tab.details"], @@ -37,17 +41,17 @@ export default class ItemSheet5e extends ItemSheet { /* -------------------------------------------- */ /** @override */ - getData() { - const data = super.getData(); + async getData(options) { + const data = super.getData(options); data.labels = this.item.labels; data.config = CONFIG.SW5E; // Item Type, Status, and Details - data.itemType = data.item.type.titleCase(); + data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`); data.itemStatus = this._getItemStatus(data.item); data.itemProperties = this._getItemProperties(data.item); data.isPhysical = data.item.data.hasOwnProperty("quantity"); - + // Potential consumption targets data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item); @@ -55,17 +59,20 @@ export default class ItemSheet5e extends ItemSheet { data.hasAttackRoll = this.item.hasAttack; data.isHealing = data.item.data.actionType === "heal"; data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat"; - data.isLine = ["line", "wall"].includes(data.item.data.target?.type); + data.isLine = ["line", "wall"].includes(data.item.data.target?.type); // Vehicles data.isCrewed = data.item.data.activation?.type === 'crew'; data.isMountable = this._isItemMountable(data.item); + + // Prepare Active Effects + data.effects = prepareActiveEffectCategories(this.entity.effects); 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 @@ -109,6 +116,8 @@ export default class ItemSheet5e extends ItemSheet { // Charges else if ( consume.type === "charges" ) { return actor.items.reduce((obj, i) => { + + // Limited-use items const uses = i.data.data.uses || {}; if ( uses.per && uses.max ) { const label = uses.per === "charges" ? @@ -116,6 +125,10 @@ export default class ItemSheet5e extends ItemSheet { ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {max: uses.max, 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")})`; return obj; }, {}) } @@ -177,31 +190,26 @@ export default class ItemSheet5e extends ItemSheet { } 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 === "fightingmastery" ) { + //props.push(labels.fightingmastery); + } + else if ( item.type === "fightingstyle" ) { + //props.push(labels.fightingstyle); + } + else if ( item.type === "lightsaberform" ) { + //props.push(labels.lightsaberform); } - - else if ( item.type === "background" ) { - - } - - else if ( item.type === "classfeature" ) { - - } - - else if ( item.type === "fightingmastery" ) { - - } - - else if ( item.type === "fightingstyle" ) { - - } - - else if ( item.type === "lightsaberform" ) { - - } // Action type if ( item.data.actionType ) { @@ -240,8 +248,8 @@ export default class ItemSheet5e extends ItemSheet { /** @override */ setPosition(position={}) { - if ( !this._minimized ) { - position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; + if ( !(this._minimized || position.height) ) { + position.height = (this._tabs[0].active === "details") ? "auto" : this.options.height; } return super.setPosition(position); } @@ -251,17 +259,20 @@ export default class ItemSheet5e extends ItemSheet { /* -------------------------------------------- */ /** @override */ - _updateObject(event, formData) { + _getSubmitData(updateData={}) { - // TODO: This can be removed once 0.7.x is release channel - if ( !formData.data ) formData = expandObject(formData); + // 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 = formData.data?.damage; + // Handle Damage array + const damage = data.data?.damage; if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]); - // Update the Item - super._updateObject(event, formData); + // Return the flattened submission data + return flattenObject(data); } /* -------------------------------------------- */ @@ -269,8 +280,14 @@ export default class ItemSheet5e extends ItemSheet { /** @override */ activateListeners(html) { super.activateListeners(html); - html.find(".damage-control").click(this._onDamageControl.bind(this)); - html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this)); + if ( this.isEditable ) { + html.find(".damage-control").click(this._onDamageControl.bind(this)); + html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.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) + }); + } } /* -------------------------------------------- */ @@ -328,4 +345,12 @@ export default class ItemSheet5e extends ItemSheet { maximum: skills.number }).render(true) } + + /* -------------------------------------------- */ + + /** @override */ + async _onSubmit(...args) { + if ( this._tabs[0].active === "details" ) this.position.height = "auto"; + await super._onSubmit(...args); + } } diff --git a/module/migration.js b/module/migration.js index 6009ac4b..32b2fa74 100644 --- a/module/migration.js +++ b/module/migration.js @@ -14,6 +14,7 @@ export const migrateWorld = async function() { await a.update(updateData, {enforceTypes: false}); } } catch(err) { + err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`; console.error(err); } } @@ -27,6 +28,7 @@ export const migrateWorld = async function() { await i.update(updateData, {enforceTypes: false}); } } catch(err) { + err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`; console.error(err); } } @@ -40,15 +42,15 @@ export const migrateWorld = async function() { await s.update(updateData, {enforceTypes: false}); } } catch(err) { + err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`; console.error(err); } } // Migrate World Compendium Packs - const packs = game.packs.filter(p => { - return (p.metadata.package === "world") && ["Actor", "Item", "Scene"].includes(p.metadata.entity) - }); - for ( let p of packs ) { + for ( let p of game.packs ) { + if ( p.metadata.package !== "world" ) continue; + if ( !["Actor", "Item", "Scene"].includes(p.metadata.entity) ) continue; await migrateCompendium(p); } @@ -68,27 +70,46 @@ export const migrateCompendium = async function(pack) { const entity = pack.metadata.entity; if ( !["Actor", "Item", "Scene"].includes(entity) ) return; + // Unlock the pack for editing + const wasLocked = pack.locked; + await pack.configure({locked: false}); + // Begin by requesting server-side data model migration and get the migrated content await pack.migrate(); const content = await pack.getContent(); // Iterate over compendium entries - applying fine-tuned migration functions for ( let ent of content ) { + let updateData = {}; try { - let updateData = null; - if (entity === "Item") updateData = migrateItemData(ent.data); - else if (entity === "Actor") updateData = migrateActorData(ent.data); - else if ( entity === "Scene" ) updateData = migrateSceneData(ent.data); - if (!isObjectEmpty(updateData)) { - expandObject(updateData); - updateData["_id"] = ent._id; - await pack.updateEntity(updateData); - console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`); + switch (entity) { + case "Actor": + updateData = migrateActorData(ent.data); + break; + case "Item": + updateData = migrateItemData(ent.data); + break; + case "Scene": + updateData = migrateSceneData(ent.data); + break; } - } catch(err) { + if ( isObjectEmpty(updateData) ) continue; + + // Save the entry, if data was changed + updateData["_id"] = ent._id; + await pack.updateEntity(updateData); + console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`); + } + + // Handle migration failures + catch(err) { + err.message = `Failed sw5e system migration for entity ${ent.name} in pack ${pack.collection}: ${err.message}`; console.error(err); } } + + // Apply the original locked status for the pack + pack.configure({locked: wasLocked}); console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`); }; @@ -107,9 +128,7 @@ export const migrateActorData = function(actor) { // Actor Data Updates _migrateActorBonuses(actor, updateData); - - // Remove deprecated fields - _migrateRemoveDeprecated(actor, updateData); + _migrateActorMovement(actor, updateData); // Migrate Owned Items if ( !actor.items ) return updateData; @@ -172,11 +191,6 @@ function cleanActorData(actorData) { */ export const migrateItemData = function(item) { const updateData = {}; - - // Remove deprecated fields - _migrateRemoveDeprecated(item, updateData); - - // Return the migrated update data return updateData; }; @@ -225,31 +239,17 @@ function _migrateActorBonuses(actor, updateData) { } } - /* -------------------------------------------- */ - /** - * A general migration to remove all fields from the data model which are flagged with a _deprecated tag + * Migrate the actor bonuses object * @private */ -const _migrateRemoveDeprecated = function(ent, updateData) { - const flat = flattenObject(ent.data); - - // Identify objects to deprecate - const toDeprecate = Object.entries(flat).filter(e => e[0].endsWith("_deprecated") && (e[1] === true)).map(e => { - let parent = e[0].split("."); - parent.pop(); - return parent.join("."); - }); - - // Remove them - for ( let k of toDeprecate ) { - let parts = k.split("."); - parts[parts.length-1] = "-=" + parts[parts.length-1]; - updateData[`data.${parts.join(".")}`] = null; - } -}; +function _migrateActorMovement(actor, updateData) { + if ( actor.data.attributes?.movement?.walk !== undefined ) return; + const s = (actor.data.attributes?.speed?.value || "").split(" "); + if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null; +} /* -------------------------------------------- */ @@ -280,3 +280,24 @@ export async function purgeFlags(pack) { } await pack.configure({locked: true}); } + +/* -------------------------------------------- */ + + +/** + * Purge the data model of any inner objects which have been flagged as _deprecated. + * @param {object} data The data to clean + * @private + */ +export function removeDeprecatedObjects(data) { + for ( let [k, v] of Object.entries(data) ) { + if ( getType(v) === "Object" ) { + if (v._deprecated === true) { + console.log(`Deleting deprecated object key ${k}`); + delete data[k]; + } + else removeDeprecatedObjects(v); + } + } + return data; +} diff --git a/module/pixi/ability-template.js b/module/pixi/ability-template.js index e07795e6..a7bf6e53 100644 --- a/module/pixi/ability-template.js +++ b/module/pixi/ability-template.js @@ -37,7 +37,7 @@ export default class AbilityTemplate extends MeasuredTemplate { templateData.width = target.value; templateData.direction = 45; break; - case "ray": // 5e rays are most commonly 1 square (5 ft) in width + case "ray": // 5e rays are most commonly 1 square (5 ft) in width templateData.width = target.width ?? canvas.dimensions.distance; break; default: diff --git a/module/settings.js b/module/settings.js index f7c6c8a4..8898d8a6 100644 --- a/module/settings.js +++ b/module/settings.js @@ -7,11 +7,11 @@ export const registerSystemSettings = function() { name: "System Migration Version", scope: "world", config: false, - type: Number, - default: 0 + type: String, + default: "" }); - /** + /** * Register resting variants */ game.settings.register("sw5e", "restVariant", { @@ -82,18 +82,6 @@ export const registerSystemSettings = function() { type: Boolean, }); - /** - * Option to automatically create Power Measured Template on roll - */ - game.settings.register("sw5e", "alwaysPlacePowerTemplate", { - name: "SETTINGS.5eAutoPowerTemplateN", - hint: "SETTINGS.5eAutoPowerTemplateL", - scope: "client", - config: true, - default: false, - type: Boolean - }); - /** * Option to automatically collapse Item Card descriptions */ diff --git a/module/templates.js b/module/templates.js index b477dfa9..450eaaae 100644 --- a/module/templates.js +++ b/module/templates.js @@ -4,17 +4,16 @@ * @return {Promise} */ export const preloadHandlebarsTemplates = async function() { + return loadTemplates([ - // Define template paths to load - const templatePaths = [ + // Shared Partials + "systems/sw5e/templates/actors/parts/active-effects.html", // Actor Sheet Partials "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-effects.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-biography.html", "systems/sw5e/templates/actors/newActor/parts/swalt-core.html", @@ -30,8 +29,5 @@ export const preloadHandlebarsTemplates = async function() { "systems/sw5e/templates/items/parts/item-activation.html", "systems/sw5e/templates/items/parts/item-description.html", "systems/sw5e/templates/items/parts/item-mountable.html" - ]; - - // Load the template parts - return loadTemplates(templatePaths); + ]); }; diff --git a/sw5e.css b/sw5e.css index d7e176c6..426fe368 100644 --- a/sw5e.css +++ b/sw5e.css @@ -231,6 +231,12 @@ /* ----------------------------------------- */ /* Trait Lists */ /* ----------------------------------------- */ + /* ----------------------------------------- */ + /* Items Lists */ + /* ----------------------------------------- */ + /* ----------------------------------------- */ + /* Active Effects */ + /* ----------------------------------------- */ } .sw5e.sheet .window-content { overflow-y: hidden; @@ -404,6 +410,88 @@ margin: 0; padding: 0; } +.sw5e.sheet .items-list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + scrollbar-width: thin; + color: #7a7971; +} +.sw5e.sheet .items-list .item-list { + list-style: none; + margin: 0; + padding: 0; +} +.sw5e.sheet .items-list .item { + align-items: center; + padding: 0 2px; + border-bottom: 1px solid #c9c7b8; +} +.sw5e.sheet .items-list .item:last-child { + border-bottom: none; +} +.sw5e.sheet .items-list .item .item-name { + color: #191813; +} +.sw5e.sheet .items-list .item .item-name .item-image { + flex: 0 0 30px; + height: 30px; + background-size: 30px; + border: none; + margin-right: 5px; +} +.sw5e.sheet .items-list .item .item-name h4 { + margin: 0; + white-space: nowrap; + overflow-x: hidden; +} +.sw5e.sheet .items-list .items-header { + height: 28px; + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: 2px groove #eeede0; + font-weight: bold; +} +.sw5e.sheet .items-list .items-header > * { + font-size: 12px; + text-align: center; +} +.sw5e.sheet .items-list .items-header .item-name { + padding-left: 5px; + font-family: 'Russo One'; + font-size: 14px; + font-weight: 400; +} +.sw5e.sheet .items-list .item-name { + flex: 2; + margin: 0; + overflow: hidden; + font-size: 13px; + text-align: left; + align-items: center; +} +.sw5e.sheet .items-list .item-controls { + flex: 0 0 60px; + justify-content: space-between; +} +.sw5e.sheet .items-list .item-controls a { + font-size: 12px; + text-align: center; +} +.sw5e.sheet .effects .item .effect-source, +.sw5e.sheet .effects .item .effect-duration, +.sw5e.sheet .effects .item .effect-controls { + text-align: center; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; + font-size: 12px; +} +.sw5e.sheet .effects .item .effect-controls { + border: none; +} /* ----------------------------------------- */ /* Trait Selector /* ----------------------------------------- */ @@ -447,9 +535,6 @@ /* Powerbook */ /* ----------------------------------------- */ /* ----------------------------------------- */ - /* Active Effects */ - /* ----------------------------------------- */ - /* ----------------------------------------- */ /* TinyMCE */ /* ----------------------------------------- */ } @@ -515,6 +600,7 @@ font-weight: 400; color: #4b4a44; border-bottom: 1px solid #c9c7b8; + white-space: nowrap; } .sw5e.sheet.actor .tab.attributes { overflow: hidden; @@ -559,6 +645,7 @@ font-family: "Signika", sans-serif; font-size: 12px; font-weight: 400; + white-space: nowrap; } .sw5e.sheet.actor .ability-scores { flex: 0 0 100px; @@ -771,35 +858,10 @@ border-bottom: 2px groove #eeede0; } .sw5e.sheet.actor .inventory-list { - list-style: none; - margin: 0; padding: 0 5px; - overflow-y: auto; - scrollbar-width: thin; - color: #7a7971; -} -.sw5e.sheet.actor .inventory-list .item { - line-height: 30px; - padding: 0 2px; - border-bottom: 1px solid #c9c7b8; -} -.sw5e.sheet.actor .inventory-list .item:last-child { - border-bottom: none; } .sw5e.sheet.actor .inventory-list .item .item-name { cursor: pointer; - max-height: 30px; - overflow: hidden; -} -.sw5e.sheet.actor .inventory-list .item .item-name .item-image { - flex: 0 0 30px; - background-size: 30px; - margin-right: 5px; -} -.sw5e.sheet.actor .inventory-list .item .item-name h4 { - margin: 0; - white-space: nowrap; - overflow-x: hidden; } .sw5e.sheet.actor .inventory-list .item .item-name.rollable:hover .item-image { background-image: url("../../icons/svg/d20-grey.svg") !important; @@ -821,36 +883,14 @@ flex: 0 0 80px; text-align: right; font-size: 11px; - color: #7a7971; white-space: nowrap; } -.sw5e.sheet.actor .inventory-list .inventory-header { - margin: 2px 0; - padding: 0; - align-items: center; - background: rgba(0, 0, 0, 0.05); - border: 2px groove #eeede0; - font-weight: bold; - line-height: 24px; -} -.sw5e.sheet.actor .inventory-list .inventory-header h3 { - margin: 0 -5px 0 0; - padding-left: 5px; - font-family: 'Russo One'; - font-size: 20px; - font-weight: 400; - font-size: 16px; -} .sw5e.sheet.actor .inventory-list .inventory-header .item-controls a.item-create { flex: 0 0 100%; } -.sw5e.sheet.actor .inventory-list .item-name { - color: #191813; -} .sw5e.sheet.actor .inventory-list .item-detail { flex: 0 0 70px; font-size: 12px; - color: #7a7971; text-align: center; border-right: 1px solid #c9c7b8; word-break: break-word; @@ -868,45 +908,15 @@ border-left: 1px solid #c9c7b8; border-right: 1px solid #c9c7b8; } -.sw5e.sheet.actor .inventory-list .item-list { - list-style: none; - margin: 0; - padding: 0; -} .sw5e.sheet.actor .inventory-list .item-controls { flex: 0 0 44px; - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: flex-start; - justify-content: flex-end; -} -.sw5e.sheet.actor .inventory-list .item-controls > * { - flex: 1; -} -.sw5e.sheet.actor .inventory-list .item-controls .flex1 { - flex: 1; -} -.sw5e.sheet.actor .inventory-list .item-controls .flex2 { - flex: 2; -} -.sw5e.sheet.actor .inventory-list .item-controls .flex3 { - flex: 3; -} -.sw5e.sheet.actor .inventory-list .item-controls .flex4 { - flex: 4; -} -.sw5e.sheet.actor .inventory-list .item-controls a { - flex: 0 0 22px; - font-size: 12px; - text-align: center; - color: #7a7971; } .sw5e.sheet.actor .inventory-list .item-summary { flex: 0 0 100%; font-size: 12px; line-height: 16px; padding: 0.25em 0.5em; + color: #191813; border-top: 1px solid #c9c7b8; } .sw5e.sheet.actor .encumbrance { @@ -1034,42 +1044,25 @@ .sw5e.sheet.actor .powerbook-empty .item-controls { flex: 1; } -.sw5e.sheet.actor .effects .effect-name { - flex: 2; - align-items: center; - color: #191813; -} -.sw5e.sheet.actor .effects .effect-name h4 { - margin: 0; -} -.sw5e.sheet.actor .effects .effect-icon { - flex: 0 0 30px; - height: 30px; - margin-right: 5px; - border: none; -} -.sw5e.sheet.actor .effects .effect-source, -.sw5e.sheet.actor .effects .effect-duration { - text-align: center; - border-left: 1px solid #c9c7b8; - border-right: 1px solid #c9c7b8; -} -.sw5e.sheet.actor .effects .effect-controls { - flex: 0 0 60px; - text-align: right; -} -.sw5e.sheet.actor .effects .effect { - align-items: center; - border-bottom: 1px solid #c9c7b8; -} -.sw5e.sheet.actor .effects .effect:last-child { - border-bottom: none; -} .sw5e.sheet.actor .editor { padding: 0 8px; } +#actor-flags .window-content { + overflow-y: hidden; +} +#actor-flags form { + height: 100%; +} +#actor-flags .form-body { + height: calc(100% - 40px); + padding-right: 8px; + margin-bottom: 4px; + overflow-y: auto; +} .sw5e.sheet.item { - min-height: 420px; + min-height: 400px; + max-height: 95%; + min-width: 480px; /* ----------------------------------------- */ /* Sheet Header */ /* ----------------------------------------- */ @@ -1090,7 +1083,7 @@ border: 2px solid #000; } .sw5e.sheet.item .sheet-header .item-subtitle { - flex: 0 0 80px; + flex: 0 0 100px; height: 60px; margin: 0; padding: 5px; @@ -1165,14 +1158,15 @@ .sw5e.sheet.item .details .form-group.input-select-select select { flex: 1.5; } +.sw5e.sheet.item .details .form-group.uses-per .form-fields { + flex-wrap: nowrap; +} .sw5e.sheet.item .details .form-group.uses-per input { - flex: 1; + flex: 0 0 32px; } .sw5e.sheet.item .details .form-group.uses-per span { flex: 0 0 16px; -} -.sw5e.sheet.item .details .form-group.uses-per select { - flex: 3; + margin: 0 4px 0 0; } .sw5e.sheet.item .details span.sep { flex: 0 0 8px; @@ -1544,30 +1538,3 @@ max-width: 40px; text-align: right; } -input[type="number"] { - width: calc(100% - 2px); - min-width: 20px; - height: 26px; - background: rgba(0, 0, 0, 0.05); - padding: 1px 3px; - margin: 0; - color: #191813; - font-family: inherit; - font-size: inherit; - text-align: inherit; - line-height: inherit; - border: 1px solid #7a7971; - border-radius: 3px; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; - -moz-appearance: textfield; -} -input[type="number"]:focus { - box-shadow: 0 0 5px red; -} -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; -} diff --git a/sw5e.js b/sw5e.js index 8e17c450..335acd28 100644 --- a/sw5e.js +++ b/sw5e.js @@ -29,6 +29,7 @@ import ActorSheet5eCharacterNew from "./module/actor/sheets/newSheet/character.j import ItemSheet5e from "./module/item/sheet.js"; import ShortRestDialog from "./module/apps/short-rest.js"; import TraitSelector from "./module/apps/trait-selector.js"; +import MovementConfig from "./module/apps/movement-config.js"; // Import Helpers import * as chat from "./module/chat.js"; @@ -54,7 +55,8 @@ Hooks.once("init", function() { ActorSheet5eVehicle, ItemSheet5e, ShortRestDialog, - TraitSelector + TraitSelector, + MovementConfig }, canvas: { AbilityTemplate @@ -74,7 +76,7 @@ Hooks.once("init", function() { CONFIG.SW5E = SW5E; CONFIG.Actor.entityClass = Actor5e; CONFIG.Item.entityClass = Item5e; - if ( CONFIG.time ) CONFIG.time.roundTime = 6; // TODO remove conditional after 0.7.x + CONFIG.time.roundTime = 6; // Add DND5e namespace for module compatability game.dnd5e = game.sw5e; @@ -132,18 +134,18 @@ Hooks.once("setup", function() { // Localize CONFIG objects once up-front const toLocalize = [ - "abilities", "abilityAbbreviations", "alignments", "conditionTypes", "consumableTypes", "currencies", - "damageTypes", "damageResistanceTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", - "limitedUsePeriods", "senses", "skills", "powerComponents", "powerLevels", "powerPreparationModes", "powerSchools", - "powerScalingModes", "targetTypes", "timePeriods", "weaponProperties", "weaponTypes", "languages", - "polymorphSettings", "armorProficiencies", "weaponProficiencies", "toolProficiencies", "abilityActivationTypes", - "abilityConsumptionTypes", "actorSizes", "proficiencyLevels", "armorPropertiesTypes", "cover" + "abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments", + "armorProficiencies", "armorPropertiesTypes", "conditionTypes", "consumableTypes", "cover", "currencies", "damageResistanceTypes", + "damageTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", "languages", + "limitedUsePeriods", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills", + "powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes", + "timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponTypes" ]; // Exclude some from sorting where the default order matters const noSort = [ - "abilities", "alignments", "currencies", "distanceUnits", "itemActionTypes", "proficiencyLevels", - "limitedUsePeriods", "powerComponents", "powerLevels", "weaponTypes" + "abilities", "alignments", "currencies", "distanceUnits", "movementUnits", "itemActionTypes", "proficiencyLevels", + "limitedUsePeriods", "powerComponents", "powerLevels", "powerPreparationModes", "weaponTypes" ]; // Localize and sort CONFIG objects @@ -171,22 +173,23 @@ Hooks.once("setup", function() { */ Hooks.once("ready", function() { - // Determine whether a system migration is required and feasible - const currentVersion = game.settings.get("sw5e", "systemMigrationVersion"); - const NEEDS_MIGRATION_VERSION = 0.84; - const COMPATIBLE_MIGRATION_VERSION = 0.80; - let needMigration = (currentVersion < NEEDS_MIGRATION_VERSION) || (currentVersion === null); - - // Perform the migration - if ( needMigration && game.user.isGM ) { - if ( currentVersion && (currentVersion < COMPATIBLE_MIGRATION_VERSION) ) { - ui.notifications.error(`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.`, {permanent: true}); - } - migrations.migrateWorld(); - } - // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot)); + + // Determine whether a system migration is required and feasible + if ( !game.user.isGM ) return; + const currentVersion = game.settings.get("sw5e", "systemMigrationVersion"); + const NEEDS_MIGRATION_VERSION = "1.1.0"; + const COMPATIBLE_MIGRATION_VERSION = 0.80; + const needsMigration = currentVersion && isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion); + if ( !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(); }); /* -------------------------------------------- */ diff --git a/system.json b/system.json index a61e5c98..80ff336b 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "name": "sw5e", "title": "SW 5th Edition", "description": "A comprehensive game system for running games of SW 5th Edition in the Foundry VTT environment.", - "version": 0.98, + "version": "1.1.1", "author": "Dev Team", "scripts": [], "esmodules": ["sw5e.js"], @@ -56,17 +56,17 @@ "path": "./packs/packs/feats.db", "entity": "Item" }, - { + { "name": "fightingstyles", - "label": "Fighting Styles", - "path": "./packs/packs/fightingstyles.db", - "entity": "Item" + "label": "Fighting Styles", + "path": "./packs/packs/fightingstyles.db", + "entity": "Item" }, - { + { "name": "fightingmasteries", - "label": "Fighting Masteries", - "path": "./packs/packs/fightingmasteries.db", - "entity": "Item" + "label": "Fighting Masteries", + "path": "./packs/packs/fightingmasteries.db", + "entity": "Item" }, { "name": "forcepowers", @@ -103,20 +103,20 @@ "label": "Species Traits", "path": "./packs/packs/speciestraits.db", "entity": "Item" - }, - { + }, + { "name": "tables", "label": "Tables", "path": "./packs/packs/tables.db", "entity": "RollTable" - }, - { + }, + { "name": "techpowers", "label": "Tech Powers", "path": "./packs/packs/techpowers.db", "entity": "Item" - }, - { + }, + { "name": "weapons", "label": "Weapons", "path": "./packs/packs/weapons.db", @@ -135,8 +135,8 @@ "gridUnits": "ft", "primaryTokenAttribute": "attributes.hp", "secondaryTokenAttribute": null, - "minimumCoreVersion": "0.5.6", - "compatibleCoreVersion": "0.7.5", + "minimumCoreVersion": "0.7.6", + "compatibleCoreVersion": "0.7.6", "url": "https://github.com/unrealkakeman89/sw5e", "manifest": "https://raw.githubusercontent.com/unrealkakeman89/sw5e/master/system.json", "download": "https://github.com/unrealkakeman89/sw5e/archive/master.zip" diff --git a/template.json b/template.json index b88bf5d5..ea19a747 100644 --- a/template.json +++ b/template.json @@ -76,10 +76,18 @@ }, "creature": { "attributes": { + "movement": { + "burrow": 0, + "climb": 0, + "fly": 0, + "swim": 0, + "walk": 30, + "units": "ft", + "hover": false + }, "powercasting": "int", "speed": { - "value": "30 ft", - "special": "" + "_deprecated": true } }, "details": { @@ -481,14 +489,14 @@ "value": "" } }, - "speciesDescription": { - "data": "$characteristics-table", - "description": { - "value": "", - "chat": "", - "unidentified": "" - }, - "traits": "" + "speciesDescription": { + "data": "$characteristics-table", + "description": { + "value": "", + "chat": "", + "unidentified": "" + }, + "traits": "" }, "physicalItem": { "quantity": 1, @@ -560,14 +568,14 @@ } } }, - "archetype": { - "templates": ["archetypeDescription"], - "className": "", - "description": "" - }, - "background": { - "templates": ["backgroundDescription"] - }, + "archetype": { + "templates": ["archetypeDescription"], + "className": "", + "description": "" + }, + "background": { + "templates": ["backgroundDescription"] + }, "backpack": { "templates": ["itemDescription", "physicalItem"], "capacity": { @@ -593,19 +601,19 @@ }, "powercasting": "none" }, - "classfeature": { - "templates": ["itemDescription", "activatedEffect", "action"], - "className": "" - }, - "fightingmastery": { - "templates": ["fightingmasteryDescription"] - }, - "fightingstyle": { - "templates": ["fightingstyleDescription"] - }, - "lightsaberform": { - "templates": ["lightsaberformDescription"] - }, + "classfeature": { + "templates": ["itemDescription", "activatedEffect", "action"], + "className": "" + }, + "fightingmastery": { + "templates": ["fightingmasteryDescription"] + }, + "fightingstyle": { + "templates": ["fightingstyleDescription"] + }, + "lightsaberform": { + "templates": ["lightsaberformDescription"] + }, "consumable": { "templates": ["itemDescription", "physicalItem", "activatedEffect", "action"], "consumableType": "potion", @@ -641,7 +649,7 @@ }, "species": { "templates": ["speciesDescription"] - }, + }, "tool": { "templates": ["itemDescription", "physicalItem"], "ability": "int", diff --git a/templates/actors/oldActor/character-sheet.html b/templates/actors/oldActor/character-sheet.html index 0997fe68..33200116 100644 --- a/templates/actors/oldActor/character-sheet.html +++ b/templates/actors/oldActor/character-sheet.html @@ -82,25 +82,25 @@
  • -

    {{ localize "SW5E.Speed" }}

    +

    + {{ localize "SW5E.Speed" }} +

    - + {{movement.primary}}
    - + {{movement.special}}
  • -

    {{ localize "SW5E.Initiative" }}

    +

    {{ localize "SW5E.Initiative" }}

    {{numberFormat data.attributes.init.total decimals=0 sign=true}}
    {{ localize "SW5E.Modifier" }} -
  • @@ -108,17 +108,17 @@ - {{!-- NPC Sheet Navigation --}} + {{!-- Character Sheet Navigation --}} - {{!-- NPC Sheet Body --}} + {{!-- Character Sheet Body --}}
    @@ -183,7 +183,7 @@ {{!-- Counters --}}
    -

    {{ localize "SW5E.DeathSave" }}

    +

    {{ localize "SW5E.DeathSave" }}

    {{> "systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html"}}
    - + {{!-- Effects Tab --}}
    - {{> "systems/sw5e/templates/actors/oldActor/parts/actor-effects.html"}} + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}}
    - + {{!-- Biography Tab --}}
    diff --git a/templates/actors/oldActor/npc-sheet.html b/templates/actors/oldActor/npc-sheet.html index 6644268e..5b527165 100644 --- a/templates/actors/oldActor/npc-sheet.html +++ b/templates/actors/oldActor/npc-sheet.html @@ -62,14 +62,14 @@
  • -

    {{ localize "SW5E.Speed" }}

    +

    + {{ localize "SW5E.Speed" }} +

    - + {{movement.primary}}
    - + {{movement.special}}
  • @@ -81,6 +81,7 @@ {{ localize "SW5E.Attributes" }} {{ localize "SW5E.Features" }} {{ localize "SW5E.Powerbook" }} + {{ localize "SW5E.Effects" }} {{ localize "SW5E.Biography" }} @@ -143,24 +144,29 @@
    - -
    + +
    {{!-- Traits --}} - {{> "systems/sw5e/templates/actors/parts/actor-traits.html"}} + {{> "systems/sw5e/templates/actors/oldActor/parts/actor-traits.html"}}
    {{!-- Features Tab --}}
    - {{> "systems/sw5e/templates/actors/parts/actor-features.html" sections=features}} + {{> "systems/sw5e/templates/actors/oldActor/parts/actor-features.html" sections=features}}
    {{!-- Powerbook Tab --}}
    - {{> "systems/sw5e/templates/actors/parts/actor-powerbook.html"}} + {{> "systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html"}} +
    + + {{!-- Effects Tab --}} +
    + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}}
    {{!-- Biography Tab --}} @@ -168,4 +174,4 @@ {{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable}} - \ No newline at end of file + diff --git a/templates/actors/oldActor/parts/actor-effects.html b/templates/actors/oldActor/parts/actor-effects.html deleted file mode 100644 index 35d864a3..00000000 --- a/templates/actors/oldActor/parts/actor-effects.html +++ /dev/null @@ -1,28 +0,0 @@ -
      -{{#each effects as |section sid|}} -
    1. -

      {{localize section.label}}

      -
      Source
      -
      Duration
      -
      -
    2. - -
        - {{#each section.effects as |effect|}} -
      1. -
        - -

        {{effect.data.label}}

        -
        -
        {{effect.sourceName}}
        -
        {{effect.duration.label}}
        -
        - - - -
        -
      2. - {{/each}} -
      -{{/each}} -
    diff --git a/templates/actors/oldActor/parts/actor-features.html b/templates/actors/oldActor/parts/actor-features.html index e8d79f26..3b40f8b0 100644 --- a/templates/actors/oldActor/parts/actor-features.html +++ b/templates/actors/oldActor/parts/actor-features.html @@ -8,9 +8,9 @@ {{/unless}} -
      +
        {{#each sections as |section sid|}} -
      1. +
      2. {{localize section.label}}

        {{#if section.hasActions}} @@ -25,7 +25,7 @@ {{/if}} {{#if ../owner}} -
        +
        {{localize "SW5E.Add"}} @@ -78,7 +78,7 @@
        {{item.data.name}}
        - + {{else if section.isFightingstyles}}
        {{item.data.name}} @@ -119,7 +119,7 @@ {{/if}} {{#if ../../owner}} -
        + -
          +
            {{#each sections as |section sid|}} -
          1. +
          2. {{localize section.label}}

            {{#if section.columns}} @@ -33,18 +32,18 @@
            {{label}}
            {{/each}} {{else}} - {{#if ../isCharacter}} -
            {{localize "SW5E.Weight"}}
            - {{/if}} + {{#if ../isCharacter}} +
            {{localize "SW5E.Weight"}}
            + {{/if}} -
            {{localize "SW5E.Charges"}}
            -
            {{localize "SW5E.Usage"}}
            +
            {{localize "SW5E.Charges"}}
            +
            {{localize "SW5E.Usage"}}
            {{/if}} {{#if ../owner}} -
            + @@ -53,13 +52,13 @@
              {{#each section.items as |item iid|}} -
            1. -
              - {{#if section.editableName}} - - {{else}} -
              +
            2. +
              + {{#if section.editableName}} + + {{else}} +

              {{item.name~}} {{~#if item.isStack}} ({{item.data.quantity}}){{/if}} @@ -82,32 +81,32 @@

              {{/each}} {{else}} - {{#if ../../isCharacter}} -
              - {{#if item.totalWeight}} -
              - {{ item.totalWeight }} {{localize "SW5E.AbbreviationLbs"}} + {{#if ../../isCharacter}} +
              + {{#if item.totalWeight}} +
              + {{ item.totalWeight }} {{localize "SW5E.AbbreviationLbs"}} +
              + {{/if}} +
              + {{/if}} + +
              + {{#if item.hasUses }} + + / {{item.data.uses.max}} + {{/if}}
              - {{/if}} -
              - {{/if}} -
              - {{#if item.hasUses }} - - / {{item.data.uses.max}} - {{/if}} -
              - -
              - {{#if item.data.activation.type }} - {{item.labels.activation}} - {{/if}} -
              +
              + {{#if item.data.activation.type }} + {{item.labels.activation}} + {{/if}} +
              {{/if}} {{#if ../../owner}} -
              +
              {{#unless @root.isVehicle}} {{/unless}} diff --git a/templates/actors/oldActor/parts/actor-powerbook.html b/templates/actors/oldActor/parts/actor-powerbook.html index 1ed728bb..e551fe6f 100644 --- a/templates/actors/oldActor/parts/actor-powerbook.html +++ b/templates/actors/oldActor/parts/actor-powerbook.html @@ -27,12 +27,10 @@
              -
                +
                  {{#each powerbook as |section|}} -
                1. -
                  -

                  {{section.label}}

                  -
                  +
                2. +

                  {{section.label}}

                  {{#if section.usesSlots}} @@ -46,6 +44,7 @@ {{/if}} + {{ else }} {{{section.uses}}} / @@ -57,7 +56,7 @@
                  {{localize "SW5E.PowerUsage"}}
                  {{localize "SW5E.PowerTarget"}}
                  -
                3. {{localize "SW5E.FilterNoPowers"}}

                4. {{else}}
                5. -
                  + diff --git a/templates/actors/oldActor/parts/actor-traits.html b/templates/actors/oldActor/parts/actor-traits.html index 7bf70446..50a901eb 100644 --- a/templates/actors/oldActor/parts/actor-traits.html +++ b/templates/actors/oldActor/parts/actor-traits.html @@ -11,6 +11,11 @@
                  {{#unless isVehicle}} +
                  + + +
                  +
                  diff --git a/templates/actors/oldActor/vehicle-sheet.html b/templates/actors/oldActor/vehicle-sheet.html index 52e5d2dd..e8f11e13 100644 --- a/templates/actors/oldActor/vehicle-sheet.html +++ b/templates/actors/oldActor/vehicle-sheet.html @@ -58,8 +58,7 @@
                6. {{localize 'SW5E.Speed'}}

                  - +
                7. @@ -139,16 +138,16 @@
            - {{> 'systems/sw5e/templates/actors/parts/actor-traits.html'}} + {{> 'systems/sw5e/templates/actors/oldActor/parts/actor-traits.html'}}
        - {{> 'systems/sw5e/templates/actors/parts/actor-features.html' sections=features}} + {{> 'systems/sw5e/templates/actors/oldActor/parts/actor-features.html' sections=features}}
        - {{> 'systems/sw5e/templates/actors/parts/actor-inventory.html' sections=cargo}} + {{> 'systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html' sections=cargo}}
        diff --git a/templates/actors/parts/active-effects.html b/templates/actors/parts/active-effects.html new file mode 100644 index 00000000..b5ed128f --- /dev/null +++ b/templates/actors/parts/active-effects.html @@ -0,0 +1,38 @@ +
          +{{#each effects as |section sid|}} +
        1. +

          {{localize section.label}}

          +
          Source
          +
          Duration
          + +
        2. + +
            + {{#each section.effects as |effect|}} +
          1. +
            + +

            {{effect.data.label}}

            +
            +
            {{effect.sourceName}}
            +
            {{effect.duration.label}}
            + +
          2. + {{/each}} +
          +{{/each}} +
        diff --git a/templates/apps/actor-flags.html b/templates/apps/actor-flags.html index 2692bf90..2cbeeff4 100644 --- a/templates/apps/actor-flags.html +++ b/templates/apps/actor-flags.html @@ -1,44 +1,48 @@
        -

        {{localize 'SW5E.FlagsInstructions'}}

        +
        +

        {{localize 'SW5E.FlagsInstructions'}}

        - {{#each flags as |fs section|}} -

        {{localize section}}

        - {{#each fs as |flag key|}} + {{#each flags as |fs section|}} +

        {{localize section}}

        + {{#each fs as |flag key|}} -
        - +
        + - {{#if flag.isCheckbox}} - + {{#if flag.isCheckbox}} + - {{else if flag.isSelect}} - + {{else if flag.isSelect}} + - {{else}} - - {{/if}} + {{else}} + + {{/if}} -

        {{localize flag.hint}}

        -
        - {{/each}} - {{/each}} +

        {{localize flag.hint}}

        +
        + {{/each}} + {{/each}} -

        {{localize "SW5E.Bonuses"}}

        -

        {{localize "SW5E.BonusesHint"}}

        - {{#each bonuses as |b|}} -
        - - -
        - {{/each}} +

        {{localize "SW5E.Bonuses"}}

        +

        {{localize "SW5E.BonusesHint"}}

        + {{#each bonuses as |b|}} +
        + + +
        + {{/each}} +
        - +
        + +
        diff --git a/templates/apps/cast-cast.html b/templates/apps/cast-cast.html deleted file mode 100644 index 426b65d5..00000000 --- a/templates/apps/cast-cast.html +++ /dev/null @@ -1,34 +0,0 @@ -
        -

        {{ localize "SW5E.SpellCastHint" }} {{item.name}} {{ localize "SW5E.cast" }}.

        - - {{#unless canCast}} -

        {{ localize "SW5E.SpellCastNoSlots" }}

        - {{/unless}} - -
        - -
        - -
        -
        - -
        - {{#if canUpcast}} - - {{/if}} - - {{#if hasPlaceableTemplate}} -
        - -
        - {{/if}} -
        -
        diff --git a/templates/apps/movement-config.html b/templates/apps/movement-config.html new file mode 100644 index 00000000..75dd9e5f --- /dev/null +++ b/templates/apps/movement-config.html @@ -0,0 +1,34 @@ +
        +

        {{localize "SW5E.MovementConfigHint"}}

        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        + +
        diff --git a/templates/apps/trait-selector.html b/templates/apps/trait-selector.html index 012db93d..903556aa 100644 --- a/templates/apps/trait-selector.html +++ b/templates/apps/trait-selector.html @@ -15,5 +15,5 @@
        {{/if}} - + diff --git a/templates/chat/tool-card.html b/templates/chat/tool-card.html index 5953a8af..944cf982 100644 --- a/templates/chat/tool-card.html +++ b/templates/chat/tool-card.html @@ -1,7 +1,7 @@
        -

        {{item.name}}

        +

        {{item.name}}

        {{{data.description.value}}}
        diff --git a/templates/items/archetype.html b/templates/items/archetype.html index 85a77aa4..35c05b9c 100644 --- a/templates/items/archetype.html +++ b/templates/items/archetype.html @@ -31,6 +31,7 @@ {{!-- Item Sheet Navigation --}} @@ -39,7 +40,13 @@ {{!-- Description Tab --}}
        - {{editor content=data.description.value target="data.description.value" button=true owner=owner editable=editable}} -
        + {{editor content=data.description.value target="data.description.value" button=true owner=owner editable=editable}} +
        + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        + diff --git a/templates/items/background.html b/templates/items/background.html index 996253fb..515a11bd 100644 --- a/templates/items/background.html +++ b/templates/items/background.html @@ -24,6 +24,7 @@ {{!-- Item Sheet Navigation --}} @@ -32,68 +33,42 @@ {{!-- Description Tab --}}
        -
        {{editor content=data.flavorText.value target="data.flavorText.value" button=true editable=editable}}
        -

        Skill Proficiencies: {{{data.skillProficiencies.value}}}

        -

        Tool Proficiencies: {{{data.toolProficiencies.value}}}

        -

        Languages: {{{data.languages.value}}}

        -

        Equipment: {{{data.equipment.value}}}

        -

        {{{data.flavorName.value}}}

        -

        {{{data.flavorDescription.value}}}

        -

        {{{data.flavorOptions.value}}}

        -

        Feature: {{{data.featureName.value}}}

        -

        {{{data.featureText.value}}}

        -

        Background Feat

        -

        As a further embodiment of the experience and training of your background, you can choose from the following feats:

        -

        {{{data.featOptions.value}}}

        -

        Suggested Characteristics

        -

        {{{data.suggestedCharacteristics.value}}}

        -

        {{{data.personalityTraitOptions.value}}}

         

        -

        {{{data.idealOptions.value}}}

         

        -

        {{{data.flawOptions.value}}}

         

        -

        {{{data.bondOptions.value}}}

        +
        {{editor content=data.flavorText.value target="data.flavorText.value" button=true editable=editable}}
        +

        Skill Proficiencies: {{{data.skillProficiencies.value}}}

        +

        Tool Proficiencies: {{{data.toolProficiencies.value}}}

        +

        Languages: {{{data.languages.value}}}

        +

        Equipment: {{{data.equipment.value}}}

        +

        {{{data.flavorName.value}}}

        +

        {{{data.flavorDescription.value}}}

        +

        {{{data.flavorOptions.value}}}

        +

        Feature: {{{data.featureName.value}}}

        +

        {{{data.featureText.value}}}

        +

        Background Feat

        +

        As a further embodiment of the experience and training of your background, you can choose from the following feats:

        +

        {{{data.featOptions.value}}}

        +

        Suggested Characteristics

        +

        {{{data.suggestedCharacteristics.value}}}

        +

        {{{data.personalityTraitOptions.value}}}

         

        +

        {{{data.idealOptions.value}}}

         

        +

        {{{data.flawOptions.value}}}

         

        +

        {{{data.bondOptions.value}}}

        - -
        - + nullField.forEach(function(element) { + if (element.value === null) { + element.previousElementSibling.style.display = 'none'; + element.style.display = 'none'; + } + }); + +
        + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        + diff --git a/templates/items/cast.html b/templates/items/cast.html deleted file mode 100644 index b6a9905f..00000000 --- a/templates/items/cast.html +++ /dev/null @@ -1,151 +0,0 @@ -
        - - {{!-- Item Sheet Header --}} -
        - - -
        -

        - -

        - -
        -

        {{itemType}}

        - {{itemStatus}} -
        - -
          -
        • - {{labels.level}} -
        • -
        • - {{labels.school}} -
        • -
        • - -
        • -
        -
        -
        - - {{!-- Item Sheet Navigation --}} - - - {{!-- Item Sheet Body --}} -
        - - {{!-- Description Tab --}} - {{> "systems/sw5e/templates/items/parts/item-description.html"}} - - {{!-- Details Tab --}} -
        -

        {{ localize "SW5E.CastDetails" }}

        - - {{!-- Cast Level --}} -
        - - -
        - - {{!-- Cast School --}} -
        - - -
        - - {{!-- Cast Components --}} -
        - - - - - - -
        - - {{!-- Material Components --}} -
        - - - {{#if data.materials.value}} -
        - - - - - - -
        - {{/if}} -
        - - {{!-- Preparation Mode --}} -
        - -
        - - -
        -
        - -

        {{ localize "SW5E.CastingHeader" }}

        - - {{!-- Item Activation Template --}} - {{> "systems/sw5e/templates/items/parts/item-activation.html"}} - -

        {{ localize "SW5E.CastEffects" }}

        - - {{!-- Item Action Template --}} - {{> "systems/sw5e/templates/items/parts/item-action.html"}} - - {{!-- Cast Level Scaling --}} -
        - -
        - - -
        -
        -
        -
        -
        diff --git a/templates/items/classfeature.html b/templates/items/classfeature.html index 02bec146..d7c3510d 100644 --- a/templates/items/classfeature.html +++ b/templates/items/classfeature.html @@ -32,6 +32,7 @@ {{!-- Item Sheet Body --}} @@ -69,5 +70,11 @@ {{!-- Item Action Template --}} {{> "systems/sw5e/templates/items/parts/item-action.html"}}
        + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        + diff --git a/templates/items/consumable.html b/templates/items/consumable.html index a07f5687..01acd002 100644 --- a/templates/items/consumable.html +++ b/templates/items/consumable.html @@ -32,6 +32,7 @@ {{!-- Item Sheet Body --}} @@ -86,5 +87,10 @@ {{!-- Item Action Template --}} {{> "systems/sw5e/templates/items/parts/item-action.html"}}
        + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        diff --git a/templates/items/equipment.html b/templates/items/equipment.html index 42415b85..050658d8 100644 --- a/templates/items/equipment.html +++ b/templates/items/equipment.html @@ -32,6 +32,7 @@ {{!-- Item Sheet Body --}} @@ -142,5 +143,10 @@ {{!-- Item Action Template --}} {{> "systems/sw5e/templates/items/parts/item-action.html"}} + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        diff --git a/templates/items/feat.html b/templates/items/feat.html index b5eb2ce7..e2340d82 100644 --- a/templates/items/feat.html +++ b/templates/items/feat.html @@ -32,6 +32,7 @@ {{!-- Item Sheet Body --}} @@ -69,5 +70,11 @@ {{!-- Item Action Template --}} {{> "systems/sw5e/templates/items/parts/item-action.html"}} + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        + diff --git a/templates/items/fightingmastery.html b/templates/items/fightingmastery.html index 2b969dad..0da21baa 100644 --- a/templates/items/fightingmastery.html +++ b/templates/items/fightingmastery.html @@ -24,6 +24,7 @@ {{!-- Item Sheet Navigation --}} @@ -36,36 +37,11 @@ {{editor content=data.description.value target="data.description.value" button=true editable=editable}} - diff --git a/templates/items/fightingstyle.html b/templates/items/fightingstyle.html index 2b969dad..0da21baa 100644 --- a/templates/items/fightingstyle.html +++ b/templates/items/fightingstyle.html @@ -24,6 +24,7 @@ {{!-- Item Sheet Navigation --}} @@ -36,36 +37,11 @@ {{editor content=data.description.value target="data.description.value" button=true editable=editable}} - diff --git a/templates/items/lightsaberform.html b/templates/items/lightsaberform.html index 2b969dad..0da21baa 100644 --- a/templates/items/lightsaberform.html +++ b/templates/items/lightsaberform.html @@ -24,6 +24,7 @@ {{!-- Item Sheet Navigation --}} @@ -36,36 +37,11 @@ {{editor content=data.description.value target="data.description.value" button=true editable=editable}} - diff --git a/templates/items/loot.html b/templates/items/loot.html index 04604e15..3a0249dd 100644 --- a/templates/items/loot.html +++ b/templates/items/loot.html @@ -25,8 +25,26 @@ + {{!-- Item Sheet Navigation --}} + + {{!-- Item Sheet Body --}}
        - {{> "systems/sw5e/templates/items/parts/item-description.html"}} + + {{!-- Description Tab --}} +
        + {{> "systems/sw5e/templates/items/parts/item-description.html"}} + +
        + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        +
        diff --git a/templates/items/parts/item-activation.html b/templates/items/parts/item-activation.html index 0afc2d24..b0091a46 100644 --- a/templates/items/parts/item-activation.html +++ b/templates/items/parts/item-activation.html @@ -112,8 +112,9 @@
        - {{ localize "SW5E.of" }} + {{ localize "SW5E.of" }} + {{ localize "SW5E.per" }}
        + {{!-- Power Components --}} +
        + + +
        + + {{!-- Material Components --}} +
        + + + {{#if data.materials.value}} +
        + + + + + + +
        + {{/if}} +
        + {{!-- Preparation Mode --}}
        @@ -76,24 +101,10 @@ {{ localize "SW5E.PowerPrepared" }}
        - {{!-- Concentration Mode --}} -
        - -
        - -
        -

        {{ localize "SW5E.PowerCastingHeader" }}

        @@ -120,5 +131,10 @@ + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        diff --git a/templates/items/species.html b/templates/items/species.html index 16a90a29..e28a5164 100644 --- a/templates/items/species.html +++ b/templates/items/species.html @@ -26,6 +26,7 @@ {{!-- Item Sheet Body --}} @@ -110,8 +111,11 @@ {{editor content=data.traits.value target="data.traits.value" button=true editable=editable}} - + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        + - - diff --git a/templates/items/weapon.html b/templates/items/weapon.html index 57c9f6ca..40b0a69a 100644 --- a/templates/items/weapon.html +++ b/templates/items/weapon.html @@ -32,6 +32,7 @@ {{!-- Item Sheet Body --}} @@ -110,5 +111,10 @@ {{!-- Item Action Template --}} {{> "systems/sw5e/templates/items/parts/item-action.html"}} + + {{!-- Effects Tab --}} +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +