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"}} +